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 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), 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)) 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"], "拜访自动完成", ) # Create draft copies for companions for companion_id in data.companions: if companion_id != uuid.UUID(current_user["user_id"]): draft = 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="", # Leave blank for companion to fill customer_demand="", companions=[], photos=[], manager_id=companion_id, ) db.add(draft) 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"}