from datetime import date, timedelta from uuid import UUID from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import aliased from app.models.customer import Customer from app.models.user import User from app.models.visit import Visit from app.models.work_plan import WorkPlan from app.models.mini_business import MiniBusiness from app.models.key_visit import KeyVisit from app.models.daily_note import DailyNote from app.models.leave import Leave from app.services.holidays import load_holiday_sets from app.utils.timezone import today_cst, is_working_day def get_week_range(reference_date: date | None = None): today = reference_date or date.today() monday = today - timedelta(days=today.weekday()) sunday = monday + timedelta(days=6) return monday, sunday async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = None) -> dict: """Get dashboard statistics for a given week (defaults to current).""" monday, sunday = get_week_range(reference_date) today = date.today() visits_count = (await db.execute( select(func.count(Visit.id)).where(Visit.visit_date >= monday, Visit.visit_date <= sunday) )).scalar() or 0 plans_count = (await db.execute( select(func.count(WorkPlan.id)) )).scalar() or 0 mini_biz_count = (await db.execute( select(func.count(MiniBusiness.id)) )).scalar() or 0 key_visit_count = (await db.execute( select(func.count(KeyVisit.id)) )).scalar() or 0 # Week leave count — distinct managers on leave this week leaves_count = (await db.execute( select(func.count(func.distinct(Leave.manager_id))).where( and_(Leave.start_date <= sunday, Leave.end_date >= monday) ) )).scalar() or 0 return { "week_visits": visits_count, "work_plans": plans_count, "mini_business": mini_biz_count, "key_visits": key_visit_count, "week_leaves": leaves_count, "week_start": str(monday), "week_end": str(sunday), } async def get_reporting_progress(db: AsyncSession, reference_date: date | None = None, user_id: str = "", role: str = "") -> list[dict]: """Get per-manager reporting progress. Managers only see themselves.""" monday, sunday = get_week_range(reference_date) # Get all users who need to report (any role, filtered by require_report) managers_result = await db.execute(select(User).where(User.require_report == True)) all_reporters = managers_result.scalars().all() # Filter: managers only see themselves reporters = all_reporters if role in ("director", "leader") else [m for m in all_reporters if str(m.id) == user_id] # Get visit counts per manager this week visits_result = await db.execute( select(Visit.manager_id, func.count(Visit.id)) .where(Visit.visit_date >= monday, Visit.visit_date <= sunday) .group_by(Visit.manager_id) ) visit_map = {str(uid): cnt for uid, cnt in visits_result.all()} # Load holiday data (auto-refreshes from API if not cached) holidays, workdays = await load_holiday_sets(db) # Query leave records for today — build a set of managers on leave today = today_cst() is_rest = not is_working_day(today, holidays, workdays) leaves_result = await db.execute( select(Leave).where(and_( Leave.start_date <= today, Leave.end_date >= today, )) ) on_leave_today = {str(r.manager_id): r for r in leaves_result.scalars().all()} progress = [] for m in reporters: count = visit_map.get(str(m.id), 0) # Calculate expected working days (Mon-Fri) days_passed = min((date.today() - monday).days + 1, 5) expected = days_passed # At least 1 visit per working day mid = str(m.id) leave = on_leave_today.get(mid) progress.append({ "manager_id": mid, "manager_name": m.name, "department": m.department, "visit_count": count, "expected": expected, "completed": count >= expected, "has_reported_today": False, # Will be set below "on_leave": leave is not None, "is_rest_day": is_rest, "leave_info": { "leave_type": leave.leave_type, "start_date": str(leave.start_date), "end_date": str(leave.end_date), } if leave else None, }) # Check today's reporting — visits OR daily notes today_visits = await db.execute( select(Visit.manager_id).where(Visit.visit_date == today) ) today_notes = await db.execute( select(DailyNote.manager_id).where(DailyNote.note_date == today) ) reported_today = {str(uid) for uid, in today_visits.all()} | {str(uid) for uid, in today_notes.all()} for p in progress: p["has_reported_today"] = p["manager_id"] in reported_today return progress async def get_weekly_report( db: AsyncSession, user_id: UUID, role: str, filter_manager_id: UUID | None = None, filter_customer_id: UUID | None = None, reference_date: date | None = None, ) -> dict: """Get full weekly report data organized by module.""" monday, sunday = get_week_range(reference_date) # Base filters respecting role visibility customer_map = {} user_map = {} customers_result = await db.execute(select(Customer.id, Customer.name)) customer_map = {c.id: c.name for c in customers_result.all()} users_result = await db.execute(select(User.id, User.name)) user_map = {u.id: u.name for u in users_result.all()} def build_manager_filter(existing_filter=None): """If role is manager, only see own data. Otherwise optionally filter by manager_id.""" if role == "manager": return str(user_id) return str(filter_manager_id) if filter_manager_id else None # ── Visits ── visit_query = select(Visit).where(Visit.visit_date >= monday, Visit.visit_date <= sunday) if role == "manager": visit_query = visit_query.where(Visit.manager_id == user_id) elif filter_manager_id: visit_query = visit_query.where(Visit.manager_id == filter_manager_id) if filter_customer_id: visit_query = visit_query.where(Visit.customer_id == filter_customer_id) visit_query = visit_query.order_by(Visit.visit_date.desc()) visits_result = await db.execute(visit_query) visits = visits_result.scalars().all() visits_data = [] for v in visits: # Resolve companion names: system users → names, external → direct companion_names_resolved = [user_map.get(c, str(c)) for c in (v.companions or [])] companion_names_resolved.extend(v.companion_names or []) visits_data.append({ "id": str(v.id), "customer_id": str(v.customer_id), "customer_name": customer_map.get(v.customer_id, ""), "visit_date": str(v.visit_date), "visit_method": v.visit_method, "time_range": v.time_range, "visitor_name": v.visitor_name or "", "visitor_phone": v.visitor_phone or "", "communication_content": v.communication_content, "customer_demand": v.customer_demand, "companions": [str(c) for c in (v.companions or [])], "companion_names": v.companion_names or [], "companion_names_resolved": companion_names_resolved, "photos": v.photos or [], "manager_id": str(v.manager_id), "manager_name": user_map.get(v.manager_id, ""), "edit_log": v.edit_log or [], "created_at": str(v.created_at), }) # ── Work Plans ── wp_query = select(WorkPlan) if role == "manager": wp_query = wp_query.where(WorkPlan.manager_id == user_id) elif filter_manager_id: wp_query = wp_query.where(WorkPlan.manager_id == filter_manager_id) if filter_customer_id: wp_query = wp_query.where(WorkPlan.customer_id == filter_customer_id) wp_result = await db.execute(wp_query) work_plans_data = [] for w in wp_result.scalars(): work_plans_data.append({ "id": str(w.id), "customer_id": str(w.customer_id), "customer_name": customer_map.get(w.customer_id, ""), "plan_content": w.plan_content, "plan_date": str(w.plan_date), "manager_id": str(w.manager_id), "manager_name": user_map.get(w.manager_id, ""), "status": w.status, }) # ── Mini Business ── mb_query = select(MiniBusiness) if role == "manager": mb_query = mb_query.where(MiniBusiness.manager_id == user_id) elif filter_manager_id: mb_query = mb_query.where(MiniBusiness.manager_id == filter_manager_id) if filter_customer_id: mb_query = mb_query.where(MiniBusiness.customer_id == filter_customer_id) mb_result = await db.execute(mb_query) mini_biz_data = [] for m in mb_result.scalars(): mini_biz_data.append({ "id": str(m.id), "customer_id": str(m.customer_id), "customer_name": customer_map.get(m.customer_id, ""), "product_type": m.product_type, "amount": m.amount, "follow_up_detail": m.follow_up_detail, "status": m.status, "manager_id": str(m.manager_id), "manager_name": user_map.get(m.manager_id, ""), "expected_revenue_date": m.expected_revenue_date, }) # ── Key Visits ── kv_query = select(KeyVisit) if role == "manager": kv_query = kv_query.where(KeyVisit.manager_id == user_id) elif filter_manager_id: kv_query = kv_query.where(KeyVisit.manager_id == filter_manager_id) if filter_customer_id: kv_query = kv_query.where(KeyVisit.customer_id == filter_customer_id) kv_result = await db.execute(kv_query) key_visits_data = [] for k in kv_result.scalars(): key_visits_data.append({ "id": str(k.id), "customer_id": str(k.customer_id), "customer_name": customer_map.get(k.customer_id, ""), "urgency_level": k.urgency_level, "description": k.description, "progress_status": k.progress_status, "planned_date": k.planned_date, "planned_visitor": k.planned_visitor, "visit_target": k.visit_target, "manager_id": str(k.manager_id), "manager_name": user_map.get(k.manager_id, ""), }) # ── Daily Notes ── dn_query = select(DailyNote).where(DailyNote.note_date >= monday, DailyNote.note_date <= sunday) if role == "manager": dn_query = dn_query.where(DailyNote.manager_id == user_id) elif filter_manager_id: dn_query = dn_query.where(DailyNote.manager_id == filter_manager_id) dn_query = dn_query.order_by(DailyNote.note_date.desc()) dn_result = await db.execute(dn_query) daily_notes_data = [] for d in dn_result.scalars(): daily_notes_data.append({ "id": str(d.id), "note_date": str(d.note_date), "category": d.category, "content": d.content, "time_range": d.time_range, "manager_id": str(d.manager_id), "manager_name": user_map.get(d.manager_id, ""), }) return { "week_start": str(monday), "week_end": str(sunday), "visits": visits_data, "daily_notes": daily_notes_data, "work_plans": work_plans_data, "mini_business": mini_biz_data, "key_visits": key_visits_data, }