feat: 三项联动 — 计划过期提醒 + 拜访自动关闭 + 批量制定
1. 计划过期自动提醒: - scheduler.py 新增 check_overdue_plans() - 每日 9:00 检查过期计划(plan_date < today, status=计划中) - 按经理汇总 → 企微推送提醒消息 2. 拜访后自动关闭计划: - create_visit 后自动将匹配的计划标记为「已完成」 - 条件: customer_id 匹配 + status=计划中 + plan_date <= visit_date 3. 亮灯表批量制定: - 红灯/黄灯卡片右上角 ☐ 复选框 - 勾选后出现「批量制定计划」按钮 - 弹窗统一设置日期+内容 → 批量 POST Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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"]):
|
||||
|
||||
+8
-1
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user