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:
2026-08-17 09:31:12 +08:00
46 changed files with 2682 additions and 213 deletions
+53 -12
View File
@@ -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)