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_admin, get_current_user from utils import save_upload router = APIRouter(prefix="/api/reports", tags=["reports"]) def _fmt_report(r: models.Report, db: Session): c = r.charger repair_id = None if r.repair_links: repair_id = r.repair_links[0].repair_id return { "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, "cpo_name": c.cpo_name if c else None, "charger_type": c.charger_type.name if c and c.charger_type else None, "installed_at": str(c.installed_at) if c and c.installed_at else None, "issue_types": r.issue_types, "issue_detail": r.issue_detail, "error_code": r.error_code, "contact": r.contact, "occurred_at": r.occurred_at.isoformat() if r.occurred_at else None, "reported_at": r.reported_at.isoformat() if r.reported_at else None, "gps_lat": r.gps_lat, "gps_lng": r.gps_lng, "status": r.status, "photos": [p.file_path for p in r.photos], "repair_id": repair_id, } @router.post("") async def create_report( charger_id: str = Form(...), issue_types: str = Form(...), # JSON 배열 문자열 issue_detail: str = Form(""), error_code: str = Form(""), occurred_at: Optional[str] = Form(None), contact: str = Form(""), consent: bool = Form(False), gps_lat: Optional[float] = Form(None), gps_lng: Optional[float] = Form(None), photos: List[UploadFile] = File(default=[]), db: Session = Depends(get_db) ): import json charger = db.query(models.Charger).filter_by(id=charger_id).first() if not charger: raise HTTPException(404, "충전기를 찾을 수 없습니다.") # 신고 공개 정책 확인 setting = db.query(models.SystemSetting).filter_by(key="report_visibility_policy").first() policy = setting.value if setting else "immediate" initial_status = "pending_approval" if policy == "admin_approval" else "pending" issue_list = json.loads(issue_types) if isinstance(issue_types, str) else issue_types r = models.Report( charger_id=charger_id, issue_types=issue_list, issue_detail=issue_detail or None, error_code=error_code or None, occurred_at=datetime.fromisoformat(occurred_at) if occurred_at else None, contact=contact or None, consent=consent, gps_lat=gps_lat, gps_lng=gps_lng, status=initial_status ) db.add(r); db.commit(); db.refresh(r) for photo in photos: if photo.filename: path = save_upload(photo, f"reports/{r.id}") db.add(models.ReportPhoto(report_id=r.id, file_path=path)) db.commit() return {"id": r.id, "status": r.status} @router.get("") def list_reports( status: Optional[str] = None, charger_id: Optional[str] = None, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user) ): q = db.query(models.Report).order_by(desc(models.Report.reported_at)) if status: q = q.filter(models.Report.status == status) if charger_id: q = q.filter(models.Report.charger_id == charger_id) # 정비사는 공개된 것만 (승인 대기 제외) if current_user.role == "mechanic": q = q.filter(models.Report.status != "pending_approval") return [_fmt_report(r, db) for r in q.all()] @router.get("/{report_id}") def get_report(report_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)): r = db.query(models.Report).filter_by(id=report_id).first() if not r: raise HTTPException(404) result = _fmt_report(r, db) # 수리 정보 포함 if r.repair_links: repair = r.repair_links[0].repair cost = repair.cost result["repair"] = { "id": repair.id, "mechanic_name": repair.mechanic.name if repair.mechanic else None, "mechanic_company": repair.mechanic.company if repair.mechanic else None, "repair_types": repair.repair_types, "description": repair.description, "started_at": repair.started_at.isoformat(), "completed_at": repair.completed_at.isoformat() if repair.completed_at else None, "result_status": repair.result_status, "photos_before": [p.file_path for p in repair.photos if p.photo_type == "before"], "photos_after": [p.file_path for p in repair.photos if p.photo_type == "after"], "cost": { "root_cause": cost.root_cause, "admin_note": cost.admin_note, "cost_party_type": cost.cost_party_type, "cost_party_custom": cost.cost_party_custom, "cost_amount": cost.cost_amount, "cost_status": cost.cost_status, "manufacturer_name": cost.manufacturer.name if cost.manufacturer else None, } if cost else None } return result @router.patch("/{report_id}/approve") def approve_report(report_id: int, db: Session = Depends(get_db), _=Depends(require_admin)): r = db.query(models.Report).filter_by(id=report_id).first() if not r: raise HTTPException(404) r.status = "pending"; db.commit() return {"ok": True} @router.patch("/{report_id}/status") def update_status(report_id: int, status: str = Form(...), db: Session = Depends(get_db), _=Depends(require_admin)): r = db.query(models.Report).filter_by(id=report_id).first() if not r: raise HTTPException(404) r.status = status; db.commit() return {"ok": True} # ── 공개 엔드포인트 — 인증 없이 특정 충전기의 진행 중 신고 조회 ── # QR 신고 페이지에서 기존 접수 현황을 사용자에게 보여줄 때 사용 @router.get("/public/{charger_id}") def public_charger_reports(charger_id: str, db: Session = Depends(get_db)): """ 해당 충전기에서 아직 해결되지 않은 신고 목록을 반환. 완료(done) / 면제 · 정산 상태는 제외하고 진행 중인 것만 반환. 개인정보(연락처) 는 반환하지 않음. """ active_statuses = ["pending_approval", "pending", "in_progress", "waiting", "revisit"] rows = ( db.query(models.Report) .filter( models.Report.charger_id == charger_id, models.Report.status.in_(active_statuses), ) .order_by(models.Report.reported_at.desc()) .limit(20) .all() ) STATUS_LABEL = { "pending_approval": "검토 대기", "pending": "접수 완료", "in_progress": "처리 중", "waiting": "부품 대기", "revisit": "재방문 예정", } result = [] for r in rows: repair = r.repair_links[0].repair if r.repair_links else None result.append({ "id": r.id, "issue_types": r.issue_types, "issue_detail": r.issue_detail or "", "status": r.status, "status_label": STATUS_LABEL.get(r.status, r.status), "reported_at": r.reported_at.isoformat() if r.reported_at else "", "occurred_at": r.occurred_at.isoformat() if r.occurred_at else "", "photo_count": len(r.photos), "mechanic_name": repair.mechanic.name if repair and repair.mechanic else None, "started_at": repair.started_at.isoformat() if repair and repair.started_at else None, }) return result