정비사가 조치 입력 시 시작·완료 시각을 직접 수정할 수 있도록 변경. 현장 처리 후 나중에 입력하는 경우 실제 조치 시간을 정확히 기록 가능. - repair.html: 자동기록 안내문구 → datetime-local 입력 필드 2개로 교체 (페이지 로드 시 현재 시각 기본 설정, 편집 모드에서 기존 값 복원) - repairs.py: POST/PUT 엔드포인트에 started_at_input / completed_at_input Form 파라미터 추가, 미입력 시 datetime.now() 유지 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
334 lines
13 KiB
Python
334 lines
13 KiB
Python
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],
|
|
} 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}
|