from datetime import date, datetime from sqlalchemy import select, func, and_ 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.leave import Leave from app.models.work_plan import WorkPlan from app.models.customer import Customer from app.services.holidays import load_holiday_sets from app.services.wecom import wecom_client from app.utils.timezone import today_cst, is_working_day async def check_daily_reporting(db: AsyncSession) -> dict: """Check today's reporting progress and send wecom reminders to managers who haven't reported.""" today = today_cst() # Load holiday data (auto-refreshes from API if not cached) holidays, workdays = await load_holiday_sets(db) if not is_working_day(today, holidays, workdays): return {"status": "rest_day", "date": str(today)} # Get all users who need to report result = await db.execute( select(User).where(User.require_report == True) ) managers = result.scalars().all() # ── Exclude managers on leave today ── leaves_today = await db.execute( select(Leave.manager_id).where(and_( Leave.start_date <= today, Leave.end_date >= today, )) ) on_leave_ids = {str(uid) for uid, in leaves_today.all()} # Get managers who have reported today reported_visits = await db.execute( select(Visit.manager_id).where(Visit.visit_date == today) ) reported_notes = await db.execute( select(DailyNote.manager_id).where(DailyNote.note_date == today) ) reported_map = {str(uid): True for uid, in reported_visits.all()} for uid, in reported_notes.all(): reported_map[str(uid)] = True not_reported = [] reported_names = [] on_leave_names = [] for m in managers: if str(m.id) in on_leave_ids: on_leave_names.append(m.name) continue # skip — exempt from reporting if str(m.id) in reported_map: reported_names.append(m.name) else: not_reported.append(m) # 1. Send template card to not-reported managers (tap to open app) if not_reported: user_ids = [m.wecom_userid for m in not_reported if m.wecom_userid] if user_ids: desc = f"{today} | 已填报 {len(reported_names)}/{len(managers)} 人" success = await wecom_client.send_template_card( user_ids=user_ids, title="📋 今日填报提醒", desc=desc, url="https://qj.dhdx.fun/m", btn_text="去填报", ) # Fallback to text if not success: names_text = "、".join(m.name for m in not_reported) await wecom_client.send_text_message( user_ids, f"📋 今日填报提醒\n\n{desc}\n未填报:{names_text}\n\n请尽快完成填报 🙏\nhttps://qj.dhdx.fun/m", ) # 2. Send summary to directors/leaders only (not broadcast to @all) directors = await db.execute( select(User).where(User.role.in_(["director", "leader"]), User.wecom_userid.isnot(None)) ) director_ids = [d.wecom_userid for d in directors.scalars().all()] if managers and director_ids: effective_total = len(managers) - len(on_leave_names) if effective_total > 0: pct = len(reported_map) / effective_total * 100 else: pct = 100.0 leave_note = "" if on_leave_names: leave_note = f"\n> 请假中(已豁免):{len(on_leave_names)}人\n" leave_note += "".join(f"- {n} (请假)\n" for n in on_leave_names) summary = ( f"## 📊 今日填报汇总\n\n" f"> 日期:{today}\n" f"> 填报率:**{pct:.0f}%** ({len(reported_map)}/{effective_total})\n" ) if leave_note: summary += leave_note + "\n" if not_reported: summary += "**未填报:**\n" + "".join(f"- {m.name}\n" for m in not_reported) else: summary += "✅ 全体已完成今日填报" await wecom_client.send_markdown_message(summary, user_ids=director_ids) return { "status": "ok", "date": str(today), "total_managers": len(managers), "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), }