feat: 三项联动 — 计划过期提醒 + 拜访自动关闭 + 批量制定

1. 计划过期自动提醒:
   - scheduler.py 新增 check_overdue_plans()
   - 每日 9:00 检查过期计划(plan_date < today, status=计划中)
   - 按经理汇总 → 企微推送提醒消息

2. 拜访后自动关闭计划:
   - create_visit 后自动将匹配的计划标记为「已完成」
   - 条件: customer_id 匹配 + status=计划中 + plan_date <= visit_date

3. 亮灯表批量制定:
   - 红灯/黄灯卡片右上角 ☐ 复选框
   - 勾选后出现「批量制定计划」按钮
   - 弹窗统一设置日期+内容 → 批量 POST

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-27 12:28:36 +08:00
parent 0e25201ede
commit b343970ecc
4 changed files with 165 additions and 4 deletions
+83 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { dashboardApi } from '@/api/dashboard'
@@ -14,13 +14,58 @@ const board = ref<any>(null)
const expandedManagers = ref<Set<string>>(new Set())
const monthOffset = ref(0)
// ── Dialog state ──
// ── Single dialog state ──
const dialogVisible = ref(false)
const selectedCustomer = ref<any>(null)
const planDate = ref(todayStr())
const planContent = ref('')
const planSaving = ref(false)
// ── Batch selection ──
const selectedCards = ref<Set<string>>(new Set())
const batchDialogVisible = ref(false)
const batchPlanDate = ref(todayStr())
const batchPlanContent = ref('')
const batchSaving = ref(false)
watch(expandedManagers, () => { selectedCards.value.clear() })
function toggleCardSelect(customerId: string) {
const s = new Set(selectedCards.value)
if (s.has(customerId)) s.delete(customerId)
else s.add(customerId)
selectedCards.value = s
}
function openBatchDialog() {
if (selectedCards.value.size === 0) { ElMessage.warning('请先勾选客户卡片'); return }
batchPlanContent.value = ''
batchPlanDate.value = todayStr()
batchDialogVisible.value = true
}
async function handleBatchCreate() {
if (!batchPlanContent.value.trim()) { ElMessage.warning('请输入计划内容'); return }
batchSaving.value = true
let created = 0
for (const cid of selectedCards.value) {
try {
await api.post('/work-plans/', {
customer_id: cid,
plan_content: batchPlanContent.value.trim(),
plan_date: batchPlanDate.value,
status: '计划中',
})
created++
} catch (_) { /* continue */ }
}
ElMessage.success(`已为 ${created} 个客户制定计划`)
batchDialogVisible.value = false
selectedCards.value.clear()
batchSaving.value = false
await loadData()
}
const referenceMonth = computed(() => {
const d = new Date()
d.setMonth(d.getMonth() + monthOffset.value)
@@ -159,7 +204,14 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
</div>
<div v-if="expandedManagers.has(m.manager_id)" class="customer-grid">
<div v-for="cust in m.customers" :key="cust.customer_id" class="customer-card" :class="`customer-card--${cust.status}`" @click="openCustomerDialog(cust)">
<div class="batch-actions" v-if="selectedCards.size > 0" style="width:100%;margin-bottom:8px">
<el-button type="primary" size="small" @click="openBatchDialog">📋 批量制定计划 ({{ selectedCards.size }})</el-button>
<el-button size="small" @click="selectedCards.clear()">取消选择</el-button>
</div>
<div v-for="cust in m.customers" :key="cust.customer_id" class="customer-card" :class="['customer-card--' + cust.status, { 'card-selected': selectedCards.has(cust.customer_id) }]" @click="openCustomerDialog(cust)">
<div v-if="cust.status !== 'green' && cust.status !== 'gray'" class="card-check" @click.stop="toggleCardSelect(cust.customer_id)">
<span v-if="selectedCards.has(cust.customer_id)"></span><span v-else></span>
</div>
<div class="card-status-stripe"></div>
<div class="card-body">
<div class="card-name-row">
@@ -270,6 +322,23 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
</el-link>
</div>
</el-dialog>
<!-- Batch Plan Dialog -->
<el-dialog v-model="batchDialogVisible" title="批量制定拜访计划" width="480px">
<p style="margin:0 0 12px;color:var(--c-text-muted)">将为 <strong>{{ selectedCards.size }}</strong> 个客户统一制定拜访计划</p>
<el-form label-position="top">
<el-form-item label="计划时间">
<el-date-picker v-model="batchPlanDate" type="date" style="width:100%" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="计划内容">
<el-input v-model="batchPlanContent" type="textarea" :rows="3" placeholder="统一的拜访计划内容" :disabled="batchSaving" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="batchSaving" @click="handleBatchCreate">批量制定</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -376,4 +445,15 @@ const statusLabel: Record<string, string> = { green: '本月已拜访', yellow:
.dlg-plan-item.dlg-plan-overdue { background: #FBF1EE; margin: 2px -4px; padding: 4px; border-radius: 4px; }
.dlg-plan-form { background: var(--c-bg-light, #faf9f6); padding: 10px 12px; border-radius: 6px; }
.plan-form-row { display: flex; gap: 8px; align-items: center; }
/* ═══ Card Checkbox ═══ */
.card-check {
position: absolute; top: 4px; right: 4px;
width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 14px; color: var(--warm-gray);
border-radius: 4px; background: rgba(255,255,255,0.8);
z-index: 2;
}
.card-check:hover { color: var(--ink); background: rgba(196,147,74,0.1); }
.customer-card.card-selected { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(196,147,74,0.2); }
</style>