## 처리시간 지표 - 업무시간 기준(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>
110 lines
4.7 KiB
Python
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}
|