초기 커밋 - EV AS 관리 시스템
This commit is contained in:
183
backend/routers/reports.py
Normal file
183
backend/routers/reports.py
Normal file
@@ -0,0 +1,183 @@
|
||||
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
|
||||
Reference in New Issue
Block a user