Files
qiji/backend/app/api/work_plans.py
T
v6ole 1ec20ee53e chore: 保存进行中的未提交改动(企微定向推送+搜索筛选UI等)
保存合并前工作树中遗留的进行中改动,避免分支合并时丢失:
- scheduler: 填报汇总改为定向推送给支局长/领导(而非广播@all)
- router: JWT token 校验修复,bind token(UUID)不被误剥离
- 工作计划/计划列表: 搜索+筛选+分页 UI
- 各列表 API 增加 search 参数支持
- docker-compose.backend.yml + backend/.dockerignore 纳入版本管理

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 09:17:03 +08:00

157 lines
6.5 KiB
Python

import uuid
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.database import get_db
from app.middleware.auth import get_current_user, require_any_role
from app.utils.timezone import parse_date
from app.utils.edit_log import compute_diff, append_entry, init_entry
from app.utils.audit import log_audit
from app.models.work_plan import WorkPlan
from app.models.customer import Customer
from app.models.user import User
from app.schemas.work_plan import WorkPlanCreate, WorkPlanUpdate, WorkPlanOut
router = APIRouter(prefix="/work-plans", tags=["WorkPlans"])
async def _enrich(wp: WorkPlan, db: AsyncSession) -> dict:
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
mgr = await db.execute(select(User.name).where(User.id == wp.manager_id))
return {
"id": str(wp.id),
"customer_id": str(wp.customer_id),
"customer_name": cust.scalar_one_or_none(),
"plan_content": wp.plan_content,
"plan_date": wp.plan_date,
"manager_id": str(wp.manager_id),
"manager_name": mgr.scalar_one_or_none(),
"status": wp.status,
"edit_log": wp.edit_log or [],
}
@router.get("/")
async def list_work_plans(
customer_id: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
query = select(WorkPlan)
if current_user["role"] == "manager":
query = query.where(WorkPlan.manager_id == uuid.UUID(current_user["user_id"]))
if customer_id:
query = query.where(WorkPlan.customer_id == uuid.UUID(customer_id))
query = query.order_by(WorkPlan.plan_date.desc()).limit(200)
result = await db.execute(query)
return [await _enrich(w, db) for w in result.scalars().all()]
@router.post("/")
async def create_work_plan(
data: WorkPlanCreate,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
plan_date = parse_date(data.plan_date)
# Auto-complete if a visit already exists for this customer on/after the plan date
from app.models.visit import Visit
if data.status == "计划中":
existing_visit = await db.execute(
select(Visit.id).where(
Visit.customer_id == data.customer_id,
Visit.visit_date >= plan_date,
).limit(1)
)
if existing_visit.scalar_one_or_none():
data.status = "已完成"
wp = WorkPlan(
customer_id=data.customer_id,
plan_content=data.plan_content,
plan_date=plan_date,
manager_id=uuid.UUID(current_user["user_id"]),
status=data.status,
)
init_entry(wp, current_user["name"])
if data.status == "已完成":
append_entry(wp, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "已有拜访记录,自动完成"}])
db.add(wp)
await db.commit()
await db.refresh(wp)
# Audit log
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
await log_audit(db, "work_plan", wp.id, f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})", "create", current_user["user_id"], current_user["name"])
await db.commit()
return await _enrich(wp, db)
@router.get("/{plan_id}")
async def get_work_plan(plan_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)):
result = await db.execute(select(WorkPlan).where(WorkPlan.id == plan_id))
wp = result.scalar_one_or_none()
if not wp:
raise HTTPException(status_code=404, detail="Not found")
if current_user["role"] == "manager" and str(wp.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
return await _enrich(wp, db)
@router.put("/{plan_id}")
async def update_work_plan(
plan_id: str, data: WorkPlanUpdate,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(WorkPlan).where(WorkPlan.id == plan_id))
wp = result.scalar_one_or_none()
if not wp:
raise HTTPException(status_code=404, detail="Not found")
if current_user["role"] == "manager" and str(wp.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
old_snapshot = {"customer_id": str(wp.customer_id), "plan_content": wp.plan_content, "plan_date": str(wp.plan_date), "status": wp.status}
update_data = data.model_dump(exclude_unset=True)
if "plan_date" in update_data and update_data["plan_date"]:
update_data["plan_date"] = parse_date(update_data["plan_date"])
if "manager_id" in update_data and update_data["manager_id"]:
update_data["manager_id"] = uuid.UUID(update_data["manager_id"])
for k, v in update_data.items():
setattr(wp, k, v)
new_snapshot = {"customer_id": str(wp.customer_id), "plan_content": wp.plan_content, "plan_date": str(wp.plan_date), "status": wp.status}
changes = compute_diff(old_snapshot, new_snapshot)
if changes:
append_entry(wp, current_user["name"], changes, getattr(data, "edit_reason", None))
await db.commit()
await db.refresh(wp)
# Audit log
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
await log_audit(db, "work_plan", wp.id, f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})", "update", current_user["user_id"], current_user["name"])
await db.commit()
return await _enrich(wp, db)
@router.delete("/{plan_id}")
async def delete_work_plan(
plan_id: str,
current_user: dict = Depends(require_any_role),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(WorkPlan).where(WorkPlan.id == plan_id))
wp = result.scalar_one_or_none()
if not wp:
raise HTTPException(status_code=404, detail="Not found")
if current_user["role"] == "manager" and str(wp.manager_id) != current_user["user_id"]:
raise HTTPException(status_code=403, detail="Access denied")
cust = await db.execute(select(Customer.name).where(Customer.id == wp.customer_id))
entity_name = f"{cust.scalar_one_or_none() or ''} ({wp.plan_date})"
await db.delete(wp)
await db.commit()
# Audit log
await log_audit(db, "work_plan", uuid.UUID(plan_id), entity_name, "delete", current_user["user_id"], current_user["name"])
await db.commit()
return {"detail": "deleted"}