|
|
@@ -0,0 +1,265 @@
|
|
|
+from datetime import datetime, date
|
|
|
+from typing import Optional
|
|
|
+from uuid import UUID
|
|
|
+from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
+from sqlalchemy import select, delete
|
|
|
+from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
+from backend.app.core.dependencies import require_permissions, get_current_user
|
|
|
+from backend.app.db.session import get_db
|
|
|
+from backend.app.models import ScheduleItem, AdjustLog, User
|
|
|
+from backend.app.schemas.schedule import (
|
|
|
+ ScheduleCreate,
|
|
|
+ ScheduleResponse,
|
|
|
+ ScheduleUpdate,
|
|
|
+ SwapRequest,
|
|
|
+ SubstituteRequest,
|
|
|
+ StopClinicRequest
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+router = APIRouter(prefix="/schedule", tags=["schedule"])
|
|
|
+
|
|
|
+
|
|
|
+def is_limited_scope(user: User) -> bool:
|
|
|
+ return user.role and user.role.name not in {"管理员", "排班员"}
|
|
|
+
|
|
|
+
|
|
|
+async def ensure_unique_schedule(
|
|
|
+ db: AsyncSession,
|
|
|
+ dept_id: UUID,
|
|
|
+ schedule_date: date,
|
|
|
+ shift_id: Optional[UUID],
|
|
|
+ staff_id: Optional[UUID],
|
|
|
+ exclude_id: Optional[UUID] = None,
|
|
|
+) -> None:
|
|
|
+ if not shift_id or not staff_id:
|
|
|
+ return
|
|
|
+ query = select(ScheduleItem.id).where(
|
|
|
+ ScheduleItem.dept_id == dept_id,
|
|
|
+ ScheduleItem.date == schedule_date,
|
|
|
+ ScheduleItem.shift_id == shift_id,
|
|
|
+ ScheduleItem.staff_id == staff_id,
|
|
|
+ )
|
|
|
+ if exclude_id:
|
|
|
+ query = query.where(ScheduleItem.id != exclude_id)
|
|
|
+ result = await db.execute(query)
|
|
|
+ if result.scalar_one_or_none():
|
|
|
+ raise HTTPException(status_code=409, detail="该人员在此班次已有排班")
|
|
|
+
|
|
|
+
|
|
|
+@router.get("", response_model=list[ScheduleResponse], dependencies=[Depends(require_permissions(["schedule.view"]))])
|
|
|
+async def list_schedule(
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+ dept_id: Optional[UUID] = Query(default=None),
|
|
|
+ start: Optional[str] = Query(default=None),
|
|
|
+ end: Optional[str] = Query(default=None),
|
|
|
+):
|
|
|
+ query = select(ScheduleItem)
|
|
|
+ if is_limited_scope(current_user):
|
|
|
+ if not current_user.dept_id:
|
|
|
+ return []
|
|
|
+ query = query.where(ScheduleItem.dept_id == current_user.dept_id)
|
|
|
+ elif dept_id:
|
|
|
+ query = query.where(ScheduleItem.dept_id == dept_id)
|
|
|
+ if start:
|
|
|
+ query = query.where(ScheduleItem.date >= date.fromisoformat(start))
|
|
|
+ if end:
|
|
|
+ query = query.where(ScheduleItem.date <= date.fromisoformat(end))
|
|
|
+ result = await db.execute(query.order_by(ScheduleItem.date))
|
|
|
+ return result.scalars().all()
|
|
|
+
|
|
|
+
|
|
|
+@router.post("", response_model=ScheduleResponse, dependencies=[Depends(require_permissions(["schedule.create"]))])
|
|
|
+async def create_schedule(
|
|
|
+ payload: ScheduleCreate,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ if is_limited_scope(current_user) and payload.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ schedule_date = date.fromisoformat(payload.date)
|
|
|
+ await ensure_unique_schedule(db, payload.dept_id, schedule_date, payload.shift_id, payload.staff_id)
|
|
|
+ item = ScheduleItem(
|
|
|
+ dept_id=payload.dept_id,
|
|
|
+ date=schedule_date,
|
|
|
+ shift_id=payload.shift_id,
|
|
|
+ staff_id=payload.staff_id,
|
|
|
+ tag=payload.tag,
|
|
|
+ note=payload.note,
|
|
|
+ original_staff_id=payload.original_staff_id,
|
|
|
+ substitute_for_id=payload.substitute_for_id,
|
|
|
+ reason=payload.reason
|
|
|
+ )
|
|
|
+ db.add(item)
|
|
|
+ await db.commit()
|
|
|
+ await db.refresh(item)
|
|
|
+ return item
|
|
|
+
|
|
|
+
|
|
|
+@router.put("/{item_id}", response_model=ScheduleResponse, dependencies=[Depends(require_permissions(["schedule.edit"]))])
|
|
|
+async def update_schedule(
|
|
|
+ item_id: UUID,
|
|
|
+ payload: ScheduleUpdate,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
|
|
|
+ item = result.scalar_one_or_none()
|
|
|
+ if not item:
|
|
|
+ raise HTTPException(status_code=404, detail="排班不存在")
|
|
|
+ if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ update_data = payload.model_dump(exclude_unset=True)
|
|
|
+ if update_data:
|
|
|
+ next_shift_id = update_data.get("shift_id", item.shift_id)
|
|
|
+ next_staff_id = update_data.get("staff_id", item.staff_id)
|
|
|
+ await ensure_unique_schedule(db, item.dept_id, item.date, next_shift_id, next_staff_id, exclude_id=item.id)
|
|
|
+ for key, value in update_data.items():
|
|
|
+ setattr(item, key, value)
|
|
|
+ await db.commit()
|
|
|
+ await db.refresh(item)
|
|
|
+ return item
|
|
|
+
|
|
|
+
|
|
|
+@router.delete("/{item_id}", dependencies=[Depends(require_permissions(["schedule.delete"]))])
|
|
|
+async def delete_schedule(
|
|
|
+ item_id: UUID,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
|
|
|
+ item = result.scalar_one_or_none()
|
|
|
+ if not item:
|
|
|
+ raise HTTPException(status_code=404, detail="排班不存在")
|
|
|
+ if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ await db.delete(item)
|
|
|
+ await db.commit()
|
|
|
+ return {"success": True}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/swap", dependencies=[Depends(require_permissions(["schedule.swap"]))])
|
|
|
+async def swap_schedule(
|
|
|
+ payload: SwapRequest,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ result = await db.execute(select(ScheduleItem).where(ScheduleItem.id.in_([payload.source_id, payload.target_id])))
|
|
|
+ items = result.scalars().all()
|
|
|
+ if len(items) != 2:
|
|
|
+ raise HTTPException(status_code=404, detail="排班记录不存在")
|
|
|
+ item_a, item_b = items
|
|
|
+ if is_limited_scope(current_user) and (item_a.dept_id != current_user.dept_id or item_b.dept_id != current_user.dept_id):
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ item_a.staff_id, item_b.staff_id = item_b.staff_id, item_a.staff_id
|
|
|
+ db.add(AdjustLog(
|
|
|
+ dept_id=item_a.dept_id,
|
|
|
+ date=item_a.date,
|
|
|
+ type="swap",
|
|
|
+ operator=current_user.name,
|
|
|
+ created_at=datetime.utcnow(),
|
|
|
+ note="调班"
|
|
|
+ ))
|
|
|
+ await db.commit()
|
|
|
+ return {"success": True}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/substitute", dependencies=[Depends(require_permissions(["schedule.substitute"]))])
|
|
|
+async def substitute_schedule(
|
|
|
+ payload: SubstituteRequest,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == payload.item_id))
|
|
|
+ item = result.scalar_one_or_none()
|
|
|
+ if not item:
|
|
|
+ raise HTTPException(status_code=404, detail="排班不存在")
|
|
|
+ if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ await ensure_unique_schedule(db, item.dept_id, item.date, item.shift_id, payload.new_staff_id)
|
|
|
+ item.tag = "substituted"
|
|
|
+ item.original_staff_id = item.staff_id
|
|
|
+
|
|
|
+ substitute_item = ScheduleItem(
|
|
|
+ dept_id=item.dept_id,
|
|
|
+ date=item.date,
|
|
|
+ shift_id=item.shift_id,
|
|
|
+ staff_id=payload.new_staff_id,
|
|
|
+ tag="substitute",
|
|
|
+ substitute_for_id=item.id,
|
|
|
+ original_staff_id=item.original_staff_id,
|
|
|
+ reason=payload.reason
|
|
|
+ )
|
|
|
+ db.add(substitute_item)
|
|
|
+ db.add(AdjustLog(
|
|
|
+ dept_id=item.dept_id,
|
|
|
+ date=item.date,
|
|
|
+ type="substitute",
|
|
|
+ operator=current_user.name,
|
|
|
+ created_at=datetime.utcnow(),
|
|
|
+ note=payload.reason
|
|
|
+ ))
|
|
|
+ await db.commit()
|
|
|
+ await db.refresh(substitute_item)
|
|
|
+ return {"success": True, "substitute_id": str(substitute_item.id)}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/cancel-substitute", dependencies=[Depends(require_permissions(["schedule.substitute"]))])
|
|
|
+async def cancel_substitute(
|
|
|
+ item_id: UUID,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
|
|
|
+ substitute_item = result.scalar_one_or_none()
|
|
|
+ if not substitute_item:
|
|
|
+ raise HTTPException(status_code=404, detail="替班记录不存在")
|
|
|
+ if is_limited_scope(current_user) and substitute_item.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+
|
|
|
+ if substitute_item.substitute_for_id:
|
|
|
+ original_result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == substitute_item.substitute_for_id))
|
|
|
+ original_item = original_result.scalar_one_or_none()
|
|
|
+ if original_item:
|
|
|
+ original_item.tag = "normal"
|
|
|
+
|
|
|
+ await db.delete(substitute_item)
|
|
|
+ await db.commit()
|
|
|
+ return {"success": True}
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/stop-clinic", dependencies=[Depends(require_permissions(["schedule.stopClinic"]))])
|
|
|
+async def stop_clinic(
|
|
|
+ payload: StopClinicRequest,
|
|
|
+ db: AsyncSession = Depends(get_db),
|
|
|
+ current_user: User = Depends(get_current_user),
|
|
|
+):
|
|
|
+ if is_limited_scope(current_user) and payload.dept_id != current_user.dept_id:
|
|
|
+ raise HTTPException(status_code=403, detail="无权操作其他科室")
|
|
|
+ await db.execute(
|
|
|
+ delete(ScheduleItem)
|
|
|
+ .where(
|
|
|
+ ScheduleItem.dept_id == payload.dept_id,
|
|
|
+ ScheduleItem.date == date.fromisoformat(payload.date)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ stop_item = ScheduleItem(
|
|
|
+ dept_id=payload.dept_id,
|
|
|
+ date=date.fromisoformat(payload.date),
|
|
|
+ shift_id=None,
|
|
|
+ staff_id=current_user.id,
|
|
|
+ tag="stopClinic",
|
|
|
+ note=payload.reason
|
|
|
+ )
|
|
|
+ db.add(stop_item)
|
|
|
+ db.add(AdjustLog(
|
|
|
+ dept_id=payload.dept_id,
|
|
|
+ date=payload.date,
|
|
|
+ type="stopClinic",
|
|
|
+ operator=current_user.name,
|
|
|
+ created_at=datetime.utcnow(),
|
|
|
+ note=payload.reason
|
|
|
+ ))
|
|
|
+ await db.commit()
|
|
|
+ return {"success": True}
|