## 처리시간 지표 - 업무시간 기준(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>
121 lines
5.3 KiB
Python
121 lines
5.3 KiB
Python
import json
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, 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, require_manufacturer, get_current_user
|
|
from utils import save_upload
|
|
|
|
router = APIRouter(prefix="/api/improvements", tags=["improvements"])
|
|
|
|
def _fmt(imp: models.Improvement):
|
|
return {
|
|
"id": imp.id, "title": imp.title, "category": imp.category,
|
|
"description": imp.description, "priority": imp.priority,
|
|
"part_name": imp.part_name, "status": imp.status,
|
|
"manufacturer_id": imp.manufacturer_id,
|
|
"manufacturer_name": imp.manufacturer.name if imp.manufacturer else None,
|
|
"manufacturer_company": imp.manufacturer.company if imp.manufacturer else None,
|
|
"created_by_name": imp.creator.name if imp.creator else None,
|
|
"sw_deploy_target": str(imp.sw_deploy_target) if imp.sw_deploy_target else None,
|
|
"sw_deployed_at": str(imp.sw_deployed_at) if imp.sw_deployed_at else None,
|
|
"manufacturer_memo": imp.manufacturer_memo,
|
|
"created_at": imp.created_at.isoformat(),
|
|
"report_ids": [ir.report_id for ir in imp.report_links],
|
|
"report_count": len(imp.report_links),
|
|
"attachments": [{"path": a.file_path, "name": a.file_name} for a in imp.attachments],
|
|
"logs": [{"old": l.old_status, "new": l.new_status, "memo": l.memo,
|
|
"changed_at": l.changed_at.isoformat(),
|
|
"by": l.changer.name if l.changer else None} for l in imp.logs],
|
|
}
|
|
|
|
@router.get("")
|
|
def list_improvements(
|
|
status: Optional[str] = None, manufacturer_id: Optional[int] = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: models.User = Depends(get_current_user)
|
|
):
|
|
q = db.query(models.Improvement).order_by(desc(models.Improvement.created_at))
|
|
if current_user.role == "manufacturer":
|
|
q = q.filter(models.Improvement.manufacturer_id == current_user.id)
|
|
if status: q = q.filter(models.Improvement.status == status)
|
|
if manufacturer_id: q = q.filter(models.Improvement.manufacturer_id == manufacturer_id)
|
|
return [_fmt(imp) for imp in q.all()]
|
|
|
|
@router.get("/{imp_id}")
|
|
def get_improvement(imp_id: int, db: Session = Depends(get_db),
|
|
current_user: models.User = Depends(get_current_user)):
|
|
imp = db.query(models.Improvement).filter_by(id=imp_id).first()
|
|
if not imp: raise HTTPException(404)
|
|
if current_user.role == "manufacturer" and imp.manufacturer_id != current_user.id:
|
|
raise HTTPException(403)
|
|
return _fmt(imp)
|
|
|
|
@router.post("")
|
|
async def create_improvement(
|
|
title: str = Form(...), category: str = Form(...),
|
|
description: str = Form(...), priority: str = Form("normal"),
|
|
part_name: str = Form(""), manufacturer_id: int = Form(...),
|
|
report_ids: str = Form("[]"),
|
|
sw_deploy_target: Optional[str] = Form(None),
|
|
attachments: List[UploadFile] = File(default=[]),
|
|
db: Session = Depends(get_db),
|
|
current_user: models.User = Depends(require_admin)
|
|
):
|
|
imp = models.Improvement(
|
|
title=title, category=category, description=description,
|
|
priority=priority, part_name=part_name or None,
|
|
manufacturer_id=manufacturer_id, created_by=current_user.id,
|
|
sw_deploy_target=sw_deploy_target or None,
|
|
)
|
|
db.add(imp); db.commit(); db.refresh(imp)
|
|
|
|
for rid in json.loads(report_ids):
|
|
db.add(models.ImprovementReport(improvement_id=imp.id, report_id=int(rid)))
|
|
|
|
for f in attachments:
|
|
if f.filename:
|
|
path = save_upload(f, f"improvements/{imp.id}")
|
|
db.add(models.ImprovementAttachment(improvement_id=imp.id, file_path=path, file_name=f.filename))
|
|
|
|
db.add(models.ImprovementLog(improvement_id=imp.id, changed_by=current_user.id,
|
|
old_status=None, new_status="registered", memo="개선항목 등록"))
|
|
db.commit()
|
|
return {"id": imp.id}
|
|
|
|
@router.delete("/bulk")
|
|
def bulk_delete_improvements(
|
|
ids: List[int] = Body(...),
|
|
db: Session = Depends(get_db),
|
|
_=Depends(require_admin)
|
|
):
|
|
if not ids:
|
|
raise HTTPException(400, "삭제할 항목을 선택하세요.")
|
|
result = db.execute(text("DELETE FROM improvements WHERE id = ANY(:ids)"), {"ids": ids})
|
|
db.commit()
|
|
return {"deleted": result.rowcount}
|
|
|
|
@router.patch("/{imp_id}/status")
|
|
def update_status(
|
|
imp_id: int, status: str = Form(...), memo: str = Form(""),
|
|
sw_deployed_at: Optional[str] = Form(None),
|
|
manufacturer_memo: str = Form(""),
|
|
db: Session = Depends(get_db),
|
|
current_user: models.User = Depends(get_current_user)
|
|
):
|
|
imp = db.query(models.Improvement).filter_by(id=imp_id).first()
|
|
if not imp: raise HTTPException(404)
|
|
if current_user.role == "manufacturer" and imp.manufacturer_id != current_user.id:
|
|
raise HTTPException(403)
|
|
old_status = imp.status
|
|
imp.status = status
|
|
if sw_deployed_at: imp.sw_deployed_at = sw_deployed_at
|
|
if manufacturer_memo: imp.manufacturer_memo = manufacturer_memo
|
|
db.add(models.ImprovementLog(improvement_id=imp.id, changed_by=current_user.id,
|
|
old_status=old_status, new_status=status, memo=memo))
|
|
db.commit()
|
|
return {"ok": True}
|