Files
ev-charger-as/backend/routers/costs.py
byun 2e8751ea6c 기능 추가 및 버그 수정 — 처리시간 지표, 대시보드 차트, UI 개선
## 처리시간 지표
- 업무시간 기준(09-18 평일) / 공휴일 제외 24h / 달력 기준 3가지 모드 선택
- 공휴일 DB 관리 (holidays 테이블, 수동 등록·삭제·일괄 추가)
- 2026년 공휴일 등록 지원
- 설정 페이지에서 라디오 버튼으로 모드 선택

## 대시보드 차트
- 월별 평균 처리시간 막대 차트 추가
- 월별 신고 접수 건수 누적 막대 차트 추가
- 월별 → 일별 드릴다운 (막대 클릭 시 해당 월의 일별 차트로 전환)
- 일별 막대 클릭 시 처리 완료/신고 접수 상세 내역 모달
- 충전기별 누적 고장 건수 Top 10 수평 막대 차트 추가

## 신고 목록
- # 컬럼을 DB PK 대신 현재 목록 순서(1, 2, 3…)로 표시
- 엑셀 export 접수번호도 순차번호로 변경

## 모바일 네비게이션 버그 수정
- 모바일에서 가로 오버플로우 시 nav가 body 넓이로 늘어나
  햄버거 버튼이 화면 밖으로 밀리는 문제 수정
- nav를 position:fixed + body padding-top:54px 로 변경 (전체 페이지 적용)
- 충전기 관리·신고 목록 페이지 지도 컨테이너에 isolation:isolate 적용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 06:52:56 +09:00

110 lines
4.7 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Form, Body
from sqlalchemy.orm import Session
from sqlalchemy import desc, text
from typing import List, Optional
from datetime import datetime
from database import get_db
import models
from auth import require_admin
router = APIRouter(prefix="/api/costs", tags=["costs"])
@router.get("")
def list_costs(
cost_status: Optional[str] = None,
cost_party_type: Optional[str] = None,
db: Session = Depends(get_db), _=Depends(require_admin)
):
q = db.query(models.RepairCost).join(models.Repair)
if cost_status: q = q.filter(models.RepairCost.cost_status == cost_status)
if cost_party_type: q = q.filter(models.RepairCost.cost_party_type == cost_party_type)
q = q.order_by(desc(models.RepairCost.reviewed_at))
result = []
for cost in q.all():
repair = cost.repair
rids = [rr.report_id for rr in repair.report_links]
charger_id, station_name, charger_type = None, None, None
if rids:
r = db.query(models.Report).filter_by(id=rids[0]).first()
if r and r.charger:
charger_id = r.charger_id
station_name = r.charger.station_name
charger_type = r.charger.charger_type.name if r.charger.charger_type else None
result.append({
"id": cost.id, "repair_id": cost.repair_id,
"report_ids": rids, "charger_id": charger_id,
"station_name": station_name, "charger_type": charger_type,
"mechanic_name": repair.mechanic.name if repair.mechanic else None,
"mechanic_company": repair.mechanic.company if repair.mechanic else None,
"completed_at": repair.completed_at.isoformat() if repair.completed_at else None,
"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,
"reviewed_by_name": cost.reviewer.name if cost.reviewer else None,
"reviewed_at": cost.reviewed_at.isoformat() if cost.reviewed_at else None,
})
return result
@router.delete("/bulk")
def bulk_delete_costs(
ids: List[int] = Body(...),
db: Session = Depends(get_db),
_=Depends(require_admin)
):
if not ids:
raise HTTPException(400, "삭제할 항목을 선택하세요.")
result = db.execute(text("DELETE FROM repair_costs WHERE id = ANY(:ids)"), {"ids": ids})
db.commit()
return {"deleted": result.rowcount}
@router.get("/stats")
def cost_stats(db: Session = Depends(get_db), _=Depends(require_admin)):
from sqlalchemy import func, extract
now = datetime.now()
monthly = db.query(func.sum(models.RepairCost.cost_amount)).filter(
extract('year', models.RepairCost.reviewed_at) == now.year,
extract('month', models.RepairCost.reviewed_at) == now.month,
).scalar() or 0
pending = db.query(models.RepairCost).filter_by(cost_status="pending").count()
return {"monthly_total": monthly, "pending_count": pending}
@router.post("/repair/{repair_id}")
def upsert_cost(
repair_id: int,
root_cause: str = Form(""),
admin_note: str = Form(""),
cost_party_type: str = Form(...),
cost_party_manufacturer_id: Optional[int] = Form(None),
cost_party_custom: str = Form(""),
cost_amount: int = Form(0),
cost_status: str = Form("pending"),
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, "조치 내역을 찾을 수 없습니다.")
cost = db.query(models.RepairCost).filter_by(repair_id=repair_id).first()
if cost:
cost.root_cause = root_cause; cost.admin_note = admin_note
cost.cost_party_type = cost_party_type
cost.cost_party_manufacturer_id = cost_party_manufacturer_id or None
cost.cost_party_custom = cost_party_custom or None
cost.cost_amount = cost_amount; cost.cost_status = cost_status
cost.reviewed_by = current_user.id; cost.reviewed_at = datetime.now()
else:
cost = models.RepairCost(
repair_id=repair_id, root_cause=root_cause, admin_note=admin_note,
cost_party_type=cost_party_type,
cost_party_manufacturer_id=cost_party_manufacturer_id or None,
cost_party_custom=cost_party_custom or None,
cost_amount=cost_amount, cost_status=cost_status,
reviewed_by=current_user.id, reviewed_at=datetime.now()
)
db.add(cost)
db.commit()
return {"ok": True}