169 lines
6.1 KiB
Python
169 lines
6.1 KiB
Python
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()
|