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:
@@ -22,7 +22,7 @@ async def download_weekly_report_template():
|
|||||||
# Sheet 1: 每日拜访记录
|
# Sheet 1: 每日拜访记录
|
||||||
ws1 = wb.active
|
ws1 = wb.active
|
||||||
ws1.title = "每日拜访记录"
|
ws1.title = "每日拜访记录"
|
||||||
ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"])
|
ws1.append(["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "相关人员", "客户经理"])
|
||||||
for c in ws1[1]: c.font = header_font
|
for c in ws1[1]: c.font = header_font
|
||||||
ws1.append(["XX科技有限公司", "2026-06-23", "上门", "9:00-10:00", "韦柳柏", "13800000000", "沟通了解云桌面需求", "希望扩容", "韦柳柏, 张科长", "韦矍森"])
|
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
|
ws1.column_dimensions['A'].width = 20; ws1.column_dimensions['E'].width = 30; ws1.column_dimensions['F'].width = 20
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ async def get_weekly_report(
|
|||||||
|
|
||||||
visits_data = []
|
visits_data = []
|
||||||
for v in visits:
|
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({
|
visits_data.append({
|
||||||
"id": str(v.id),
|
"id": str(v.id),
|
||||||
"customer_id": str(v.customer_id),
|
"customer_id": str(v.customer_id),
|
||||||
@@ -145,12 +148,17 @@ async def get_weekly_report(
|
|||||||
"visit_date": str(v.visit_date),
|
"visit_date": str(v.visit_date),
|
||||||
"visit_method": v.visit_method,
|
"visit_method": v.visit_method,
|
||||||
"time_range": v.time_range,
|
"time_range": v.time_range,
|
||||||
|
"visitor_name": v.visitor_name or "",
|
||||||
|
"visitor_phone": v.visitor_phone or "",
|
||||||
"communication_content": v.communication_content,
|
"communication_content": v.communication_content,
|
||||||
"customer_demand": v.customer_demand,
|
"customer_demand": v.customer_demand,
|
||||||
"companions": [str(c) for c in (v.companions or [])],
|
"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 [],
|
"photos": v.photos or [],
|
||||||
"manager_id": str(v.manager_id),
|
"manager_id": str(v.manager_id),
|
||||||
"manager_name": user_map.get(v.manager_id, ""),
|
"manager_name": user_map.get(v.manager_id, ""),
|
||||||
|
"edit_log": v.edit_log or [],
|
||||||
"created_at": str(v.created_at),
|
"created_at": str(v.created_at),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async def export_weekly_report(db: AsyncSession, reference_date: date | None = N
|
|||||||
# ── Sheet 1: 每日拜访记录 ──
|
# ── Sheet 1: 每日拜访记录 ──
|
||||||
ws1 = wb.active
|
ws1 = wb.active
|
||||||
ws1.title = "每日拜访记录"
|
ws1.title = "每日拜访记录"
|
||||||
headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "同访人员", "客户经理"]
|
headers1 = ["客户单位", "拜访日期", "拜访方式", "时间范围", "拜访人姓名", "拜访人电话", "沟通内容", "客户需求", "相关人员", "客户经理"]
|
||||||
ws1.append(headers1)
|
ws1.append(headers1)
|
||||||
for col in range(1, len(headers1) + 1):
|
for col in range(1, len(headers1) + 1):
|
||||||
cell = ws1.cell(row=1, column=col)
|
cell = ws1.cell(row=1, column=col)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Customer light board — visit coverage matrix per manager.
|
"""Customer light board — visit coverage matrix per manager.
|
||||||
|
|
||||||
Status:
|
Status (rolling 30-day window):
|
||||||
green — visited this month (亮灯)
|
green — visited within last 30 days (亮灯)
|
||||||
yellow — visited last month but not this month (临期)
|
yellow — visited 31-60 days ago (临期)
|
||||||
red — not visited in 2+ months, or never visited (灭灯+警示)
|
red — visited 61+ days ago, or never visited (灭灯+警示)
|
||||||
gray — customer has no assigned primary manager (未分配)
|
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]:
|
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:
|
if last_visit_date is None:
|
||||||
return ("red", 99) # never visited
|
return ("red", 99) # never visited
|
||||||
|
|
||||||
this_month = month_start(ref)
|
days_since = (ref - last_visit_date).days
|
||||||
last_month = month_start(this_month - timedelta(days=1))
|
|
||||||
|
|
||||||
if last_visit_date >= this_month:
|
if days_since <= 30:
|
||||||
return ("green", 0)
|
return ("green", 0)
|
||||||
elif last_visit_date >= last_month:
|
elif days_since <= 60:
|
||||||
return ("yellow", 0)
|
return ("yellow", 0)
|
||||||
|
|
||||||
# Count how many consecutive months missed
|
# Each 30-day block beyond 60 days counts as one missed period
|
||||||
cursor = month_start(ref)
|
periods = days_since // 30
|
||||||
missed = 0
|
return ("red", min(periods, 99))
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_light_board(
|
async def get_light_board(
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function goWeeklyReport(customerId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Status label map
|
// 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
<span v-if="cust.in_use_services" class="card-services">{{ cust.in_use_services }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-visit-info">
|
<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 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>
|
<span v-if="cust.last_visit_method" class="card-method">{{ cust.last_visit_method }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +182,7 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="cust.status === 'red' && cust.consecutive_missed_months > 0" class="card-warning">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -221,7 +221,7 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
|
|||||||
<div class="dlg-status" :class="`dlg-status--${selectedCustomer.status}`">
|
<div class="dlg-status" :class="`dlg-status--${selectedCustomer.status}`">
|
||||||
{{ statusLabel[selectedCustomer.status] }}
|
{{ statusLabel[selectedCustomer.status] }}
|
||||||
<span v-if="selectedCustomer.consecutive_missed_months > 1" style="margin-left:6px">
|
<span v-if="selectedCustomer.consecutive_missed_months > 1" style="margin-left:6px">
|
||||||
(连续 {{ selectedCustomer.consecutive_missed_months }} 个月)
|
(连续 {{ selectedCustomer.consecutive_missed_months }} 个周期)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ const notesByDate = computed(() => {
|
|||||||
</el-form-item>
|
</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_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-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-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-option v-for="u in allUsers" :key="u.id" :label="u.name" :value="u.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ async function handleDelete() {
|
|||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<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>
|
</template>
|
||||||
<el-select v-model="form.companions" multiple filterable allow-create default-first-option placeholder="选择或输入姓名(可多选)" style="width:100%">
|
<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" />
|
<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.
Reference in New Issue
Block a user