import uuid 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, or_ from app.database import get_db from app.middleware.auth import get_current_user, require_any_role from app.models.visit import Visit from app.models.customer import Customer from app.models.user import User from app.schemas.visit import VisitCreate, VisitUpdate, VisitOut, VisitListOut from app.utils.timezone import today_cst, parse_date from app.services.minio_client import delete_objects from app.utils.edit_log import compute_diff, append_entry, init_entry router = APIRouter(prefix="/visits", tags=["Visits"]) async def _enrich_visit(visit: Visit, db: AsyncSession) -> dict: """Enrich a visit record with customer/manager names.""" customer_name = None manager_name = None if visit.customer_id: cust_result = await db.execute(select(Customer.name).where(Customer.id == visit.customer_id)) customer_name = cust_result.scalar_one_or_none() if visit.manager_id: mgr_result = await db.execute(select(User.name).where(User.id == visit.manager_id)) manager_name = mgr_result.scalar_one_or_none() # Resolve companion UUIDs to names companion_names_resolved = list(visit.companion_names or []) if visit.companions: comp_result = await db.execute(select(User.name).where(User.id.in_(visit.companions))) companion_names_resolved = [n for n, in comp_result.all()] + companion_names_resolved return { "id": str(visit.id), "customer_id": str(visit.customer_id), "customer_name": customer_name, "visit_date": visit.visit_date, "visit_method": visit.visit_method, "time_range": visit.time_range, "visitor_name": visit.visitor_name or "", "visitor_phone": visit.visitor_phone or "", "communication_content": visit.communication_content, "customer_demand": visit.customer_demand, "companions": visit.companions, "companion_names": visit.companion_names or [], "companion_names_resolved": companion_names_resolved, "photos": visit.photos, "manager_id": str(visit.manager_id), "manager_name": manager_name, "edit_log": visit.edit_log or [], "created_at": str(visit.created_at), "updated_at": str(visit.updated_at), } @router.get("/") 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), ): """List visits. Managers see only their own, directors/leaders see all.""" query = select(Visit) if current_user["role"] == "manager": query = query.where(Visit.manager_id == uuid.UUID(current_user["user_id"])) if date_from: query = query.where(Visit.visit_date >= parse_date(date_from)) if date_to: 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) visits = result.scalars().all() # Enrich enriched = [] for v in visits: enriched.append(await _enrich_visit(v, db)) return enriched @router.get("/today") async def list_today_visits( current_user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get today's visits for the current user's mobile home screen.""" query = select(Visit).where(Visit.visit_date == today_cst()) if current_user["role"] == "manager": query = query.where(Visit.manager_id == uuid.UUID(current_user["user_id"])) query = query.order_by(Visit.created_at.desc()) result = await db.execute(query) visits = result.scalars().all() enriched = [] for v in visits: enriched.append(await _enrich_visit(v, db)) count = len(enriched) return {"count": count, "visits": enriched} @router.get("/{visit_id}") async def get_visit( visit_id: str, current_user: dict = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Visit).where(Visit.id == visit_id)) visit = result.scalar_one_or_none() if not visit: raise HTTPException(status_code=404, detail="Visit not found") # Permission check if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]: raise HTTPException(status_code=403, detail="Access denied") return await _enrich_visit(visit, db) @router.post("/") async def create_visit( data: VisitCreate, current_user: dict = Depends(require_any_role), db: AsyncSession = Depends(get_db), ): """Create a visit record. If companions are selected, creates draft copies for them.""" visit = Visit( customer_id=data.customer_id, visit_date=parse_date(data.visit_date), visit_method=data.visit_method, time_range=data.time_range, communication_content=data.communication_content, customer_demand=data.customer_demand, companions=data.companions, companion_names=data.companion_names, photos=data.photos, 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(visit, current_user["name"]) db.add(visit) # Update customer's last visit tracking cust_result = await db.execute(select(Customer).where(Customer.id == data.customer_id)) customer = cust_result.scalar_one_or_none() if customer: customer.last_visit_date = parse_date(data.visit_date) customer.last_visit_manager_id = uuid.UUID(current_user["user_id"]) db.add(customer) # Auto-complete matching work plans for this customer 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 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) await db.commit() await db.refresh(visit) return await _enrich_visit(visit, db) @router.put("/{visit_id}") async def update_visit( visit_id: str, data: VisitUpdate, current_user: dict = Depends(require_any_role), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Visit).where(Visit.id == visit_id)) visit = result.scalar_one_or_none() if not visit: raise HTTPException(status_code=404, detail="Visit not found") if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]: raise HTTPException(status_code=403, detail="Access denied") # Snapshot old values for diff old_snapshot = { "customer_id": str(visit.customer_id), "visit_date": str(visit.visit_date), "visit_method": visit.visit_method, "time_range": visit.time_range, "visitor_name": visit.visitor_name or "", "visitor_phone": visit.visitor_phone or "", "communication_content": visit.communication_content, "customer_demand": visit.customer_demand, } 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"]) for key, value in update_data.items(): setattr(visit, key, value) # Compute diff and append to edit_log new_snapshot = { "customer_id": str(visit.customer_id), "visit_date": str(visit.visit_date), "visit_method": visit.visit_method, "time_range": visit.time_range, "visitor_name": visit.visitor_name or "", "visitor_phone": visit.visitor_phone or "", "communication_content": visit.communication_content, "customer_demand": visit.customer_demand, } changes = compute_diff(old_snapshot, new_snapshot) if changes: append_entry(visit, current_user["name"], changes, getattr(data, "edit_reason", None)) 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) @router.delete("/{visit_id}") async def delete_visit( visit_id: str, current_user: dict = Depends(require_any_role), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Visit).where(Visit.id == visit_id)) visit = result.scalar_one_or_none() if not visit: raise HTTPException(status_code=404, detail="Visit not found") if current_user["role"] == "manager" and str(visit.manager_id) != current_user["user_id"]: raise HTTPException(status_code=403, detail="Access denied") customer_id = visit.customer_id manager_id = visit.manager_id # Clean up photos in MinIO if visit.photos: delete_objects(visit.photos) await db.delete(visit) # Recalculate customer's last_visit_date from remaining visits latest = await db.execute( select(func.max(Visit.visit_date)).where( Visit.customer_id == customer_id, ) ) new_latest = latest.scalar() cust = await db.get(Customer, customer_id) if cust: if new_latest: cust.last_visit_date = new_latest # Keep the existing manager if date unchanged, or find who made the latest visit latest_visit = await db.execute( select(Visit).where( Visit.customer_id == customer_id, Visit.visit_date == new_latest, ).order_by(Visit.created_at.desc()).limit(1) ) lv = latest_visit.scalar_one_or_none() if lv: cust.last_visit_manager_id = lv.manager_id else: cust.last_visit_date = None cust.last_visit_manager_id = None await db.commit() return {"detail": "deleted"}