Merge branch 'main' into develop
# Conflicts: # backend/app/api/key_visits.py # backend/app/api/mini_business.py # backend/app/api/visits.py # backend/app/api/work_plans.py # backend/app/main.py # backend/app/models/__init__.py # backend/app/schemas/key_visit.py # backend/app/schemas/mini_business.py # backend/app/schemas/work_plan.py # backend/app/services/light_board.py # frontend/src/components/DesktopLayout.vue # frontend/src/stores/theme.ts # frontend/src/views/desktop/ManagerWorkspace.vue # frontend/src/views/desktop/WorkPlans.vue # frontend/src/views/mobile/KeyVisitForm.vue # frontend/src/views/mobile/LeaveForm.vue # frontend/src/views/mobile/PlansList.vue # frontend/src/views/mobile/VisitForm.vue # frontend/src/views/mobile/WorkPlanForm.vue
This commit is contained in:
@@ -51,6 +51,7 @@ async def list_customers(
|
||||
industry: Optional[str] = Query(None),
|
||||
service: Optional[str] = Query(None),
|
||||
manager_id: Optional[str] = Query(None),
|
||||
customer_type: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
@@ -61,6 +62,8 @@ async def list_customers(
|
||||
|
||||
base_query = select(Customer)
|
||||
|
||||
if customer_type:
|
||||
base_query = base_query.where(Customer.customer_type == customer_type)
|
||||
if industry:
|
||||
base_query = base_query.where(Customer.industry.ilike(f"%{industry}%"))
|
||||
if service:
|
||||
@@ -90,7 +93,7 @@ async def list_customers(
|
||||
|
||||
# Paginate
|
||||
offset = (page - 1) * page_size
|
||||
query = base_query.order_by(Customer.name).offset(offset).limit(page_size)
|
||||
query = base_query.order_by(Customer.customer_type, Customer.name).offset(offset).limit(page_size)
|
||||
result = await db.execute(query)
|
||||
items = result.scalars().all()
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ async def _enrich(note: DailyNote, db: AsyncSession) -> dict:
|
||||
async def list_notes(
|
||||
date_from: Optional[str] = Query(None),
|
||||
date_to: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -41,6 +42,8 @@ async def list_notes(
|
||||
query = query.where(DailyNote.note_date >= parse_date(date_from))
|
||||
if date_to:
|
||||
query = query.where(DailyNote.note_date <= parse_date(date_to))
|
||||
if search:
|
||||
query = query.where(DailyNote.content.ilike(f"%{search}%"))
|
||||
query = query.order_by(DailyNote.note_date.desc(), DailyNote.created_at.desc()).limit(100)
|
||||
result = await db.execute(query)
|
||||
return [await _enrich(n, db) for n in result.scalars().all()]
|
||||
@@ -81,7 +84,7 @@ async def create_note(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
note = DailyNote(
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else uuid.UUID(current_user["user_id"]),
|
||||
note_date=parse_date(data.note_date),
|
||||
category=data.category,
|
||||
content=data.content,
|
||||
@@ -112,6 +115,8 @@ async def update_note(
|
||||
|
||||
old_snapshot = {"note_date": str(note.note_date), "category": note.category, "content": note.content, "time_range": note.time_range}
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if current_user["role"] not in ("director", "leader"):
|
||||
update_data.pop("manager_id", None)
|
||||
if "note_date" in update_data and update_data["note_date"]:
|
||||
update_data["note_date"] = parse_date(update_data["note_date"])
|
||||
for k, v in update_data.items():
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, or_
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_any_role
|
||||
from app.models.key_visit import KeyVisit
|
||||
@@ -37,6 +37,8 @@ async def _enrich(k: KeyVisit, db: AsyncSession) -> dict:
|
||||
@router.get("/")
|
||||
async def list_key_visits(
|
||||
customer_id: Optional[str] = Query(None),
|
||||
customer_type: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -45,11 +47,32 @@ async def list_key_visits(
|
||||
query = query.where(KeyVisit.manager_id == uuid.UUID(current_user["user_id"]))
|
||||
if customer_id:
|
||||
query = query.where(KeyVisit.customer_id == uuid.UUID(customer_id))
|
||||
if customer_type:
|
||||
query = query.join(Customer, KeyVisit.customer_id == Customer.id).where(Customer.customer_type == customer_type)
|
||||
if search:
|
||||
query = query.outerjoin(Customer, KeyVisit.customer_id == Customer.id).where(
|
||||
or_(KeyVisit.description.ilike(f"%{search}%"), KeyVisit.planned_visitor.ilike(f"%{search}%"), Customer.name.ilike(f"%{search}%"))
|
||||
)
|
||||
query = query.order_by(KeyVisit.planned_date.desc()).limit(200)
|
||||
result = await db.execute(query)
|
||||
return [await _enrich(k, db) for k in result.scalars().all()]
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
async def get_key_visit(
|
||||
item_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(KeyVisit).where(KeyVisit.id == item_id))
|
||||
k = result.scalar_one_or_none()
|
||||
if not k:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(k.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return await _enrich(k, db)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_key_visit(
|
||||
data: KeyVisitCreate,
|
||||
@@ -64,7 +87,7 @@ async def create_key_visit(
|
||||
planned_date=data.planned_date,
|
||||
planned_visitor=data.planned_visitor,
|
||||
visit_target=data.visit_target,
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else uuid.UUID(current_user["user_id"]),
|
||||
)
|
||||
init_entry(k, current_user["name"])
|
||||
db.add(k)
|
||||
@@ -103,6 +126,8 @@ async def update_key_visit(
|
||||
|
||||
old_snapshot = {"customer_id": str(k.customer_id), "urgency_level": k.urgency_level, "description": k.description, "progress_status": k.progress_status, "planned_date": k.planned_date, "planned_visitor": k.planned_visitor, "visit_target": k.visit_target}
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if current_user["role"] not in ("director", "leader"):
|
||||
update_data.pop("manager_id", None)
|
||||
if "manager_id" in update_data and update_data["manager_id"]:
|
||||
update_data["manager_id"] = uuid.UUID(update_data["manager_id"])
|
||||
for key, v in update_data.items():
|
||||
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, or_
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_any_role
|
||||
from app.models.mini_business import MiniBusiness
|
||||
@@ -36,6 +36,8 @@ async def _enrich(m: MiniBusiness, db: AsyncSession) -> dict:
|
||||
@router.get("/")
|
||||
async def list_mini_business(
|
||||
customer_id: Optional[str] = Query(None),
|
||||
customer_type: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -44,11 +46,32 @@ async def list_mini_business(
|
||||
query = query.where(MiniBusiness.manager_id == uuid.UUID(current_user["user_id"]))
|
||||
if customer_id:
|
||||
query = query.where(MiniBusiness.customer_id == uuid.UUID(customer_id))
|
||||
if customer_type:
|
||||
query = query.join(Customer, MiniBusiness.customer_id == Customer.id).where(Customer.customer_type == customer_type)
|
||||
if search:
|
||||
query = query.outerjoin(Customer, MiniBusiness.customer_id == Customer.id).where(
|
||||
or_(MiniBusiness.product_type.ilike(f"%{search}%"), MiniBusiness.follow_up_detail.ilike(f"%{search}%"), Customer.name.ilike(f"%{search}%"))
|
||||
)
|
||||
query = query.order_by(MiniBusiness.expected_revenue_date.desc()).limit(200)
|
||||
result = await db.execute(query)
|
||||
return [await _enrich(m, db) for m in result.scalars().all()]
|
||||
|
||||
|
||||
@router.get("/{item_id}")
|
||||
async def get_mini_business(
|
||||
item_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(MiniBusiness).where(MiniBusiness.id == item_id))
|
||||
m = result.scalar_one_or_none()
|
||||
if not m:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
if current_user["role"] == "manager" and str(m.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
return await _enrich(m, db)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_mini_business(
|
||||
data: MiniBusinessCreate,
|
||||
@@ -61,7 +84,7 @@ async def create_mini_business(
|
||||
amount=data.amount,
|
||||
follow_up_detail=data.follow_up_detail,
|
||||
status=data.status,
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else uuid.UUID(current_user["user_id"]),
|
||||
expected_revenue_date=data.expected_revenue_date,
|
||||
)
|
||||
init_entry(m, current_user["name"])
|
||||
@@ -101,10 +124,15 @@ async def update_mini_business(
|
||||
|
||||
old_snapshot = {"customer_id": str(m.customer_id), "product_type": m.product_type, "amount": m.amount, "follow_up_detail": m.follow_up_detail, "status": m.status, "expected_revenue_date": m.expected_revenue_date}
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if current_user["role"] not in ("director", "leader"):
|
||||
update_data.pop("manager_id", None)
|
||||
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(m, k, v)
|
||||
# Auto-clear expected revenue date when status becomes 已流失
|
||||
if m.status == "已流失" and m.expected_revenue_date:
|
||||
m.expected_revenue_date = ""
|
||||
new_snapshot = {"customer_id": str(m.customer_id), "product_type": m.product_type, "amount": m.amount, "follow_up_detail": m.follow_up_detail, "status": m.status, "expected_revenue_date": m.expected_revenue_date}
|
||||
changes = compute_diff(old_snapshot, new_snapshot)
|
||||
if changes:
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""MiniBusinessLog CRUD API — follow-up log entries for mini business opportunities."""
|
||||
|
||||
import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_any_role
|
||||
from app.models.mini_business import MiniBusiness
|
||||
from app.models.mini_business_log import MiniBusinessLog
|
||||
from app.models.user import User
|
||||
from app.schemas.mini_business_log import (
|
||||
MiniBusinessLogCreate, MiniBusinessLogUpdate, MiniBusinessLogOut,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/mini-business", tags=["MiniBusiness Logs"])
|
||||
|
||||
|
||||
async def _get_log(db: AsyncSession, log_id: str) -> MiniBusinessLog:
|
||||
result = await db.execute(select(MiniBusinessLog).where(MiniBusinessLog.id == log_id))
|
||||
log = result.scalar_one_or_none()
|
||||
if not log:
|
||||
raise HTTPException(status_code=404, detail="跟进记录不存在")
|
||||
return log
|
||||
|
||||
|
||||
@router.get("/{business_id}/logs")
|
||||
async def list_logs(
|
||||
business_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List follow-up logs for a mini business, newest first."""
|
||||
# Verify business exists and user has access
|
||||
mb = await db.get(MiniBusiness, business_id)
|
||||
if not mb:
|
||||
raise HTTPException(status_code=404, detail="商机不存在")
|
||||
if current_user["role"] == "manager" and str(mb.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
result = await db.execute(
|
||||
select(MiniBusinessLog)
|
||||
.where(MiniBusinessLog.business_id == business_id)
|
||||
.order_by(MiniBusinessLog.log_date.desc(), MiniBusinessLog.created_at.desc())
|
||||
)
|
||||
logs = result.scalars().all()
|
||||
|
||||
# Resolve creator names
|
||||
user_ids = {log.created_by for log in logs}
|
||||
users = (await db.execute(select(User).where(User.id.in_(user_ids)))).scalars().all()
|
||||
user_map = {u.id: u.name for u in users}
|
||||
|
||||
return [
|
||||
{
|
||||
"id": log.id,
|
||||
"business_id": log.business_id,
|
||||
"log_date": str(log.log_date),
|
||||
"method": log.method,
|
||||
"content": log.content,
|
||||
"created_by": log.created_by,
|
||||
"created_by_name": user_map.get(log.created_by, ""),
|
||||
"created_at": str(log.created_at) if log.created_at else None,
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{business_id}/logs")
|
||||
async def create_log(
|
||||
business_id: str,
|
||||
data: MiniBusinessLogCreate,
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Add a follow-up log entry."""
|
||||
mb = await db.get(MiniBusiness, business_id)
|
||||
if not mb:
|
||||
raise HTTPException(status_code=404, detail="商机不存在")
|
||||
if current_user["role"] == "manager" and str(mb.manager_id) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
log = MiniBusinessLog(
|
||||
business_id=uuid.UUID(business_id),
|
||||
log_date=data.log_date,
|
||||
method=data.method,
|
||||
content=data.content,
|
||||
created_by=uuid.UUID(current_user["user_id"]),
|
||||
)
|
||||
db.add(log)
|
||||
await db.commit()
|
||||
await db.refresh(log)
|
||||
|
||||
creator = await db.get(User, log.created_by)
|
||||
return {
|
||||
"id": log.id,
|
||||
"business_id": log.business_id,
|
||||
"log_date": str(log.log_date),
|
||||
"method": log.method,
|
||||
"content": log.content,
|
||||
"created_by": log.created_by,
|
||||
"created_by_name": creator.name if creator else "",
|
||||
"created_at": str(log.created_at) if log.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/logs/{log_id}")
|
||||
async def update_log(
|
||||
log_id: str,
|
||||
data: MiniBusinessLogUpdate,
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update a follow-up log entry."""
|
||||
log = await _get_log(db, log_id)
|
||||
|
||||
# Managers can only edit their own logs; directors/leaders can edit any
|
||||
if current_user["role"] == "manager" and str(log.created_by) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for k, v in update_data.items():
|
||||
setattr(log, k, v)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(log)
|
||||
return {"id": log.id, "log_date": str(log.log_date), "method": log.method, "content": log.content}
|
||||
|
||||
|
||||
@router.delete("/logs/{log_id}")
|
||||
async def delete_log(
|
||||
log_id: str,
|
||||
current_user: dict = Depends(require_any_role),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a follow-up log entry."""
|
||||
log = await _get_log(db, log_id)
|
||||
|
||||
if current_user["role"] == "manager" and str(log.created_by) != current_user["user_id"]:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
||||
await db.delete(log)
|
||||
await db.commit()
|
||||
return {"status": "deleted"}
|
||||
+53
-12
@@ -3,7 +3,7 @@ from datetime import date, datetime
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, or_
|
||||
from app.database import get_db
|
||||
from app.middleware.auth import get_current_user, require_any_role
|
||||
from app.models.visit import Visit
|
||||
@@ -74,6 +74,8 @@ async def list_visits(
|
||||
date_from: Optional[str] = Query(None),
|
||||
date_to: Optional[str] = Query(None),
|
||||
customer_id: Optional[str] = Query(None),
|
||||
customer_type: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -89,6 +91,12 @@ async def list_visits(
|
||||
query = query.where(Visit.visit_date <= parse_date(date_to))
|
||||
if customer_id:
|
||||
query = query.where(Visit.customer_id == uuid.UUID(customer_id))
|
||||
if customer_type:
|
||||
query = query.join(Customer, Visit.customer_id == Customer.id).where(Customer.customer_type == customer_type)
|
||||
if search:
|
||||
query = query.outerjoin(Customer, Visit.customer_id == Customer.id).where(
|
||||
or_(Visit.communication_content.ilike(f"%{search}%"), Visit.customer_demand.ilike(f"%{search}%"), Visit.visitor_name.ilike(f"%{search}%"), Customer.name.ilike(f"%{search}%"))
|
||||
)
|
||||
|
||||
query = query.order_by(Visit.visit_date.desc(), Visit.created_at.desc()).limit(200)
|
||||
result = await db.execute(query)
|
||||
@@ -161,7 +169,7 @@ async def create_visit(
|
||||
companions=data.companions,
|
||||
companion_names=data.companion_names,
|
||||
photos=data.photos,
|
||||
manager_id=uuid.UUID(current_user["user_id"]),
|
||||
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else uuid.UUID(current_user["user_id"]),
|
||||
visit_group_id=group_id,
|
||||
)
|
||||
init_entry(visit, current_user["name"])
|
||||
@@ -176,18 +184,22 @@ async def create_visit(
|
||||
db.add(customer)
|
||||
|
||||
# Auto-complete matching work plans for this customer
|
||||
from app.models.work_plan import WorkPlan
|
||||
plans_result = await db.execute(
|
||||
select(WorkPlan).where(
|
||||
WorkPlan.customer_id == data.customer_id,
|
||||
WorkPlan.status == "计划中",
|
||||
WorkPlan.plan_date <= parse_date(data.visit_date),
|
||||
from app.services.visits import auto_complete_work_plans
|
||||
await auto_complete_work_plans(
|
||||
db, data.customer_id, parse_date(data.visit_date),
|
||||
current_user["name"], "拜访自动完成",
|
||||
)
|
||||
|
||||
# Auto-create follow-up logs for active mini business opportunities
|
||||
from app.models.mini_business import MiniBusiness
|
||||
from app.models.mini_business_log import MiniBusinessLog
|
||||
method_map = {"上门": "上门", "电话": "电话", "微信": "微信", "出差": "其他"}
|
||||
active_biz = await db.execute(
|
||||
select(MiniBusiness).where(
|
||||
MiniBusiness.customer_id == data.customer_id,
|
||||
MiniBusiness.status == "跟进中",
|
||||
)
|
||||
)
|
||||
for plan in plans_result.scalars().all():
|
||||
plan.status = "已完成"
|
||||
append_entry(plan, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "拜访自动完成"}])
|
||||
|
||||
# Create full copies for companions (not blank drafts)
|
||||
creator_name = current_user["name"]
|
||||
for companion_id in data.companions:
|
||||
@@ -215,6 +227,24 @@ async def create_visit(
|
||||
init_entry(draft, current_user["name"])
|
||||
db.add(draft)
|
||||
|
||||
# Auto-create follow-up logs for active mini business opportunities
|
||||
for biz in active_biz.scalars().all():
|
||||
parts = []
|
||||
if data.communication_content and data.communication_content.strip():
|
||||
parts.append(f"沟通内容:{data.communication_content.strip()}")
|
||||
if data.customer_demand and data.customer_demand.strip():
|
||||
parts.append(f"客户需求:{data.customer_demand.strip()}")
|
||||
log_content = "\n".join(parts) if parts else "拜访跟进"
|
||||
follow_method = method_map.get(data.visit_method, "其他")
|
||||
log = MiniBusinessLog(
|
||||
business_id=biz.id,
|
||||
log_date=parse_date(data.visit_date),
|
||||
method=follow_method,
|
||||
content=log_content,
|
||||
created_by=uuid.UUID(current_user["user_id"]),
|
||||
)
|
||||
db.add(log)
|
||||
|
||||
# Audit log (before commit — part of same transaction)
|
||||
cust = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id))
|
||||
await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({data.visit_date})", "create", current_user["user_id"], current_user["name"])
|
||||
@@ -251,6 +281,8 @@ async def update_visit(
|
||||
}
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if current_user["role"] not in ("director", "leader"):
|
||||
update_data.pop("manager_id", None)
|
||||
if "visit_date" in update_data and update_data["visit_date"]:
|
||||
update_data["visit_date"] = parse_date(update_data["visit_date"])
|
||||
if "manager_id" in update_data and update_data["manager_id"]:
|
||||
@@ -289,6 +321,15 @@ async def update_visit(
|
||||
await log_audit(db, "visit", visit.id, f"{cust.scalar_one_or_none() or ''} ({visit.visit_date})", "update", current_user["user_id"], current_user["name"], ", ".join(changes.keys()) if changes else "")
|
||||
await db.commit()
|
||||
await db.refresh(visit)
|
||||
|
||||
# Auto-complete matching work plans after visit update
|
||||
from app.services.visits import auto_complete_work_plans
|
||||
await auto_complete_work_plans(
|
||||
db, visit.customer_id, visit.visit_date,
|
||||
current_user["name"], "拜访更新自动完成",
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return await _enrich_visit(visit, db)
|
||||
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ 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 sqlalchemy import select, or_
|
||||
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
|
||||
@@ -36,6 +35,8 @@ async def _enrich(wp: WorkPlan, db: AsyncSession) -> dict:
|
||||
@router.get("/")
|
||||
async def list_work_plans(
|
||||
customer_id: Optional[str] = Query(None),
|
||||
customer_type: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -44,59 +45,49 @@ async def list_work_plans(
|
||||
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))
|
||||
if customer_type:
|
||||
query = query.join(Customer, WorkPlan.customer_id == Customer.id).where(Customer.customer_type == customer_type)
|
||||
if search:
|
||||
query = query.outerjoin(Customer, WorkPlan.customer_id == Customer.id).where(
|
||||
or_(WorkPlan.plan_content.ilike(f"%{search}%"), Customer.name.ilike(f"%{search}%"))
|
||||
)
|
||||
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.get("/{item_id}")
|
||||
async def get_work_plan(
|
||||
item_id: str,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
result = await db.execute(select(WorkPlan).where(WorkPlan.id == item_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.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"]),
|
||||
plan_date=parse_date(data.plan_date),
|
||||
manager_id=data.manager_id if (current_user["role"] in ("director", "leader") and data.manager_id) else 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)
|
||||
|
||||
|
||||
@@ -115,10 +106,10 @@ async def update_work_plan(
|
||||
|
||||
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 current_user["role"] not in ("director", "leader"):
|
||||
update_data.pop("manager_id", None)
|
||||
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}
|
||||
@@ -127,10 +118,6 @@ async def update_work_plan(
|
||||
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)
|
||||
|
||||
|
||||
@@ -146,11 +133,6 @@ async def delete_work_plan(
|
||||
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"}
|
||||
|
||||
Reference in New Issue
Block a user