From 2633513ce370175e9d95110b98064f118575b7c4 Mon Sep 17 00:00:00 2001 From: v6ole Date: Sun, 12 Jul 2026 13:01:55 +0800 Subject: [PATCH] feat: add Leave service layer Co-Authored-By: Claude --- backend/app/services/leaves.py | 204 +++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 backend/app/services/leaves.py diff --git a/backend/app/services/leaves.py b/backend/app/services/leaves.py new file mode 100644 index 0000000..10e8e13 --- /dev/null +++ b/backend/app/services/leaves.py @@ -0,0 +1,204 @@ +from datetime import date +from uuid import UUID +from sqlalchemy import select, func, and_ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.leave import Leave +from app.models.user import User +from app.utils.timezone import today_cst +from app.services.dashboard import get_week_range + + +async def get_leaves( + db: AsyncSession, + user_id: str, + role: str, + manager_id: str | None = None, + status: str | None = None, + page: int = 1, + page_size: int = 25, +) -> dict: + """List leaves. Managers see only their own.""" + today = today_cst() + + query = select(Leave) + count_q = select(func.count(Leave.id)) + + # Role filter + if role == "manager": + query = query.where(Leave.manager_id == user_id) + count_q = count_q.where(Leave.manager_id == user_id) + elif manager_id: + query = query.where(Leave.manager_id == manager_id) + count_q = count_q.where(Leave.manager_id == manager_id) + + # Status filter + if status == "active": + query = query.where(and_(Leave.start_date <= today, Leave.end_date >= today)) + count_q = count_q.where(and_(Leave.start_date <= today, Leave.end_date >= today)) + elif status == "upcoming": + query = query.where(Leave.start_date > today) + count_q = count_q.where(Leave.start_date > today) + elif status == "past": + query = query.where(Leave.end_date < today) + count_q = count_q.where(Leave.end_date < today) + + total = (await db.execute(count_q)).scalar() or 0 + + query = query.order_by(Leave.start_date.desc()) + query = query.offset((page - 1) * page_size).limit(page_size) + result = await db.execute(query) + rows = result.scalars().all() + + # Resolve names + user_ids = set() + for r in rows: + user_ids.add(r.manager_id) + user_ids.add(r.submitted_by) + users_result = await db.execute(select(User).where(User.id.in_(user_ids))) + user_map = {u.id: u.name for u in users_result.scalars().all()} + + items = [] + for r in rows: + days = ((r.end_date - r.start_date).days + 1) if r.start_date and r.end_date else 0 + items.append({ + "id": r.id, + "manager_id": r.manager_id, + "manager_name": user_map.get(r.manager_id, ""), + "leave_type": r.leave_type, + "start_date": str(r.start_date), + "end_date": str(r.end_date), + "days": days, + "reason": r.reason or "", + "submitted_by": r.submitted_by, + "submitted_by_name": user_map.get(r.submitted_by, ""), + "created_at": str(r.created_at) if r.created_at else None, + "updated_at": str(r.updated_at) if r.updated_at else None, + }) + + return {"items": items, "total": total, "page": page, "page_size": page_size} + + +async def get_leave_by_id(db: AsyncSession, leave_id: UUID) -> Leave | None: + result = await db.execute(select(Leave).where(Leave.id == leave_id)) + return result.scalar() + + +async def create_leave( + db: AsyncSession, + data: dict, + submitted_by: UUID, + role: str, +) -> Leave: + """Create leave. Manager can only create for self; director can create for anyone.""" + if role == "manager": + if str(data.get("manager_id")) != str(submitted_by): + raise PermissionError("客户经理只能为自己提交请假") + + if data["start_date"] > data["end_date"]: + raise ValueError("结束日期不能早于开始日期") + + leave = Leave( + manager_id=data["manager_id"], + leave_type=data.get("leave_type", "事假"), + start_date=data["start_date"], + end_date=data["end_date"], + reason=data.get("reason", ""), + submitted_by=submitted_by, + ) + db.add(leave) + await db.commit() + await db.refresh(leave) + return leave + + +async def update_leave( + db: AsyncSession, + leave_id: UUID, + data: dict, + user_id: UUID, + role: str, +) -> Leave: + """Update leave. Director can edit any; manager can edit own submitted leaves.""" + leave = await get_leave_by_id(db, leave_id) + if not leave: + raise ValueError("请假记录不存在") + + if role == "manager": + if str(leave.submitted_by) != str(user_id): + raise PermissionError("客户经理只能编辑自己提交的请假") + + for field in ("manager_id", "leave_type", "start_date", "end_date", "reason"): + if field in data and data[field] is not None: + setattr(leave, field, data[field]) + + if leave.start_date > leave.end_date: + raise ValueError("结束日期不能早于开始日期") + + await db.commit() + await db.refresh(leave) + return leave + + +async def delete_leave( + db: AsyncSession, + leave_id: UUID, + user_id: UUID, + role: str, +) -> bool: + """Delete leave. Director can delete any; manager can delete own submitted leaves.""" + leave = await get_leave_by_id(db, leave_id) + if not leave: + return False + + if role == "manager": + if str(leave.submitted_by) != str(user_id): + raise PermissionError("客户经理只能删除自己提交的请假") + + await db.delete(leave) + await db.commit() + return True + + +async def get_leave_overview( + db: AsyncSession, + user_id: str, + role: str, + reference_date: date | None = None, +) -> dict: + """Get leave overview for the dashboard card. Returns leaves overlapping with current week.""" + monday, sunday = get_week_range(reference_date) + + query = select(Leave).where( + and_(Leave.start_date <= sunday, Leave.end_date >= monday) + ) + if role == "manager": + query = query.where(Leave.manager_id == user_id) + + result = await db.execute(query.order_by(Leave.start_date)) + rows = result.scalars().all() + + # Deduplicate by manager_id (one manager may have multiple leave records) + user_ids = set(r.manager_id for r in rows) + users_result = await db.execute(select(User).where(User.id.in_(user_ids))) + user_map = {u.id: u.name for u in users_result.scalars().all()} + + manager_set = set() + leave_list = [] + for r in rows: + days = ((r.end_date - r.start_date).days + 1) if r.start_date and r.end_date else 0 + leave_list.append({ + "manager_id": str(r.manager_id), + "manager_name": user_map.get(r.manager_id, ""), + "leave_type": r.leave_type, + "start_date": str(r.start_date), + "end_date": str(r.end_date), + "days": days, + }) + manager_set.add(str(r.manager_id)) + + return { + "week_start": str(monday), + "week_end": str(sunday), + "total_on_leave": len(manager_set), + "leave_list": leave_list, + }