Files
qiji/backend/app/services/dashboard.py
T
v6ole ab20bc5a1f Merge branch 'main' into develop
# Conflicts:
#	backend/app/api/key_visits.py
#	backend/app/api/mini_business.py
#	backend/app/api/visits.py
#	backend/app/api/work_plans.py
#	backend/app/main.py
#	backend/app/models/__init__.py
#	backend/app/schemas/key_visit.py
#	backend/app/schemas/mini_business.py
#	backend/app/schemas/work_plan.py
#	backend/app/services/light_board.py
#	frontend/src/components/DesktopLayout.vue
#	frontend/src/stores/theme.ts
#	frontend/src/views/desktop/ManagerWorkspace.vue
#	frontend/src/views/desktop/WorkPlans.vue
#	frontend/src/views/mobile/KeyVisitForm.vue
#	frontend/src/views/mobile/LeaveForm.vue
#	frontend/src/views/mobile/PlansList.vue
#	frontend/src/views/mobile/VisitForm.vue
#	frontend/src/views/mobile/WorkPlanForm.vue
2026-08-17 09:31:12 +08:00

350 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, parse_date
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, user_id: str = "", role: str = "") -> 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(func.distinct(func.coalesce(Visit.visit_group_id, 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
# Overdue plans (status=计划中, plan_date < today). Managers see only own.
overdue_q = select(func.count(WorkPlan.id)).where(
WorkPlan.status == "计划中",
WorkPlan.plan_date < today,
)
if role == "manager":
overdue_q = overdue_q.where(WorkPlan.manager_id == UUID(user_id))
overdue_plans = (await db.execute(overdue_q)).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,
"overdue_plans": overdue_plans,
"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 (direct + companions) OR daily notes
from sqlalchemy import union_all
today_direct = select(Visit.manager_id).where(Visit.visit_date == today)
today_companion = select(func.unnest(Visit.companions).label("manager_id")).where(
Visit.visit_date == today, Visit.companions.isnot(None)
)
today_combined = union_all(today_direct, today_companion).subquery()
today_visits = await db.execute(select(today_combined.c.manager_id))
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) | (Visit.companions.any(user_id))
)
elif filter_manager_id:
visit_query = visit_query.where(
(Visit.manager_id == filter_manager_id) | (Visit.companions.any(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()
# Group visits by visit_group_id (or id for solo visits)
groups: dict[str, list] = {}
for v in visits:
gid = str(v.visit_group_id) if v.visit_group_id else str(v.id)
groups.setdefault(gid, []).append(v)
visits_data = []
for gid, gvisits in groups.items():
# Determine the "primary" — the creator's record (not a companion copy)
def _is_companion_copy(v) -> bool:
content = (v.communication_content or "").strip()
# New-style: has "(协同XXX" suffix
if content.endswith("") and "(协同" in content:
return True
# Old-style: empty draft created for companion
if not content:
return True
return False
creator_records = [v for v in gvisits if not _is_companion_copy(v)]
primary = creator_records[0] if creator_records else gvisits[0]
companion_names_resolved = [user_map.get(c, str(c)) for c in (primary.companions or [])]
companion_names_resolved.extend(primary.companion_names or [])
merged = {
"id": str(primary.id),
"customer_id": str(primary.customer_id),
"customer_name": customer_map.get(primary.customer_id, ""),
"visit_date": str(primary.visit_date),
"visit_method": primary.visit_method,
"time_range": primary.time_range,
"visitor_name": primary.visitor_name or "",
"visitor_phone": primary.visitor_phone or "",
"visit_group_id": gid if len(gvisits) > 1 else None,
"companions": [str(c) for c in (primary.companions or [])],
"companion_names": primary.companion_names or [],
"companion_names_resolved": companion_names_resolved,
"communication_content": primary.communication_content,
"customer_demand": primary.customer_demand,
"photos": primary.photos or [],
"manager_id": str(primary.manager_id),
"manager_name": user_map.get(primary.manager_id, ""),
"edit_log": primary.edit_log or [],
"created_at": str(primary.created_at),
"participants": [
{
"manager_id": str(v.manager_id),
"manager_name": user_map.get(v.manager_id, ""),
"role": "primary" if v is primary else "companion",
"communication_content": v.communication_content,
"customer_demand": v.customer_demand,
"photos": v.photos or [],
}
for v in gvisits
],
}
visits_data.append(merged)
# ── 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,
}