diff --git a/backend/app/api/visits.py b/backend/app/api/visits.py index 607c18d..53c0f37 100644 --- a/backend/app/api/visits.py +++ b/backend/app/api/visits.py @@ -151,6 +151,19 @@ async def create_visit( customer.last_visit_manager_id = uuid.UUID(current_user["user_id"]) 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), + ) + ) + for plan in plans_result.scalars().all(): + plan.status = "已完成" + append_entry(plan, current_user["name"], [{"field": "status", "from": "计划中", "to": "已完成", "reason": "拜访自动完成"}]) + # Create draft copies for companions for companion_id in data.companions: if companion_id != uuid.UUID(current_user["user_id"]): diff --git a/backend/app/main.py b/backend/app/main.py index b4395b7..ee8aff6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,7 +7,7 @@ from app.database import engine, Base, async_session from app.api import router as api_router from app.api import auth, users, customers, visits, work_plans, mini_business, key_visits from app.api import dashboard, upload, export, import_data, wecom, daily_notes, ai_summary -from app.services.scheduler import check_daily_reporting +from app.services.scheduler import check_daily_reporting, check_overdue_plans _scheduler = AsyncIOScheduler() @@ -18,6 +18,12 @@ async def _scheduled_check(): await check_daily_reporting(db) +async def _scheduled_overdue_check(): + """Check for overdue plans and remind managers (9:00 AM).""" + async with async_session() as db: + await check_overdue_plans(db) + + @asynccontextmanager async def lifespan(app: FastAPI): # Startup: create tables if not exists (for dev convenience) @@ -51,6 +57,7 @@ async def lifespan(app: FastAPI): # Start daily reporting scheduler (17:30 CST = 09:30 UTC) _scheduler.add_job(_scheduled_check, "cron", hour=17, minute=30, id="daily_check") + _scheduler.add_job(_scheduled_overdue_check, "cron", hour=9, minute=0, id="overdue_check") _scheduler.start() yield diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index dc79871..bca593b 100644 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -4,6 +4,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.models.visit import Visit from app.models.daily_note import DailyNote from app.models.user import User +from app.models.work_plan import WorkPlan +from app.models.customer import Customer from app.services.wecom import wecom_client from app.utils.timezone import today_cst @@ -85,3 +87,62 @@ async def check_daily_reporting(db: AsyncSession) -> dict: "reported": len(reported_map), "not_reported": len(not_reported), } + + +async def check_overdue_plans(db: AsyncSession) -> dict: + """Check for overdue work plans and remind managers (runs at 9:00 AM).""" + today = today_cst() + if today.weekday() >= 5: + return {"status": "weekend", "date": str(today)} + + result = await db.execute( + select(WorkPlan).where( + WorkPlan.status == "计划中", + WorkPlan.plan_date < today, + ).order_by(WorkPlan.manager_id, WorkPlan.plan_date) + ) + overdue = result.scalars().all() + + if not overdue: + return {"status": "ok", "date": str(today), "overdue": 0} + + # Group by manager + by_manager: dict[str, list] = {} + for p in overdue: + mid = str(p.manager_id) + by_manager.setdefault(mid, []).append(p) + + users_result = await db.execute( + select(User).where(User.id.in_([uid for uid in by_manager.keys()])) + ) + user_map = {str(u.id): u for u in users_result.scalars().all()} + + for mid, plans in by_manager.items(): + user = user_map.get(mid) + if not user or not user.wecom_userid: + continue + names = "、".join(f"{p.customer_id}" for p in plans[:5]) + # Get customer names + cust_result = await db.execute( + select(Customer.name).where(Customer.id.in_([p.customer_id for p in plans[:5]])) + ) + cust_names = [r[0] for r in cust_result.all()] + + plan_lines = "".join(f"- {n} (计划 {p.plan_date})\n" for p, n in zip(plans[:5], cust_names)) + content = ( + f"📅 拜访计划过期提醒\n\n" + f"以下 {len(plans)} 个拜访计划已过期,请尽快安排拜访:\n" + f"{plan_lines}" + ) + if len(plans) > 5: + content += f"... 还有 {len(plans) - 5} 个过期计划\n" + content += f"\n👉 查看详情:https://qj.dhdx.fun/light-board" + + await wecom_client.send_text_message([user.wecom_userid], content) + + return { + "status": "ok", + "date": str(today), + "overdue": len(overdue), + "managers_affected": len(by_manager), + } diff --git a/frontend/src/views/desktop/LightBoard.vue b/frontend/src/views/desktop/LightBoard.vue index 1f9a333..ced83eb 100644 --- a/frontend/src/views/desktop/LightBoard.vue +++ b/frontend/src/views/desktop/LightBoard.vue @@ -1,5 +1,5 @@