import json from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlalchemy.orm import Session from sqlalchemy import desc from typing import List, Optional from datetime import datetime from database import get_db import models from auth import require_mechanic, require_admin, get_current_user from utils import save_upload router = APIRouter(prefix="/api/repairs", tags=["repairs"]) STATUS_MAP = { "done": "done", "waiting": "waiting", "revisit": "revisit", "in_progress": "in_progress", } def _fmt_repair(repair: models.Repair) -> dict: reports = [] charger_id = None station_name = None charger_name = None for link in repair.report_links: r = link.report if r: if not charger_id and r.charger: charger_id = r.charger_id station_name = r.charger.station_name charger_name = r.charger.name reports.append({ "id": r.id, "charger_id": r.charger_id, "issue_types": r.issue_types, "status": r.status, }) return { "id": repair.id, "charger_id": charger_id, "charger_name": charger_name, "station_name": station_name, "repair_types": repair.repair_types, "description": repair.description, "result_status": repair.result_status, "mechanic_lat": repair.mechanic_lat, "mechanic_lng": repair.mechanic_lng, "started_at": repair.started_at.isoformat(), "completed_at": repair.completed_at.isoformat() if repair.completed_at else None, "approved_at": repair.approved_at.isoformat() if repair.approved_at else None, "approved_by_name": repair.approver.name if repair.approved_by and repair.approver else None, "re_dispatch_requested": repair.re_dispatch_requested or False, "re_dispatch_requested_at": repair.re_dispatch_requested_at.isoformat() if repair.re_dispatch_requested_at else None, "photos_before": [{"id": p.id, "path": p.file_path} for p in repair.photos if p.photo_type == "before"], "photos_after": [{"id": p.id, "path": p.file_path} for p in repair.photos if p.photo_type == "after"], "reports": reports, "report_count": len(reports), } @router.get("/pending") def pending_reports(db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): """정비사용: 처리 가능한 신고 목록 (pending / in_progress)""" q = db.query(models.Report).filter( models.Report.status.in_(["pending", "in_progress"]) ).order_by(desc(models.Report.reported_at)) result = [] for r in q.all(): c = r.charger # in_progress 신고만 기존 repair 편집 모드; pending은 재조치 포함 새 조치 생성 repair_id = None if r.status == "in_progress" and r.repair_links: repair_id = r.repair_links[0].repair_id result.append({ "id": r.id, "charger_id": r.charger_id, "charger_name": c.name if c else None, "station_name": c.station_name if c else None, "charger_type": c.charger_type.name if c and c.charger_type else None, "issue_types": r.issue_types, "status": r.status, "reported_at": r.reported_at.isoformat(), "occurred_at": r.occurred_at.isoformat() if r.occurred_at else None, "repair_id": repair_id, "re_dispatch_count": r.re_dispatch_count or 0, "gps_lat": c.gps_lat if c else None, "gps_lng": c.gps_lng if c else None, "location_detail": c.location_detail if c else None, }) return result @router.get("/my") def my_repairs(db: Session = Depends(get_db), current_user: models.User = Depends(require_mechanic)): repairs = db.query(models.Repair).filter_by( mechanic_id=current_user.id ).order_by(desc(models.Repair.completed_at)).limit(100).all() return [_fmt_repair(r) for r in repairs] @router.get("/charger/{charger_id}/open") def open_reports_for_charger(charger_id: str, db: Session = Depends(get_db), _=Depends(require_mechanic)): """특정 충전기의 미처리 신고 목록 (중복처리용)""" reports = db.query(models.Report).filter( models.Report.charger_id == charger_id, models.Report.status.in_(["pending", "in_progress"]) ).order_by(models.Report.reported_at).all() return [{ "id": r.id, "issue_types": r.issue_types, "issue_detail": r.issue_detail, "status": r.status, "reported_at": r.reported_at.isoformat(), "photos": [p.file_path for p in r.photos], "re_dispatch_count": r.re_dispatch_count or 0, } for r in reports] @router.get("/{repair_id}") def get_repair(repair_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) if repair.mechanic_id != current_user.id and current_user.role != "admin": raise HTTPException(403, "접근 권한이 없습니다.") return _fmt_repair(repair) def _parse_dt(s: Optional[str]) -> Optional[datetime]: if not s: return None try: return datetime.fromisoformat(s) except Exception: return None @router.post("") async def create_repair( report_ids: str = Form(...), repair_types: str = Form(...), description: str = Form(...), result_status: str = Form("done"), started_at_input: Optional[str] = Form(None), completed_at_input: Optional[str] = Form(None), mechanic_lat: Optional[float] = Form(None), mechanic_lng: Optional[float] = Form(None), photos_before: List[UploadFile] = File(default=[]), photos_after: List[UploadFile] = File(default=[]), db: Session = Depends(get_db), current_user: models.User = Depends(require_mechanic) ): now = datetime.now() rids = json.loads(report_ids) repair = models.Repair( mechanic_id=current_user.id, repair_types=json.loads(repair_types), description=description, started_at=_parse_dt(started_at_input) or now, completed_at=_parse_dt(completed_at_input) or now, result_status=result_status, mechanic_lat=mechanic_lat, mechanic_lng=mechanic_lng, ) db.add(repair); db.commit(); db.refresh(repair) for rid in rids: r = db.query(models.Report).filter_by(id=rid).first() if r: r.status = STATUS_MAP.get(result_status, "in_progress") db.add(models.RepairReport(repair_id=repair.id, report_id=rid)) for photo in photos_before: if photo.filename: db.add(models.RepairPhoto(repair_id=repair.id, photo_type="before", file_path=save_upload(photo, f"repairs/{repair.id}"))) for photo in photos_after: if photo.filename: db.add(models.RepairPhoto(repair_id=repair.id, photo_type="after", file_path=save_upload(photo, f"repairs/{repair.id}"))) db.commit() return {"id": repair.id} @router.put("/{repair_id}") async def update_repair( repair_id: int, repair_types: str = Form(...), description: str = Form(...), result_status: str = Form("done"), started_at_input: Optional[str] = Form(None), completed_at_input: Optional[str] = Form(None), mechanic_lat: Optional[float] = Form(None), mechanic_lng: Optional[float] = Form(None), photos_before: List[UploadFile] = File(default=[]), photos_after: List[UploadFile] = File(default=[]), db: Session = Depends(get_db), current_user: models.User = Depends(require_mechanic) ): repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) if repair.mechanic_id != current_user.id: raise HTTPException(403, "본인 조치 이력만 수정할 수 있습니다.") if repair.approved_at: raise HTTPException(403, "관리자가 승인한 조치는 수정할 수 없습니다.") repair.repair_types = json.loads(repair_types) repair.description = description repair.result_status = result_status if started_at_input: repair.started_at = _parse_dt(started_at_input) or repair.started_at repair.completed_at = _parse_dt(completed_at_input) or datetime.now() if mechanic_lat is not None: repair.mechanic_lat = mechanic_lat if mechanic_lng is not None: repair.mechanic_lng = mechanic_lng for link in repair.report_links: r = link.report if r: r.status = STATUS_MAP.get(result_status, "in_progress") for photo in photos_before: if photo.filename: db.add(models.RepairPhoto(repair_id=repair_id, photo_type="before", file_path=save_upload(photo, f"repairs/{repair_id}"))) for photo in photos_after: if photo.filename: db.add(models.RepairPhoto(repair_id=repair_id, photo_type="after", file_path=save_upload(photo, f"repairs/{repair_id}"))) db.commit() return {"ok": True} @router.post("/{repair_id}/approve") def approve_repair( repair_id: int, improvement_action: str = Form("none"), # none | link | create improvement_id: Optional[int] = Form(None), imp_title: str = Form(""), imp_category: str = Form(""), imp_description: str = Form(""), imp_priority: str = Form("normal"), imp_manufacturer_id: Optional[int] = Form(None), db: Session = Depends(get_db), current_user: models.User = Depends(require_admin) ): repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) repair.approved_at = datetime.now() repair.approved_by = current_user.id target_imp_id = None if improvement_action == "link" and improvement_id: target_imp_id = improvement_id elif improvement_action == "create": if not imp_title or not imp_category or not imp_description: raise HTTPException(400, "개선항목 제목, 분류, 내용을 모두 입력해 주세요.") imp = models.Improvement( title=imp_title, category=imp_category, description=imp_description, priority=imp_priority, manufacturer_id=imp_manufacturer_id or None, created_by=current_user.id, ) db.add(imp); db.flush() db.add(models.ImprovementLog( improvement_id=imp.id, changed_by=current_user.id, old_status=None, new_status="registered", memo=f"조치 승인 시 생성 (수리 #{repair_id})" )) target_imp_id = imp.id if target_imp_id: for link in repair.report_links: exists = db.query(models.ImprovementReport).filter_by( improvement_id=target_imp_id, report_id=link.report_id ).first() if not exists: db.add(models.ImprovementReport( improvement_id=target_imp_id, report_id=link.report_id )) db.commit() return {"ok": True, "improvement_id": target_imp_id} @router.post("/{repair_id}/re-dispatch") def re_dispatch_repair( repair_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(require_admin) ): """기존 조치 기록을 유지하며 재조치 요청 — 신고를 pending으로 되돌림""" repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) if repair.approved_at: raise HTTPException(400, "이미 승인된 조치는 재조치 요청할 수 없습니다.") repair.re_dispatch_requested = True repair.re_dispatch_requested_at = datetime.now() for link in repair.report_links: if link.report: link.report.status = "pending" link.report.re_dispatch_count = (link.report.re_dispatch_count or 0) + 1 db.commit() return {"ok": True} @router.delete("/{repair_id}") def cancel_repair( repair_id: int, db: Session = Depends(get_db), _=Depends(require_admin) ): repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) # 연결된 신고를 접수(pending) 상태로 되돌림 for link in repair.report_links: if link.report: link.report.status = "pending" db.delete(repair) # cascade: RepairReport, RepairPhoto 자동 삭제 db.commit() return {"ok": True} @router.delete("/{repair_id}/photos/{photo_id}") def delete_repair_photo(repair_id: int, photo_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(require_mechanic)): repair = db.query(models.Repair).filter_by(id=repair_id).first() if not repair: raise HTTPException(404) if repair.mechanic_id != current_user.id and current_user.role != "admin": raise HTTPException(403) if repair.approved_at and current_user.role != "admin": raise HTTPException(403, "승인된 조치는 수정할 수 없습니다.") photo = db.query(models.RepairPhoto).filter_by(id=photo_id, repair_id=repair_id).first() if not photo: raise HTTPException(404) db.delete(photo); db.commit() return {"ok": True}