feat: 历史周报+图片预览+照片管理+导入优化+用户合并

新增:
- 历史周报: Dashboard/周报详情支持周选择器翻看往周,归档只读
- 图片预览: 全屏大图(移动端+PC端),点击遮罩关闭
- PC端照片管理: 编辑时可上传/删除照片
- 客户导入改为更新模式: 重名自动更新信息,显示操作明细
- 导入跳过原因: 旧周报导入也显示每条跳过原因

优化:
- 填报进度权限: 经理只看到自己,支局长/领导看全员
- 客户经理暖灰色hash标签列
- 用户去重合并(4组),数据完整迁移
- CLAUDE.md 更新到最新状态

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:59:31 +08:00
parent 8926475b20
commit aa3fbca710
15 changed files with 323 additions and 74 deletions
+37 -29
View File
@@ -194,11 +194,11 @@ async def import_customers(
raise HTTPException(status_code=400, detail=f"Excel 解析失败: {str(e)}")
ws = wb.active
created, skipped = 0, 0
errors = []
created, updated, skipped = 0, 0, 0
reasons = []
# Build user name → id lookup
user_rows = await db.execute(select(User.name, User.id).where(User.role == "manager"))
# Build user name → id lookup (all users, not just managers)
user_rows = await db.execute(select(User.name, User.id))
user_map = {name: uid for name, uid in user_rows.all()}
default_user_id = uuid_mod.UUID(current_user["user_id"])
@@ -220,37 +220,45 @@ async def import_customers(
contact_phone = str(row[9]).strip() if len(row) > 9 and row[9] else ""
contact_role = str(row[10]).strip() if len(row) > 10 and row[10] else ""
existing = await db.execute(select(Customer).where(Customer.name == name))
if existing.scalar_one_or_none():
skipped += 1
continue
# Resolve manager: by name from template, or fallback to current user
assignee_id = user_map.get(mgr_name, default_user_id)
existing_result = await db.execute(select(Customer).where(Customer.name == name))
existing = existing_result.scalar_one_or_none()
try:
customer = Customer(
name=name, industry=industry, address=address,
in_use_services=services, monthly_fee=fee,
remarks=remarks,
created_by=default_user_id,
)
db.add(customer)
await db.flush()
if contact_name:
db.add(CustomerContact(customer_id=customer.id, name=contact_name, phone=contact_phone, role_desc=contact_role))
db.add(CustomerAssignment(
customer_id=customer.id, manager_id=assignee_id,
role="primary", assigned_by=default_user_id,
))
created += 1
if existing:
# Update existing customer
existing.industry = industry or existing.industry
existing.address = address or existing.address
existing.in_use_services = services or existing.in_use_services
existing.monthly_fee = fee or existing.monthly_fee
existing.remarks = remarks or existing.remarks
# Update or create primary assignment if manager changed
if mgr_name:
assign_rows = await db.execute(
select(CustomerAssignment).where(CustomerAssignment.customer_id == existing.id, CustomerAssignment.role == "primary")
)
first_assign = assign_rows.first()
if first_assign:
first_assign[0].manager_id = assignee_id
else:
db.add(CustomerAssignment(customer_id=existing.id, manager_id=assignee_id, role="primary", assigned_by=default_user_id))
updated += 1
reasons.append(f"更新「{name}」的信息")
else:
customer = Customer(name=name, industry=industry, address=address, in_use_services=services, monthly_fee=fee, remarks=remarks, created_by=default_user_id)
db.add(customer)
await db.flush()
if contact_name:
db.add(CustomerContact(customer_id=customer.id, name=contact_name, phone=contact_phone, role_desc=contact_role))
db.add(CustomerAssignment(customer_id=customer.id, manager_id=assignee_id, role="primary", assigned_by=default_user_id))
created += 1
reasons.append(f"新建「{name}")
except Exception as e:
errors.append(f"{row_idx}行: {str(e)}")
errors.append(f"{row_idx}({name}): {str(e)}")
await db.commit()
return {"created": created, "skipped": skipped, "errors": errors}
return {"created": created, "updated": updated, "skipped": skipped, "reasons": reasons, "errors": errors}
@router.get("/check-duplicate/{name}")
+13 -5
View File
@@ -1,4 +1,5 @@
import uuid
from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,35 +12,42 @@ router = APIRouter(prefix="/dashboard", tags=["Dashboard"])
@router.get("/stats")
async def dashboard_stats(
reference_date: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get dashboard card statistics for the current week."""
stats = await get_dashboard_stats(db)
"""Get dashboard card statistics. Pass reference_date (YYYY-MM-DD) for historical weeks."""
ref = date.fromisoformat(reference_date) if reference_date else None
stats = await get_dashboard_stats(db, ref)
return stats
@router.get("/progress")
async def reporting_progress(
reference_date: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get per-manager reporting progress."""
return await get_reporting_progress(db)
"""Get per-manager reporting progress. Managers only see themselves."""
ref = date.fromisoformat(reference_date) if reference_date else None
return await get_reporting_progress(db, ref, current_user["user_id"], current_user["role"])
@router.get("/weekly-report")
async def weekly_report(
manager_id: Optional[str] = Query(None),
customer_id: Optional[str] = Query(None),
reference_date: Optional[str] = Query(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get full weekly report data (four modules)."""
"""Get full weekly report data. Pass reference_date for historical weeks."""
ref = date.fromisoformat(reference_date) if reference_date else None
return await get_weekly_report(
db=db,
user_id=uuid.UUID(current_user["user_id"]),
role=current_user["role"],
filter_manager_id=uuid.UUID(manager_id) if manager_id else None,
filter_customer_id=uuid.UUID(customer_id) if customer_id else None,
reference_date=ref,
)
+7 -3
View File
@@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends
from typing import Optional
from datetime import date
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
@@ -10,11 +12,13 @@ router = APIRouter(prefix="/export", tags=["Export"])
@router.get("/weekly-report")
async def download_weekly_report(
reference_date: Optional[str] = Query(None),
current_user: dict = Depends(require_director_or_leader),
db: AsyncSession = Depends(get_db),
):
"""Export this week's report as a 4-sheet .xlsx file."""
excel_bytes = await export_weekly_report(db)
"""Export week report as 4-sheet .xlsx. Pass reference_date for historical weeks."""
ref = date.fromisoformat(reference_date) if reference_date else None
excel_bytes = await export_weekly_report(db, ref)
return StreamingResponse(
excel_bytes,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",