10835ef15c
- 删除 POST /visits 中的草案创建逻辑(不再创建空拜访记录) - 周报查询:manager 角色也显示同访的拜访记录 - 亮灯表覆盖:同访人也算客户拜访覆盖(UNION companions) - 填报进度:同访人也算今日已填报 - 清理已有 2 条空草案记录 Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
12 KiB
Python
308 lines
12 KiB
Python
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 (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()
|
|
|
|
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,
|
|
}
|