My Stores 仪表板 — 两个标签重设计

Path: apps/web/src/pages/overview/ · Created: 2026-05-24

这份文档是什么

这份文档是 My Stores 页面 V2 设计的 source of truth, 取代现有 mvp-v3 mockup (docs.retaintive.ai/ui-design/ui-prototype/mvp-v3) 里的 My Stores section。

实施时按这份文档走,mvp-v3 HTML 不需要改 — 它是过渡 mockup, 这份文档是 final spec。

vs mvp-v3 的变化:

维度mvp-v3 (旧)本文档 (新)
时间选择4 个 time tab (Yesterday / Last Week / This Month / Last Month),每个 tab 不同 column统一 time range picker (8 预设 + Custom max 93 天),复用现有 component
视图切换时间 tab 内单 table mix call + task 数据2 个 functional tab — Call Activity / Outcomes,语义清晰分离
Column 结构Daily / Weekly 两套 column 不同同一 tab 跨所有 time range 保持 column 一致
趋势展示 (↑N% 绿 / ↓N% 红)保留保留,沿用现有 component

Product Positioning

Studio 是 call-data observation tool,不是 CRM。看到什么展示什么 — 不重建客户真相,不替 owner 决定哪些信号该藏。voicemail / no_answer 算 call (客人确实拨过来了);funnel 各阶段分开计数,同客人同事件可能在 Tab 1 和 Tab 2 都出现 — owner 自己 connect。


Page Shell

页面 5 层结构 (从上到下):

  1. Header — "My Stores" 标题 + Snapshot / Export 按钮
  2. Store grid — 8 家店卡片 (复用 mvp-v3 Store row 组件,显示店名 + Open/Closed 状态)
  3. Filter row — Time range picker + Voicemail toggle 同行
    • Time range picker:复用现有 component,8 个预设 (Today / 7d / 14d / 28d / This Week / Last Week / This Month / Last Month) + Custom (max 93 天)
    • Voicemail toggle:[✓] Include voicemail 默认 ON。Owner 可手动切换。切换会重新查询并把 Call Volume 重算
  4. Tab 切换 — Call Activity / Outcomes
  5. 当前 tab 的 multi-store table (主体)

切 tab 不重置 time range / VM toggle。默认时间 = This Week。Time picker + VM toggle 跨 tab 共享,这是相比 mvp-v3 (per-time-tab table) 最大的标准化改进。


Tab 1 — Call Activity

Call activity — 数字格式 closed / total。一个客人为同一件事打多通电话被算多次。

Sample (illustrative numbers, not live data):

#StoreCall VolumeIntro Booking (c/t)Membership (c/t)Cancel Save % (s/t)Complaint Resolve % (r/t)Team Productivity
1Harlem87 ↑5%
35 out · 52 in
14 / 87 ↑12%5 / 45 ↑8%4 / 6 (67%) ↓20%3 / 5 (60%) ↑10%16% ↑3%
2Totowa71 ↑12%
36 out · 35 in
17 / 71 ↑18%6 / 38 ↑10%2 / 2 (100%) —2 / 3 (67%) —23% ↑5%
3Westside42 ↓15%
7 out · 35 in
6 / 42 ↓22%1 / 22 —1 / 12 (8%) ↑50%1 / 3 (33%) ↓40%9% ↓12%
TOTAL425 ↑4%
172 out · 253 in
89 / 425 ↑6%31 / 180 ↑5%12 / 28 (43%) ↑12%9 / 18 (50%) ↑5%19% (avg)

