from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import or_, select from sqlalchemy.orm import Session from app.database import get_db from app.deps import current_user, require_roles from app.models import ( AttendancePoint, DeviceQRCode, Equipment, NoticePoint, PersonAttendancePoint, Personnel, Product, ProductionReport, ProductionReportItem, ReconciliationLedgerEntry, Role, WorkSession, WorkSessionDevice, ) from app.schemas import AttendancePointCreate, AttendancePointOut, PageResponse from app.services.attendance_points import accessible_points, clean_point_name, ensure_default_attendance_point from app.services.common import paginate from app.services.misc_work import ensure_misc_work_product from app.services.serializers import attendance_point_out from app.services.work_schedule import normalize_schedule_payload router = APIRouter(prefix="/api/attendance-points", tags=["attendance-points"]) def _validate_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, ) @router.get("/accessible", response_model=list[AttendancePointOut]) def list_accessible_attendance_points( user: Personnel = Depends(current_user), db: Session = Depends(get_db), ) -> list[AttendancePointOut]: return [attendance_point_out(point) for point in accessible_points(db, user)] @router.get("/public", response_model=list[AttendancePointOut]) def list_public_attendance_points( db: Session = Depends(get_db), ) -> list[AttendancePointOut]: points = db.scalars( select(AttendancePoint) .where(AttendancePoint.is_active.is_(True)) .order_by(AttendancePoint.name.asc()) ).all() rows = [] for point in points: item = attendance_point_out(point) item.remark = None item.created_at = None item.updated_at = None rows.append(item) return rows @router.get("", response_model=PageResponse) def list_attendance_points( keyword: str = "", page: int = Query(1, ge=1), page_size: int = Query(100, ge=1, le=500), _: Personnel = Depends(require_roles(Role.manager)), db: Session = Depends(get_db), ) -> PageResponse: default_point = ensure_default_attendance_point(db) ensure_misc_work_product(db, default_point.name) db.commit() query = select(AttendancePoint).order_by(AttendancePoint.updated_at.desc(), AttendancePoint.name.asc()) if keyword: like = f"%{keyword}%" query = query.where(or_(AttendancePoint.name.like(like), AttendancePoint.remark.like(like))) result = paginate(db, query, page, page_size) return PageResponse(**{**result, "rows": [attendance_point_out(row) for row in result["rows"]]}) @router.post("", response_model=AttendancePointOut) def save_attendance_point( payload: AttendancePointCreate, _: Personnel = Depends(require_roles(Role.manager)), db: Session = Depends(get_db), ) -> AttendancePointOut: name = _validate_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.commit() db.refresh(point) return attendance_point_out(point) @router.delete("/{name}", status_code=204) def delete_attendance_point( name: str, _: Personnel = Depends(require_roles(Role.manager)), db: Session = Depends(get_db), ) -> None: point_name = clean_point_name(name) point = db.get(AttendancePoint, point_name) if point is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="考勤点不存在") if point_name == ensure_default_attendance_point(db).name: raise HTTPException(status_code=400, detail="默认考勤点不能删除") point.is_active = False db.commit()