feat: 历史周报+图片预览+照片管理+导入优化+用户合并
新增: - 历史周报: Dashboard/周报详情支持周选择器翻看往周,归档只读 - 图片预览: 全屏大图(移动端+PC端),点击遮罩关闭 - PC端照片管理: 编辑时可上传/删除照片 - 客户导入改为更新模式: 重名自动更新信息,显示操作明细 - 导入跳过原因: 旧周报导入也显示每条跳过原因 优化: - 填报进度权限: 经理只看到自己,支局长/领导看全员 - 客户经理暖灰色hash标签列 - 用户去重合并(4组),数据完整迁移 - CLAUDE.md 更新到最新状态 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,9 +20,9 @@ def get_week_range(reference_date: date | None = None):
|
||||
return monday, sunday
|
||||
|
||||
|
||||
async def get_dashboard_stats(db: AsyncSession) -> dict:
|
||||
"""Get dashboard statistics for the current week."""
|
||||
monday, sunday = get_week_range()
|
||||
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(
|
||||
@@ -51,13 +51,14 @@ async def get_dashboard_stats(db: AsyncSession) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def get_reporting_progress(db: AsyncSession) -> list[dict]:
|
||||
"""Get per-manager reporting progress for the current week."""
|
||||
monday, sunday = get_week_range()
|
||||
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 managers
|
||||
managers_result = await db.execute(select(User).where(User.role == "manager"))
|
||||
managers = managers_result.scalars().all()
|
||||
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(
|
||||
@@ -102,9 +103,10 @@ 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()
|
||||
monday, sunday = get_week_range(reference_date)
|
||||
|
||||
# Base filters respecting role visibility
|
||||
customer_map = {}
|
||||
|
||||
@@ -15,7 +15,7 @@ from app.models.user import User
|
||||
async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uuid.UUID) -> dict:
|
||||
"""Parse old weekly report Excel and import data. Returns summary stats."""
|
||||
wb = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=True)
|
||||
stats = {"visits": 0, "work_plans": 0, "mini_business": 0, "key_visits": 0, "skipped": 0}
|
||||
stats = {"visits": 0, "work_plans": 0, "mini_business": 0, "key_visits": 0, "skipped": 0, "skip_reasons": []}
|
||||
|
||||
# Resolve customer name -> id cache
|
||||
customers_result = await db.execute(select(Customer.id, Customer.name))
|
||||
@@ -38,6 +38,7 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui
|
||||
customer_id = customer_map.get(cust_name)
|
||||
if not customer_id:
|
||||
stats["skipped"] += 1
|
||||
stats["skip_reasons"].append(f"客户「{cust_name}」不存在,跳过")
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -54,6 +55,7 @@ async def import_from_excel(db: AsyncSession, file_bytes: bytes, manager_id: uui
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
stats["skipped"] += 1
|
||||
stats["skip_reasons"].append(f"重复:{cust_name} {visit_date} 已存在")
|
||||
continue
|
||||
|
||||
visit = Visit(
|
||||
|
||||
Reference in New Issue
Block a user