feat: 操作日志系统 + 日期修复 + 协同去重 + 主题持久化 + 计划自动完成
本次累积提交包含以下功能: 1. 修复:el-date-picker 缺失 value-format 导致日期保存偏移(8处) 2. 新增:协同拜访 visit_group_id 去重机制(完整副本 + 周报合并 + 亮灯去重) 3. 新增:计划自动完成(创建计划检测已有拜访 / 编辑拜访触发) 4. 新增:拜访表单选择客户后自动预填过期计划内容 5. 新增:移动端主题切换按钮 + 后端持久化 6. 新增:操作日志系统 (audit_logs),全覆盖 8 个模块 CRUD 7. 新增:PC端「我的数据」支局长/领导视角客户经理列 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -103,4 +103,5 @@ def build_token_for_user(user: User) -> str:
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"department": user.department,
|
||||
"theme": user.theme or "editorial",
|
||||
})
|
||||
|
||||
@@ -28,7 +28,8 @@ async def get_dashboard_stats(db: AsyncSession, reference_date: date | None = No
|
||||
today = date.today()
|
||||
|
||||
visits_count = (await db.execute(
|
||||
select(func.count(Visit.id)).where(Visit.visit_date >= monday, Visit.visit_date <= sunday)
|
||||
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(
|
||||
@@ -171,31 +172,52 @@ async def get_weekly_report(
|
||||
visits_result = await db.execute(visit_query)
|
||||
visits = visits_result.scalars().all()
|
||||
|
||||
visits_data = []
|
||||
# Group visits by visit_group_id (or id for solo visits)
|
||||
groups: dict[str, list] = {}
|
||||
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 [],
|
||||
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" — first record in group (the creator's)
|
||||
primary = 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,
|
||||
"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),
|
||||
})
|
||||
"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)
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.leave import Leave
|
||||
from app.models.user import User
|
||||
from app.utils.timezone import today_cst
|
||||
from app.utils.audit import log_audit
|
||||
from app.services.dashboard import get_week_range
|
||||
|
||||
|
||||
@@ -88,6 +89,7 @@ async def create_leave(
|
||||
data: dict,
|
||||
submitted_by: UUID,
|
||||
role: str,
|
||||
operator_name: str = "",
|
||||
) -> Leave:
|
||||
"""Create leave. Manager can only create for self; director can create for anyone."""
|
||||
if role == "manager":
|
||||
@@ -108,6 +110,9 @@ async def create_leave(
|
||||
db.add(leave)
|
||||
await db.commit()
|
||||
await db.refresh(leave)
|
||||
# Audit log
|
||||
await log_audit(db, "leave", leave.id, f"{leave.leave_type} ({leave.start_date}~{leave.end_date})", "create", str(submitted_by), operator_name)
|
||||
await db.commit()
|
||||
return leave
|
||||
|
||||
|
||||
@@ -117,6 +122,7 @@ async def update_leave(
|
||||
data: dict,
|
||||
user_id: UUID,
|
||||
role: str,
|
||||
operator_name: str = "",
|
||||
) -> Leave:
|
||||
"""Update leave. Director can edit any; manager can edit own submitted leaves."""
|
||||
leave = await get_leave_by_id(db, leave_id)
|
||||
@@ -136,6 +142,9 @@ async def update_leave(
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(leave)
|
||||
# Audit log
|
||||
await log_audit(db, "leave", leave.id, f"{leave.leave_type} ({leave.start_date}~{leave.end_date})", "update", str(user_id), operator_name)
|
||||
await db.commit()
|
||||
return leave
|
||||
|
||||
|
||||
@@ -144,6 +153,7 @@ async def delete_leave(
|
||||
leave_id: UUID,
|
||||
user_id: UUID,
|
||||
role: str,
|
||||
operator_name: str = "",
|
||||
) -> bool:
|
||||
"""Delete leave. Director can delete any; manager can delete own submitted leaves."""
|
||||
leave = await get_leave_by_id(db, leave_id)
|
||||
@@ -154,8 +164,12 @@ async def delete_leave(
|
||||
if str(leave.submitted_by) != str(user_id):
|
||||
raise PermissionError("客户经理只能删除自己提交的请假")
|
||||
|
||||
entity_name = f"{leave.leave_type} ({leave.start_date}~{leave.end_date})"
|
||||
await db.delete(leave)
|
||||
await db.commit()
|
||||
# Audit log
|
||||
await log_audit(db, "leave", leave_id, entity_name, "delete", str(user_id), operator_name)
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user