schedule.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. from datetime import datetime, date
  2. from typing import Optional
  3. from uuid import UUID
  4. from fastapi import APIRouter, Depends, HTTPException, Query
  5. from sqlalchemy import select, delete
  6. from sqlalchemy.ext.asyncio import AsyncSession
  7. from backend.app.core.dependencies import require_permissions, get_current_user
  8. from backend.app.db.session import get_db
  9. from backend.app.models import ScheduleItem, AdjustLog, User
  10. from backend.app.schemas.schedule import (
  11. ScheduleCreate,
  12. ScheduleResponse,
  13. ScheduleUpdate,
  14. SwapRequest,
  15. SubstituteRequest,
  16. StopClinicRequest
  17. )
  18. router = APIRouter(prefix="/schedule", tags=["schedule"])
  19. def is_limited_scope(user: User) -> bool:
  20. return user.role and user.role.name not in {"管理员", "排班员"}
  21. async def ensure_unique_schedule(
  22. db: AsyncSession,
  23. dept_id: UUID,
  24. schedule_date: date,
  25. shift_id: Optional[UUID],
  26. staff_id: Optional[UUID],
  27. exclude_id: Optional[UUID] = None,
  28. ) -> None:
  29. if not shift_id or not staff_id:
  30. return
  31. query = select(ScheduleItem.id).where(
  32. ScheduleItem.dept_id == dept_id,
  33. ScheduleItem.date == schedule_date,
  34. ScheduleItem.shift_id == shift_id,
  35. ScheduleItem.staff_id == staff_id,
  36. )
  37. if exclude_id:
  38. query = query.where(ScheduleItem.id != exclude_id)
  39. result = await db.execute(query)
  40. if result.scalar_one_or_none():
  41. raise HTTPException(status_code=409, detail="该人员在此班次已有排班")
  42. @router.get("", response_model=list[ScheduleResponse], dependencies=[Depends(require_permissions(["schedule.view"]))])
  43. async def list_schedule(
  44. db: AsyncSession = Depends(get_db),
  45. current_user: User = Depends(get_current_user),
  46. dept_id: Optional[UUID] = Query(default=None),
  47. start: Optional[str] = Query(default=None),
  48. end: Optional[str] = Query(default=None),
  49. ):
  50. query = select(ScheduleItem)
  51. if is_limited_scope(current_user):
  52. if not current_user.dept_id:
  53. return []
  54. query = query.where(ScheduleItem.dept_id == current_user.dept_id)
  55. elif dept_id:
  56. query = query.where(ScheduleItem.dept_id == dept_id)
  57. if start:
  58. query = query.where(ScheduleItem.date >= date.fromisoformat(start))
  59. if end:
  60. query = query.where(ScheduleItem.date <= date.fromisoformat(end))
  61. result = await db.execute(query.order_by(ScheduleItem.date))
  62. return result.scalars().all()
  63. @router.post("", response_model=ScheduleResponse, dependencies=[Depends(require_permissions(["schedule.create"]))])
  64. async def create_schedule(
  65. payload: ScheduleCreate,
  66. db: AsyncSession = Depends(get_db),
  67. current_user: User = Depends(get_current_user),
  68. ):
  69. if is_limited_scope(current_user) and payload.dept_id != current_user.dept_id:
  70. raise HTTPException(status_code=403, detail="无权操作其他科室")
  71. schedule_date = date.fromisoformat(payload.date)
  72. await ensure_unique_schedule(db, payload.dept_id, schedule_date, payload.shift_id, payload.staff_id)
  73. item = ScheduleItem(
  74. dept_id=payload.dept_id,
  75. date=schedule_date,
  76. shift_id=payload.shift_id,
  77. staff_id=payload.staff_id,
  78. tag=payload.tag,
  79. note=payload.note,
  80. original_staff_id=payload.original_staff_id,
  81. substitute_for_id=payload.substitute_for_id,
  82. reason=payload.reason
  83. )
  84. db.add(item)
  85. await db.commit()
  86. await db.refresh(item)
  87. return item
  88. @router.put("/{item_id}", response_model=ScheduleResponse, dependencies=[Depends(require_permissions(["schedule.edit"]))])
  89. async def update_schedule(
  90. item_id: UUID,
  91. payload: ScheduleUpdate,
  92. db: AsyncSession = Depends(get_db),
  93. current_user: User = Depends(get_current_user),
  94. ):
  95. result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
  96. item = result.scalar_one_or_none()
  97. if not item:
  98. raise HTTPException(status_code=404, detail="排班不存在")
  99. if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
  100. raise HTTPException(status_code=403, detail="无权操作其他科室")
  101. update_data = payload.model_dump(exclude_unset=True)
  102. if update_data:
  103. next_shift_id = update_data.get("shift_id", item.shift_id)
  104. next_staff_id = update_data.get("staff_id", item.staff_id)
  105. await ensure_unique_schedule(db, item.dept_id, item.date, next_shift_id, next_staff_id, exclude_id=item.id)
  106. for key, value in update_data.items():
  107. setattr(item, key, value)
  108. await db.commit()
  109. await db.refresh(item)
  110. return item
  111. @router.delete("/{item_id}", dependencies=[Depends(require_permissions(["schedule.delete"]))])
  112. async def delete_schedule(
  113. item_id: UUID,
  114. db: AsyncSession = Depends(get_db),
  115. current_user: User = Depends(get_current_user),
  116. ):
  117. result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
  118. item = result.scalar_one_or_none()
  119. if not item:
  120. raise HTTPException(status_code=404, detail="排班不存在")
  121. if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
  122. raise HTTPException(status_code=403, detail="无权操作其他科室")
  123. await db.delete(item)
  124. await db.commit()
  125. return {"success": True}
  126. @router.post("/swap", dependencies=[Depends(require_permissions(["schedule.swap"]))])
  127. async def swap_schedule(
  128. payload: SwapRequest,
  129. db: AsyncSession = Depends(get_db),
  130. current_user: User = Depends(get_current_user),
  131. ):
  132. result = await db.execute(select(ScheduleItem).where(ScheduleItem.id.in_([payload.source_id, payload.target_id])))
  133. items = result.scalars().all()
  134. if len(items) != 2:
  135. raise HTTPException(status_code=404, detail="排班记录不存在")
  136. item_a, item_b = items
  137. if is_limited_scope(current_user) and (item_a.dept_id != current_user.dept_id or item_b.dept_id != current_user.dept_id):
  138. raise HTTPException(status_code=403, detail="无权操作其他科室")
  139. item_a.staff_id, item_b.staff_id = item_b.staff_id, item_a.staff_id
  140. db.add(AdjustLog(
  141. dept_id=item_a.dept_id,
  142. date=item_a.date,
  143. type="swap",
  144. operator=current_user.name,
  145. created_at=datetime.utcnow(),
  146. note="调班"
  147. ))
  148. await db.commit()
  149. return {"success": True}
  150. @router.post("/substitute", dependencies=[Depends(require_permissions(["schedule.substitute"]))])
  151. async def substitute_schedule(
  152. payload: SubstituteRequest,
  153. db: AsyncSession = Depends(get_db),
  154. current_user: User = Depends(get_current_user),
  155. ):
  156. result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == payload.item_id))
  157. item = result.scalar_one_or_none()
  158. if not item:
  159. raise HTTPException(status_code=404, detail="排班不存在")
  160. if is_limited_scope(current_user) and item.dept_id != current_user.dept_id:
  161. raise HTTPException(status_code=403, detail="无权操作其他科室")
  162. await ensure_unique_schedule(db, item.dept_id, item.date, item.shift_id, payload.new_staff_id)
  163. item.tag = "substituted"
  164. item.original_staff_id = item.staff_id
  165. substitute_item = ScheduleItem(
  166. dept_id=item.dept_id,
  167. date=item.date,
  168. shift_id=item.shift_id,
  169. staff_id=payload.new_staff_id,
  170. tag="substitute",
  171. substitute_for_id=item.id,
  172. original_staff_id=item.original_staff_id,
  173. reason=payload.reason
  174. )
  175. db.add(substitute_item)
  176. db.add(AdjustLog(
  177. dept_id=item.dept_id,
  178. date=item.date,
  179. type="substitute",
  180. operator=current_user.name,
  181. created_at=datetime.utcnow(),
  182. note=payload.reason
  183. ))
  184. await db.commit()
  185. await db.refresh(substitute_item)
  186. return {"success": True, "substitute_id": str(substitute_item.id)}
  187. @router.post("/cancel-substitute", dependencies=[Depends(require_permissions(["schedule.substitute"]))])
  188. async def cancel_substitute(
  189. item_id: UUID,
  190. db: AsyncSession = Depends(get_db),
  191. current_user: User = Depends(get_current_user),
  192. ):
  193. result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == item_id))
  194. substitute_item = result.scalar_one_or_none()
  195. if not substitute_item:
  196. raise HTTPException(status_code=404, detail="替班记录不存在")
  197. if is_limited_scope(current_user) and substitute_item.dept_id != current_user.dept_id:
  198. raise HTTPException(status_code=403, detail="无权操作其他科室")
  199. if substitute_item.substitute_for_id:
  200. original_result = await db.execute(select(ScheduleItem).where(ScheduleItem.id == substitute_item.substitute_for_id))
  201. original_item = original_result.scalar_one_or_none()
  202. if original_item:
  203. original_item.tag = "normal"
  204. await db.delete(substitute_item)
  205. await db.commit()
  206. return {"success": True}
  207. @router.post("/stop-clinic", dependencies=[Depends(require_permissions(["schedule.stopClinic"]))])
  208. async def stop_clinic(
  209. payload: StopClinicRequest,
  210. db: AsyncSession = Depends(get_db),
  211. current_user: User = Depends(get_current_user),
  212. ):
  213. if is_limited_scope(current_user) and payload.dept_id != current_user.dept_id:
  214. raise HTTPException(status_code=403, detail="无权操作其他科室")
  215. await db.execute(
  216. delete(ScheduleItem)
  217. .where(
  218. ScheduleItem.dept_id == payload.dept_id,
  219. ScheduleItem.date == date.fromisoformat(payload.date)
  220. )
  221. )
  222. stop_item = ScheduleItem(
  223. dept_id=payload.dept_id,
  224. date=date.fromisoformat(payload.date),
  225. shift_id=None,
  226. staff_id=current_user.id,
  227. tag="stopClinic",
  228. note=payload.reason
  229. )
  230. db.add(stop_item)
  231. db.add(AdjustLog(
  232. dept_id=payload.dept_id,
  233. date=payload.date,
  234. type="stopClinic",
  235. operator=current_user.name,
  236. created_at=datetime.utcnow(),
  237. note=payload.reason
  238. ))
  239. await db.commit()
  240. return {"success": True}