from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.orm import Session from app.database import get_db from app.deps import require_roles from app.models import ( AttendancePoint, DeviceQRCode, Equipment, NoticePoint, PersonAttendancePoint, Personnel, Product, ProductionReport, ProductionReportItem, ReconciliationLedgerEntry, Role, WorkSchedule, WorkSession, WorkSessionDevice, ) from app.schemas import AttendancePointCreate, WorkScheduleOut, WorkScheduleSettingsUpdate, WorkScheduleUpdate from app.services.attendance_points import clean_point_name, ensure_default_attendance_point from app.services.misc_work import ensure_misc_work_product from app.services.serializers import attendance_point_out from app.services.work_schedule import get_work_schedule, normalize_schedule_payload, update_work_schedule router = APIRouter(prefix="/api/work-schedule", tags=["work-schedule"]) def _schedule_out(schedule: WorkSchedule, db: Session | None = None) -> WorkScheduleOut: points = [] if db is not None: ensure_default_attendance_point(db) db.commit() points = [ attendance_point_out(point) for point in db.scalars( select(AttendancePoint) .where(AttendancePoint.is_active.is_(True)) .order_by(AttendancePoint.name) ).all() ] return WorkScheduleOut( id=schedule.id, day_start=schedule.day_start, day_end=schedule.day_end, lunch_start=schedule.lunch_start, lunch_end=schedule.lunch_end, dinner_start=schedule.dinner_start, dinner_end=schedule.dinner_end, overtime_start=schedule.overtime_start, overtime_end=schedule.overtime_end, night_start=schedule.night_start, night_end=schedule.night_end, attendance_latitude=schedule.attendance_latitude, attendance_longitude=schedule.attendance_longitude, attendance_radius_meters=schedule.attendance_radius_meters, auto_submit_hours=float(schedule.auto_submit_hours or 15), attendance_points=points, updated_by=schedule.updated_by, updated_at=schedule.updated_at, ) def _validate_attendance_point_payload(payload: AttendancePointCreate) -> str: name = clean_point_name(payload.name) if not name: raise HTTPException(status_code=400, detail="请输入考勤点名称") if (payload.latitude is None) != (payload.longitude is None): raise HTTPException(status_code=400, detail="经纬度必须同时填写") if payload.latitude is not None and not (-90 <= payload.latitude <= 90): raise HTTPException(status_code=400, detail="纬度范围为-90到90") if payload.longitude is not None and not (-180 <= payload.longitude <= 180): raise HTTPException(status_code=400, detail="经度范围为-180到180") if int(payload.radius_meters or 0) <= 0: raise HTTPException(status_code=400, detail="考勤范围半径必须大于0米") try: normalize_schedule_payload(payload) except ValueError as exc: raise HTTPException(status_code=400, detail=f"{name} {exc}") from exc return name def _rename_business_point(db: Session, old_name: str, new_name: str) -> None: if old_name == new_name: return for model in ( PersonAttendancePoint, Product, Equipment, WorkSession, WorkSessionDevice, ProductionReport, ProductionReportItem, DeviceQRCode, NoticePoint, ReconciliationLedgerEntry, ): db.query(model).filter(model.attendance_point_name == old_name).update( {model.attendance_point_name: new_name}, synchronize_session=False, ) def _save_attendance_point_without_commit(db: Session, payload: AttendancePointCreate) -> AttendancePoint: name = _validate_attendance_point_payload(payload) original_name = clean_point_name(payload.original_name) or name point = db.get(AttendancePoint, original_name) if point is not None and original_name != name: if db.get(AttendancePoint, name) is not None: raise HTTPException(status_code=409, detail="该考勤点名称已存在") next_point = AttendancePoint(name=name) db.add(next_point) db.flush() _rename_business_point(db, original_name, name) db.delete(point) point = next_point elif point is None: point = db.get(AttendancePoint, name) if point is None: point = AttendancePoint(name=name) db.add(point) db.flush() point.latitude = payload.latitude point.longitude = payload.longitude point.radius_meters = int(payload.radius_meters or 500) for field, value in normalize_schedule_payload(payload).items(): setattr(point, field, value) point.remark = payload.remark point.is_active = bool(payload.is_active) ensure_misc_work_product(db, name) db.flush() return point @router.get("", response_model=WorkScheduleOut) def read_work_schedule( db: Session = Depends(get_db), ) -> WorkScheduleOut: return _schedule_out(get_work_schedule(db), db) @router.put("", response_model=WorkScheduleOut) def save_work_schedule( payload: WorkScheduleUpdate, user: Personnel = Depends(require_roles(Role.manager)), db: Session = Depends(get_db), ) -> WorkScheduleOut: try: schedule = update_work_schedule(db, payload, user.phone) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return _schedule_out(schedule, db) @router.put("/settings", response_model=WorkScheduleOut) def save_work_schedule_settings( payload: WorkScheduleSettingsUpdate, user: Personnel = Depends(require_roles(Role.manager)), db: Session = Depends(get_db), ) -> WorkScheduleOut: names = [_validate_attendance_point_payload(point) for point in payload.attendance_points] duplicates = sorted({name for name in names if names.count(name) > 1}) if duplicates: raise HTTPException(status_code=400, detail=f"考勤点名称重复:{'、'.join(duplicates)}") if not names: raise HTTPException(status_code=400, detail="请至少维护一个考勤点") try: schedule = update_work_schedule(db, payload, user.phone, commit=False) for point in payload.attendance_points: _save_attendance_point_without_commit(db, point) db.commit() db.refresh(schedule) except ValueError as exc: db.rollback() raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc except HTTPException: db.rollback() raise return _schedule_out(schedule, db)