视觉: trend ↑N% 绿色 (#16A34A) 表示同比上升, ↓N% 红色 (#dc2626) 表示下降, 灰色 (#CBCED4) 表示无对比数据。Cancel Save / Complaint Resolve 列 ↑ = 救回 / 解决率上升 = 好事 = 绿色。沿用现有 component。

Design philosophy — "看苦劳 → 看功劳":Call Volume 是 activity metric(折叠到 subtext),3 个 outcome 列(Intro / Membership / Cancel Save)+ Complaint Resolve(投诉处理)是 outcome metrics(占主体),Team Productivity 是 rate metric。Voicemail toggle 控制 Call Volume 是否含 voicemail。Cancel Save % 严格只算 cancellation_risk 类 call(客人主动要走的);Complaint Resolve % 单独一列算 retention 类 call(投诉/billing/服务争议)— 两类语义不同,各自独立分母不混。

Calculation Recipes (Tab 1)

源表 calls (Neon) · Schema @retaintive/common/db/schema/calls.ts · Enum 常量 apps/api/src/constants/call-analysis.ts (CallCategory / CallSubcategory / OutcomeResult / CallState / CallDirection)

所有公式默认前缀 WHERE store_id = ? AND start_time IN period,下表只列额外条件。

公式 (在 base WHERE 上追加)含义 / 备注
Call Volume (主数字)COUNT(*) — 无追加 filter所有 call_state 都算,含 voicemail / no_answer
· Outbound (subtext)AND direction = 'Outbound'RingCentral enum 首字母大写,SQL 里写 'Outbound' 不是 'outbound'
· Inbound (subtext)AND direction = 'Inbound'Outbound + Inbound = Call Volume (direction 只有 2 值)
Intro Booking totalAND primary_subcategory = 'intro_booking'新客人 first-time,free / $12 区间;$25+ 促销 / 已 booked online 的 follow-up 不算
Intro Booking closedtotal + AND primary_outcome_result = 'success'客人确认了 class 时间
Membership totalAND primary_subcategory IN ('membership_purchase_related', 'reactivation_purchase')新签 + 询价 + winback。不含 member_upgrade / member_package (待定决策见 Pending Team Discussion · Decision 1)
Membership closedtotal + AND primary_outcome_result = 'success'签了新会员 / winback 成功
Cancel Save totalAND primary_subcategory = 'membership_cancel'客人主动要 cancel/freeze/downgrade(完全终止 membership)。membership_freeze 不算
Cancel Save savedtotal + AND primary_outcome_result = 'retained'retainedmembership_cancel 专用 outcome
Cancel Save % savedsaved / total × 100前端 compute;total = 0 显示 严格不混 complaint — 投诉处理在下一列单算
Complaint Resolve totalAND primary_subcategory IN ('member_support', 'billing_issue', 'cancellation_fee_dispute', 'complaint_feedback')客人没要 cancel,但有投诉 / billing 争议 / 服务质量问题。语义独立于 Cancel Save
Complaint Resolve resolvedtotal + AND primary_outcome_result = 'resolved'投诉处理掉了,客人继续留
Complaint Resolve %resolved / total × 100前端 compute;total = 0 显示 。Owner 通过 Cancel Save vs Complaint Resolve 两个独立列判断 "店里是拦客人强还是处理投诉强"
Team Productivity(Intro_closed + Membership_closed + Cancel_saved + Complaint_resolved) / (Intro_total + Membership_total + Cancel_total + Complaint_total) × 100分母只算 revenue-impacting 四大类(不算 Call Volume)

Sanity checks (实施时验证):

  • Outbound + Inbound = Call Volume
  • closed ≤ total (每列)
  • 任何 closed > total = SQL bug
  • Cancel Save total 与 Complaint Resolve total 互不重叠(不同 subcategory,语义独立)

Backend 改动 — voicemail toggle 替代 filter 删除: apps/api/src/routes/v3/dashboard-multi-store.ts line 181 + 206 现有 call_state IS DISTINCT FROM 'voicemail' filter — 不删,改为受 includeVoicemail query param 控制:

  • API 接 ?includeVoicemail=true|false (默认 true)
  • includeVoicemail=true → 移除 filter (含 VM)
  • includeVoicemail=false → 保留 filter (排除 VM)

UI 顶部 toggle 默认 ON,Owner 切换时重查。这避免删 filter 后 Owner 第一次打开 dashboard 数字一夜 1.77x 暴涨的 baseline shift(verify 数据:Main 7d 含 VM 560 vs 不含 317,VM 占 43%)。


Tab 2 — Outcomes

Task outcomes — 数字格式 closed / total。Task = 一客一事件 (去重,一人多通电话只算 1 个 task)。Base = closed_at IN period(本期 close 了多少 task)— 对齐 Tab 1 start_time IN period 口径,与 HubSpot "Tasks Completed" pattern 一致。Carryover task(上期建本期 close)归入"本期 close 数",反映本期工作量,但 Volume 不再包含 pending backlog(backlog 警示放别处)。

Sample (illustrative numbers, not live data):

#StoreTask VolumeIntro Booking (c/t)Membership (c/t)Cancel Save % (s/t)Complaint Resolve % (r/t)Task ProductivityImpacted RevenueAI Summary
1Harlem18 ↑10%
closed this week
10 / 12 ↑8%4 / 6 —3 / 4 (75%) ↑5%1 / 2 (50%) —77% ↑3%$1,950 ↑5%Stable week, cancel saves on track
2Totowa15 ↑15%
closed this week
12 / 14 ↑13%5 / 7 ↑20%3 / 3 (100%) —2 / 3 (67%) ↑10%83% ↑8%$2,400 ↑18%All four metrics trending up
3Westside8 ↓20%
closed this week
2 / 4 ↓25%1 / 3 ↓15%1 / 6 (17%) ↓30%0 / 2 (0%) ↓100%31% ↓40%$450 ↓60%Urgent: cancel saves below avg
TOTAL95 ↑5%
closed this week
60 / 76 ↑8%23 / 36 ↑5%12 / 23 (52%) ↑6%9 / 18 (50%) ↑5%67% (avg)$12,600 ↑8%

镜像 Tab 1 结构:同 6 列 outcome / productivity,unit 从 call 换 task。Cancel Save % 和 Complaint Resolve % 是两个独立分母列,不混(对应 schema 的 cancellation_riskretention 两个独立 type_category)。AI Summary 是 LLM 生成的一句话,不是固定 emoji 状态。

Calculation Recipes (Tab 2)

源表 tasks (Neon) · Schema @retaintive/common/db/schema/tasks.ts (TASK_TYPE_CATEGORY 9 值,TASK_CLOSE_RESULT 18 值 — v0.54 起加 'booked' + 'cancelled') · UI label + category (positive 7 / neutral 6 / negative 3) @retaintive/common/db/schema/task-ui.ts CLOSE_RESULT_OPTIONS · Task 创建规则 callytics-infrastructure/lambda/contacts-analyzer/src/core/prompt-builder.ts

所有公式默认前缀 WHERE store_id = ? AND closed_at IN period AND status = 'closed',下表只列额外条件。Task 去重靠 schema 约束 (同 contact_phone + 同 type_category 同时只能 1 个 pending,见 prompt-builder.ts CONSTRAINTS)。

Base 决策(reviewer claim #5 verify 后):Tab 2 不再用 created_at IN period OR closed_at IN period 这种 "OR base"。原口径会把 carryover task 进 base(Main 7d:188 created + 25 closed-old = 213),Owner 看到 Tab 2 数字虚高于 Tab 1。改成 closed-only 后,Tab 1 (本期 calls)和 Tab 2 (本期 closed tasks)口径平行可比,且贴 HubSpot/Salesforce "Tasks Completed" pattern。Pending backlog 信号放页面顶部 alert banner(待 V2 spec),不混进 Volume。

公式 (在 base WHERE 上追加)含义 / 备注
Task Volume (主数字)COUNT(*) — 无追加 filter本期 close 的 task 数。pending backlog 不进 base
· Closed this period (subtext)同主数字显式标注 "closed this week / month / etc."
Intro Booking totalAND type_category = 'lead_follow_up'Lead funnel 内 task,按 prompt-builder.ts line 590
Intro Booking closedtotal + AND close_result IN ('converted', 'booked')converted = task objective achieved;booked = v0.54 显式 enum
Membership totalAND type_category = 'booked_not_converted'"已 booked / showed / trialed 但未签" 客人的拉回 task,按 line 591
Membership closedtotal + AND close_result = 'converted'booked_not_converted 语境下 = 转成 paying member。lead_follow_up + converted(那是 booked intro,不是签会员,已在 Intro Booking closed 算过)
Cancel Save totalAND type_category = 'cancellation_risk'客人主动要 cancel/freeze/downgrade(prompt-builder.ts line 594)
Cancel Save savedtotal + AND close_result = 'cancel_saved'把客人从"要走"边缘救回
Cancel Save % savedsaved / total × 100前端 compute;total = 0 显示
Complaint Resolve totalAND type_category = 'retention'客人没要 cancel,但有投诉 / billing 争议 / 服务质量问题(prompt-builder.ts line 595)
Complaint Resolve resolvedtotal + AND close_result = 'issue_resolved'处理掉投诉,客人继续留
Complaint Resolve %resolved / total × 100前端 compute;total = 0 显示
Task Productivity(Intro_closed + Membership_closed + Cancel_saved + Complaint_resolved) / (Intro_total + Membership_total + Cancel_total + Complaint_total) × 100跟 Tab 1 Team Productivity 同构
Impacted RevenueSUM(coefficient(close_result)) over status='closed' AND closed_at IN period见下方 V1 系数表;Tooltip 必须明示"估算,非 POS 入账"

Sanity checks:

  • closed ≤ total(每列)
  • Intro / Membership / Cancel Save / Complaint Resolve 四类 task 互不重叠(不同 type_category)
  • Impacted Revenue ≥ 0,与 Tab 2 closed/saved 总数 directionally 一致

两个 Productivity 联合诊断 (Tab 1 Team Prod + Tab 2 Task Prod 一起看):

Team ProdTask Prod诊断
接到机会就 close,但 lead 量不够 → marketing 问题
机会多但 close 不掉 → staff 执行问题
系统性问题(店关了 / 长期病)
一切健康

Impacted Revenue V1 系数表

SoT @retaintive/common/db/schema/task-ui.ts CLOSE_RESULT_OPTIONS (v0.54 起 18 值)。

close_resulttask-ui categoryV1 系数含义
convertedpositive$450新签 paying member / 真预约上
cancel_savedpositive$450从 cancel 边缘救回
win_backpositive$300Former member 重新加入
referral_obtainedpositive$300现有 member 介绍朋友
renewedpositive$200续费 / 复活冻结账号
upgradedpositive$200升级 tier
booked(v0.54 新增)$150Intro booked 但未签会员 (跟 converted 区别:可能 no-show)
issue_resolvedpositive$0 (V1)Service complaint resolved。V2 重评估
cancelled(v0.54 新增)$0Cancellation 已批准 / 取消单已发,确认流失。待决策: V2 是否给负系数反映流失成本
Neutral (6) + Negative (3)$0attempted / no_answer / left_voicemail / callback_later / already_member / other / not_interested / wrong_number / do_not_contact

V1 系数硬编码,V2 下放 store-level config。

AI Summary

不是 SQL 算的。callytics-infrastructure cross-call AI Lambda 每天凌晨扫这家店当天所有 call analysis,综合写一句 ≤ 80 字 plain-text summary 存到 store-level summary table。Dashboard 直接读 string 字段,inline 显示。

示例 narrative:

  • "Stable week, cancel saves on track"
  • "Intro booking +20% vs last week, momentum strong"
  • "Membership conversion below avg, review SA scripts"
  • "Urgent: 8/9 cancel calls unsaved, immediate intervention needed"

V1 不带视觉状态标签 (无 emoji 无 color tag) — 文字本身已传达情绪 (urgent / stable / strong)。V2 视产品需要再加 sentiment classification。

准确性靠 cross-call AI prompt 质量,与 dashboard 本身无关。Owner 反馈 "对不上" → issue 报到 callytics-infrastructure cross-call AI pipeline。


Interaction Rules

行为跳转
Store name/store-detail/:storeId?period=<current>&voicemail=<current> 裸跳单店 dashboard,带 time + VM 设置
KPI cell(任何数字 cell)/store-detail/:storeId?metric=<col>&period=<current>&voicemail=<current> 带 metric 信号跳店内 deep-dive。dashboard reshape 接到 metric 后 default scroll 到对应 detail block(详见 2026-05-24-dashboard-reshape.md "Deep-link from My Stores")
切 tab不重置 time range / VM toggle / sort
切 VM toggle立即重查 + 重渲染数字(Call Volume 受影响最直接;含 VM time ≈ 排除 VM time × 1.77 based on Main 7d verify)
Hover ⓘ显示 formula tooltip
AI Summary 列inline 显示,不 expand

Deep-link query param spec(发给 store-detail):

ParamValues含义
metriccalls | intro | membership | cancel | complaint | productivity | tasks | revenueOwner 在 my-stores 点的具体 cell。store-detail 接到后 default focus 对应 block(若该 metric 有对应 block)
periodtoday | 7d | 14d | 28d | this-week | last-week | this-month | last-month | ISO date range与 my-stores 当前 time picker 一致
voicemailinclude | exclude与 my-stores VM toggle 一致

Backend Implications

复用 apps/api/src/routes/v3/dashboard-multi-store.ts,扩展:

  • Period enumtoday / 7d / 14d / 28d / this-week + startDate / endDate ISO query params(custom 日历)
  • includeVoicemail query param 新增 — default true,控制 line 181 + 206 现有 call_state IS DISTINCT FROM 'voicemail' filter 是否应用。不删 filter,改成参数化(避免 VM 含/不含数字一夜 1.77x 跳变,Owner 失去 baseline)
  • Tab 2 response fields: totalTasks(closed in period), introBookingClosed/Total, membershipClosed/Total, cancelSavedSaved/Total, complaintResolvedResolved/Total, taskProductivity, impactedRevenue, aiSummary
  • Tab 1 response 重构 from flat fields 成 closed/total 对:introBookingClosed/Total, membershipCallClosed/Total, cancelSaveSaved/Total, complaintResolveResolved/Total, teamProductivity
  • Tab 2 SQL base 改 closed-only:AND closed_at IN period AND status = 'closed' (替代原 created_at OR closed_at)
  • 现有 tasksCreated / tasksClosed / tasksOverdue 字段 V1 保留但不展示

Store 列表沿用 rc_stores + store_members + store_config(activated_at IS NOT NULL)(line 139-150)。多租户隔离:store_id (见 apps/api/CLAUDE.md Store-Level 隔离)。


Files to Change

文件改动
apps/web/src/pages/overview/index.vueTab 切换 + Tab 2 表格
apps/web/src/pages/overview/components/PeriodTabs.vue扩展为 popup picker (8 个选项 + 日历)
apps/web/src/pages/overview/components/CallActivityTable.vue新建 — Tab 1 表格抽离
apps/web/src/pages/overview/components/OutcomesTable.vue新建 — Tab 2 表格
apps/web/src/composables/queries/useMultiStore.ts扩展 response type
apps/api/src/routes/v3/dashboard-multi-store.tsperiod enum / task outcome aggregate / Impacted Revenue / Productivity / AI Summary / 删 voicemail filter
apps/api/docs/api-technical-reference.md + apps/api/docs/frontend-api-guide.md文档同步(pre-commit hook 强制)
CHANGELOG.md (root + apps/web + apps/api)各加 entry

Pending Team Discussion

V1 按下面"当前选择"实施,不 block 开发。Team review 后如改方向,同步更新 Tab 1 Calculation Recipes 表格 + Defaults section。

Decision 2 — Complaint subcategory list 范围

Tab 1 Complaint Resolve total 公式当前列了 4 个 subcategory: member_support + billing_issue + cancellation_fee_dispute + complaint_feedback

Scarsdale 30d 真实分布:member_support 39(最大)+ billing_issue 16 + cancellation_fee_dispute 4 + complaint_feedback 1 = 60。

member_support 这 39 通里可能很多只是"客人问问题"(not actual complaint)。如果含 member_support,Complaint Resolve total = 60;不含 = 21。两个数字差 3 倍。

待 product 拍板 哪些 subcategory 算"complaint"。

  • member_support = 范围大,可能噪声
  • 不含 member_support = 范围窄,数据稀疏但语义干净

决策人: (待填) · 决议日期: (待填)

Decision 1 — Tab 1 Membership 用 2 值还是 4 值

选项包含 subcategoryLens利弊
2 值 (当前选择)membership_purchase_related + reactivation_purchase"卖一个新 membership"跟 Tab 2 booked_not_converted 同语义;upgrade/package 是 add-on,V2 可单开列
4 值上面 2 个 + member_upgrade + member_package"所有 membership-related 营收对话"observation tool 全展示;现有 member 升级 / 加包也算 "membership activity",但跟 Tab 2 不对称

背景 — Prompt + schema 把 4 个都列为 revenue_impacting,没规定列怎么 group。这是产品 lens 决策,不是技术决策。

决策人: (待填) · 决议日期: (待填)


Schema Source-of-Truth Chain

位置
L1 Drizzle schema@retaintive/common/src/db/schema/{tasks,calls}.ts (API ^1.0.0, web ^0.54.0 — 2026-05-24 bump)
L2 Enum constants见下方 sub-list
L3 Neon test live datarestless-boat-70724564 (org org-long-math-59935021),npx neonctl connection-string 拿 conn
L4 Existing routeapps/api/src/routes/v3/dashboard-multi-store.ts (Tab 1 SQL 已写,line 165-208)

L2 详情:

  • apps/api/src/constants/call-analysis.ts — call enums (CallCategory / CallSubcategory / OutcomeResult / CallState / CallDirection)
  • @retaintive/common/db/schema/tasks.tsTASK_TYPE_CATEGORY (9 值) + TASK_CLOSE_RESULT (18 值,v0.54 起加 'booked' + 'cancelled')
  • @retaintive/common/db/schema/task-ui.tsCLOSE_RESULT_OPTIONS (Impacted Revenue 系数表 SoT)