aa3fbca710
新增: - 历史周报: Dashboard/周报详情支持周选择器翻看往周,归档只读 - 图片预览: 全屏大图(移动端+PC端),点击遮罩关闭 - PC端照片管理: 编辑时可上传/删除照片 - 客户导入改为更新模式: 重名自动更新信息,显示操作明细 - 导入跳过原因: 旧周报导入也显示每条跳过原因 优化: - 填报进度权限: 经理只看到自己,支局长/领导看全员 - 客户经理暖灰色hash标签列 - 用户去重合并(4组),数据完整迁移 - CLAUDE.md 更新到最新状态 Co-Authored-By: Claude <noreply@anthropic.com>
256 lines
9.7 KiB
Python
256 lines
9.7 KiB
Python
from datetime import date, timedelta
|
|
from uuid import UUID
|
|
from sqlalchemy import select, func
|
|
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.utils.timezone import today_cst
|
|
|
|
|
|
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
|
|
|
|
return {
|
|
"week_visits": visits_count,
|
|
"work_plans": plans_count,
|
|
"mini_business": mini_biz_count,
|
|
"key_visits": key_visit_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)
|
|
|
|
managers_result = await db.execute(select(User).where(User.role == "manager"))
|
|
all_managers = managers_result.scalars().all()
|
|
# Filter: managers only see themselves
|
|
managers = all_managers if role in ("director", "leader") else [m for m in all_managers 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()}
|
|
|
|
progress = []
|
|
for m in managers:
|
|
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
|
|
progress.append({
|
|
"manager_id": str(m.id),
|
|
"manager_name": m.name,
|
|
"department": m.department,
|
|
"visit_count": count,
|
|
"expected": expected,
|
|
"completed": count >= expected,
|
|
"has_reported_today": False, # Will be set below
|
|
})
|
|
|
|
# Check today's reporting — visits OR daily notes
|
|
today = today_cst()
|
|
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:
|
|
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,
|
|
"communication_content": v.communication_content,
|
|
"customer_demand": v.customer_demand,
|
|
"companions": [str(c) for c in (v.companions or [])],
|
|
"photos": v.photos or [],
|
|
"manager_id": str(v.manager_id),
|
|
"manager_name": user_map.get(v.manager_id, ""),
|
|
"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,
|
|
}
|