| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- 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}
|