feat: 亮灯表30天滚动窗口 + 相关人员文案统一 + 周报数据补全

- light_board.classify: 自然月 → 30/60天滚动窗口 (≤30天🟢, 31-60天🟡, 61+天🔴)
- LightBoard.vue: 4处文案同步 (近30天已拜访 / 上次 / 个周期)
- dashboard.get_weekly_report: 补全 companion_names_resolved + visitor_name + visitor_phone + edit_log
- 导入/导出模板 + VisitForm + ManagerWorkspace: 同访人员 → 相关人员
This commit is contained in:
2026-07-02 16:58:29 +08:00
parent 3285e22142
commit cd47d1aed5
8 changed files with 33 additions and 28 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ async def download_weekly_report_template():
# Sheet 1: 每日拜访记录
ws1 = wb.active
ws1.title = "每日拜访记录"
ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"])
ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "相关人员", "客户经理"])
for c in ws1[1]: c.font = header_font
ws1.append(["XX科技有限公司", "2026-06-23", "上门", "9:00-10:00", "韦柳柏", "13800000000", "沟通了解云桌面需求", "希望扩容", "韦柳柏, 张科长", "韦矍森"])
ws1.column_dimensions['A'].width = 20; ws1.column_dimensions['E'].width = 30; ws1.column_dimensions['F'].width = 20
+8
View File
@@ -138,6 +138,9 @@ async def get_weekly_report(
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),
@@ -145,12 +148,17 @@ async def get_weekly_report(
"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),
})
+1 -1
View File
@@ -41,7 +41,7 @@ async def export_weekly_report(db: AsyncSession, reference_date: date | None = N
# ── Sheet 1: 每日拜访记录 ──
ws1 = wb.active
ws1.title = "每日拜访记录"
headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"]
headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "相关人员", "客户经理"]
ws1.append(headers1)
for col in range(1, len(headers1) + 1):
cell = ws1.cell(row=1, column=col)
+17 -20
View File
@@ -1,9 +1,9 @@
"""Customer light board — visit coverage matrix per manager.
Status:
green — visited this month (亮灯)
yellow — visited last month but not this month (临期)
red — not visited in 2+ months, or never visited (灭灯+警示)
Status (rolling 30-day window):
green — visited within last 30 days (亮灯)
yellow — visited 31-60 days ago (临期)
red — visited 61+ days ago, or never visited (灭灯+警示)
gray — customer has no assigned primary manager (未分配)
"""
@@ -29,29 +29,26 @@ def month_end(ref: date) -> date:
def classify(last_visit_date: date | None, ref: date) -> tuple[str, int]:
"""Return (status, consecutive_missed_months)."""
"""Return (status, consecutive_missed_periods).
30-day rolling window:
green — visited within 30 days
yellow — visited 31-60 days ago (临期)
red — 61+ days ago, or never visited
"""
if last_visit_date is None:
return ("red", 99) # never visited
this_month = month_start(ref)
last_month = month_start(this_month - timedelta(days=1))
days_since = (ref - last_visit_date).days
if last_visit_date >= this_month:
if days_since <= 30:
return ("green", 0)
elif last_visit_date >= last_month:
elif days_since <= 60:
return ("yellow", 0)
# Count how many consecutive months missed
cursor = month_start(ref)
missed = 0
while True:
cursor = month_start(cursor - timedelta(days=1))
if last_visit_date >= cursor:
break
missed += 1
if missed > 24: # safety cap
break
return ("red", missed)
# Each 30-day block beyond 60 days counts as one missed period
periods = days_since // 30
return ("red", min(periods, 99))
async def get_light_board(
+4 -4
View File
@@ -102,7 +102,7 @@ function goWeeklyReport(customerId: string) {
}
// Status label map
const statusLabel: Record<string, string> = { green: '本月已拜访', yellow: '上月已拜访 (临期)', red: '急需拜访', gray: '未分配' }
const statusLabel: Record<string, string> = { green: '近30天已拜访', yellow: '31-60天前 (临期)', red: '急需拜访', gray: '未分配' }
</script>
<template>
@@ -171,7 +171,7 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
<span v-if="cust.in_use_services" class="card-services">{{ cust.in_use_services }}</span>
</div>
<div class="card-visit-info">
<span class="card-last-visit" v-if="cust.last_visit_date">{{ { green: '最近', yellow: '', red: '上次' }[cust.status] }}{{ cust.last_visit_date }}</span>
<span class="card-last-visit" v-if="cust.last_visit_date">{{ { green: '最近', yellow: '', red: '上次' }[cust.status] }}{{ cust.last_visit_date }}</span>
<span class="card-last-visit card-last-visit--never" v-else>从未拜访</span>
<span v-if="cust.last_visit_method" class="card-method">{{ cust.last_visit_method }}</span>
</div>
@@ -182,7 +182,7 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
</span>
</div>
<div v-if="cust.status === 'red' && cust.consecutive_missed_months > 0" class="card-warning">
连续 {{ cust.consecutive_missed_months }} 未拜访
连续 {{ cust.consecutive_missed_months }} 周期未拜访
</div>
</div>
</div>
@@ -221,7 +221,7 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
<div class="dlg-status" :class="`dlg-status--${selectedCustomer.status}`">
{{ statusLabel[selectedCustomer.status] }}
<span v-if="selectedCustomer.consecutive_missed_months > 1" style="margin-left:6px">
(连续 {{ selectedCustomer.consecutive_missed_months }} )
(连续 {{ selectedCustomer.consecutive_missed_months }} 周期)
</span>
</div>
@@ -461,7 +461,7 @@ const notesByDate = computed(() => {
</el-form-item>
<el-form-item label="拜访人姓名"><el-input v-model="form.visitor_name" placeholder="实际拜访人姓名" /></el-form-item>
<el-form-item label="拜访人电话"><el-input v-model="form.visitor_phone" placeholder="联系电话可选" /></el-form-item>
<el-form-item label="同访人员">
<el-form-item label="相关人员">
<el-select v-model="form.companions" multiple filterable allow-create default-first-option placeholder="选择或输入姓名可多选" style="width:100%">
<el-option v-for="u in allUsers" :key="u.id" :label="u.name" :value="u.id" />
</el-select>
+1 -1
View File
@@ -285,7 +285,7 @@ async function handleDelete() {
<el-form-item>
<template #label>
<span class="form-label">同访人员 <span class="form-label-hint">可输入外部人员</span></span>
<span class="form-label">相关人员 <span class="form-label-hint">可输入外部人员</span></span>
</template>
<el-select v-model="form.companions" multiple filterable allow-create default-first-option placeholder="选择或输入姓名(可多选)" style="width:100%">
<el-option v-for="m in managers" :key="m.id" :label="m.name" :value="m.id" :disabled="m.id === auth.userId" />
Binary file not shown.