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}