feat: add Leave service layer
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user