管理員端顧客卡券管理:查詢顧客卡券狀態與撤銷卡券
prospec 流程進度
跨 repo 進度
manage-customer-vouchers
Epic 1:顧客卡券管理 (Customer Voucher Management) — 管理頁面
Background
美髮系統的管理員經常需要協助顧客處理卡券相關問題(確認餘額、排除使用疑慮、處理退費、修正錯誤輸入的卡券)。目前管理端缺乏統一的卡券查詢與撤銷工具,導致客服效率低落且異動缺乏稽核軌跡。本變更新增「顧客卡券管理」管理頁面,涵蓋查詢與撤銷兩項核心能力。
User Stories
US-1: 查詢顧客卡券狀態 [P1]
As a 分店管理員, I want 透過顧客姓名、手機或卡券序號搜尋並查看顧客擁有的卡券清單, So that 我能協助顧客確認餘額或排除使用疑慮.
Acceptance Scenarios:
- WHEN 管理員於搜尋框輸入顧客姓名、手機或卡券序號,THEN 系統顯示符合條件且屬於該管理員所屬分店的卡券清單
- WHEN 卡券清單顯示,THEN 每筆需呈現顧客資訊、卡券類型(洗/剪/燙等)、剩餘次數或金額、購買日期、失效日期、卡券狀態
- WHEN 搜尋條件查無相符卡券,THEN 系統顯示「查無卡券」提示而非空白畫面
- WHEN 搜尋命中的卡券屬於其他分店,THEN 該卡券不出現於結果中
Independent Test: 以分店管理員身分登入,以已知顧客手機號搜尋,驗證回傳清單欄位完整、且僅含所屬分店資料。
US-2: 撤銷顧客卡券 [P1]
As a 具高權限角色的管理員(店長或總部管理員), I want 撤銷(作廢)特定顧客的卡券, So that 我能處理退費或修正錯誤輸入的卡券.
Acceptance Scenarios:
- WHEN 高權限管理員於卡券清單點選「撤銷」,THEN 系統彈出二次確認視窗並顯示該卡券資訊
- WHEN 管理員於確認視窗點選「確認撤銷」,THEN 該卡券標記為已作廢,不再出現於顧客端
- WHEN 卡券被撤銷,THEN 系統寫入一筆異動 log(含操作者、操作時間、卡券識別資訊、撤銷前狀態)供審核
- WHEN 不具高權限的管理員檢視卡券清單,THEN 「撤銷」操作不可見或不可點選
- WHEN 管理員於確認視窗點選「取消」,THEN 卡券狀態維持不變
Independent Test: 以店長身分撤銷一張測試卡券,驗證 ① 顧客端查不到 ② 管理端與 log 仍可查到該異動 ③ 一般管理員看不到撤銷按鈕。
Edge Cases
- 已失效/已用完的卡券:仍可被查詢並顯示(標示對應狀態)
- 跨分店資料:管理員以序號搜尋到非所屬分店的卡券時,不顯示於結果
- 重複撤銷:對已撤銷的卡券再次點選撤銷時,系統需阻止並提示「已撤銷」
- 空白搜尋:搜尋條件為空時不執行查詢,提示需輸入條件
- 同名顧客:多位顧客同名時,清單以手機等顧客資訊區辨
Functional Requirements
- FR-001: 提供卡券搜尋功能,支援顧客姓名、手機號碼、卡券序號三種查詢條件
- FR-002: 卡券清單每筆需顯示顧客資訊、卡券類型、剩餘次數/金額、購買日期、失效日期、卡券狀態
- FR-003: 卡券查詢結果需限制於操作管理員所屬分店範圍
- FR-004: 所有管理員角色皆可執行卡券查詢
- FR-005: 卡券撤銷採作廢(soft delete)方式 — 撤銷後於顧客端隱藏,資料仍保留於後端
- FR-006: 卡券撤銷限定高權限角色(店長、總部管理員)方可執行
- FR-007: 撤銷操作需經二次確認視窗
- FR-008: 每次撤銷需寫入可稽核的異動 log(操作者、時間、卡券識別資訊、撤銷前狀態)
- FR-009: 退費金流處理不在本變更範圍內(明確排除)
Success Criteria
- SC-001: 管理員以姓名、手機、序號三種條件皆能成功取得正確的卡券清單
- SC-002: 撤銷後的卡券 100% 不再出現於顧客端,且 100% 保留於後端可查
- SC-003: 每筆撤銷操作皆產生對應的可稽核 log,log 完整率 100%
- SC-004: 非高權限管理員無法執行撤銷(權限阻擋 100% 有效)
Related Modules
尚無模組可比對 — prospec/ai-knowledge/_index.md 模組索引目前為空,/prospec-knowledge-generate 尚未執行。模組關聯待知識庫建立後或於 /prospec-plan 階段補充。
Open Questions
- NEEDS CLARIFICATION: 已失效/已用完的卡券是否允許撤銷?「修正錯誤輸入」情境可能需要,但對已失效卡券撤銷的業務意義待確認。
- NEEDS CLARIFICATION: 異動 log 是否需提供管理端查詢介面?本 Story 僅要求「寫入」log,未定義檢視方式。
- NEEDS CLARIFICATION: 卡券類型(洗/剪/燙)是否為固定列舉,或可由系統設定擴充?
- WARN: Related Modules 無法自動比對 —
_index.md模組索引為空。
Constitution Check
- Reviewed against
prospec/CONSTITUTION.md - Violations noted:
CONSTITUTION.md目前為未填寫的空白模板,無實質原則可比對。建議於/prospec-plan前先補齊專案原則(可用/speckit.constitution或手動填寫)。
UI Scope
Scope: full
Design Spec: Manage Customer Vouchers
Generated from: proposal.md (ui_scope: full) Platform: figma Last updated: 2026-05-17
Overview
An admin (management-side) page that lets branch administrators search a customer's vouchers and lets high-privilege roles revoke (void) a voucher with a confirmation step. Covers proposal stories US-1 (search) and US-2 (revoke).
This design reuses the existing M+ hair-salon design system — the shared "Design Tokens" variable collection (warm gold/cream identity) already used by the M+ Customer and Designer portals — so the admin surface is visually consistent with the rest of the product.
Visual Identity
Color Palette
| Token | Value | Usage |
|---|---|---|
| --color-primary | #ca8a04 | Primary actions, active states, links |
| --color-primary-hover | #a16207 | Primary hover |
| --color-primary-muted | #ca8a0415 | Subtle tinted backgrounds, pills |
| --color-primary-border | #ca8a0433 | Card / input borders |
| --color-surface | #ffffffe6 | Card backgrounds |
| --color-surface-solid | #ffffff | Inputs, table surface |
| --color-surface-muted | #fafaf9 | Table header / zebra rows |
| --color-background | #f5f5f4 | Page background |
| --color-text | #0c0a09 | Primary text |
| --color-text-secondary | #44403c | Secondary text |
| --color-text-muted | #78716c | Labels, captions |
| --color-text-disabled | #a8a29e | Disabled / revoked rows |
| --color-border | #e7e5e4 | Table dividers |
| --color-success | #16a34a / bg #dcfce7 | Status: 有效 (active) |
| --color-error | #dc2626 / bg #fee2e2 | Destructive action, status: 已撤銷 |
| --color-disabled-bg | #f3f4f6 | Status: 已失效 / 已用完 |
Typography
Family: Noto Sans TC (CJK-capable). Latin/numeric also rendered with Noto Sans TC.
| Token | Size | Weight | Usage |
|---|---|---|---|
| --font-heading-lg | 28px | 700 | Page title |
| --font-heading | 20px | 700 | Card / dialog titles |
| --font-title | 16px | 500 | Table column headers, row primary text |
| --font-body | 14px | 400 | Table cell text, buttons |
| --font-small | 13px | 400 | Secondary cell text |
| --font-caption | 12px | 400 | Captions, helper text |
Spacing Scale
| Token | Value | Usage |
|---|---|---|
| --space-xs | 4px | Inline gaps |
| --space-sm | 8px | Compact padding, pill padding |
| --space-md | 16px | Default padding, cell padding |
| --space-lg | 24px | Card padding, section spacing |
Visual Style
- Border radius:
22px(cards/dialog),12px(inputs/buttons-rect),999px(pill badges) - Shadow:
0 25px 50px #ca8a042e(card),0 16px 40px #00000026(dialog) - Transition:
150ms ease-in-out - Page background: warm radial gradient (
#fff8e1 → #fafaf9 → #f8f5f0)
Components
TopBar
Layout: flex, horizontal, space-between, height 64px, full width, padding 0 32px
States: single static state.
Design Tokens: background --color-surface, bottom border --color-primary-border.
Children:
- Brand: M+ logo mark + "M+ 管理後台" label
- Admin meta: admin display name + role badge + logout button
PageHeader
Layout: flex, vertical, gap 8px Children:
- Title "顧客卡券管理" (
--font-heading-lg,--color-text) - Description "搜尋並管理顧客卡券,協助確認餘額或撤銷錯誤卡券。" (
--font-body,--color-text-muted)
SearchBar
Layout: card, flex horizontal, gap 12px, align center, padding --space-lg
States:
| State | Visual Changes |
|---|---|
| Default | Empty input, placeholder visible |
| Focused | Input border --color-primary |
| Disabled (search btn) | Empty input → search button --color-disabled-bg |
Children:
- Search input: leading search icon + text field, placeholder "輸入顧客姓名、手機號碼或卡券序號"
- Search button: primary, label "搜尋"
- Helper text (empty submit): "請輸入搜尋條件" (
--color-error)
VoucherTable
Layout: card; vertical; header row + data rows; columns separated by --space-md
Columns: 顧客 | 卡券類型 | 剩餘 | 購買日期 | 失效日期 | 狀態 | 操作
States:
| State | Visual Changes |
|---|---|
| Default row | Surface background, --color-text text |
| Revoked row | --color-text-disabled text, no revoke action |
| Hover row | --color-surface-muted background |
Design Tokens: header background --color-surface-muted, row divider --color-border.
Children: rows of [CustomerCell, TypeCell, RemainingCell, DateCell ×2, StatusBadge, RevokeButton]
StatusBadge
Layout: pill, padding --space-sm --space-xs, radius 999px
States (variants):
| Variant | Background | Text | Meaning |
|---|---|---|---|
| active | --color-success-bg |
--color-success |
有效 |
| used-up | --color-disabled-bg |
--color-text-disabled |
已用完 |
| expired | --color-disabled-bg |
--color-text-disabled |
已失效 |
| revoked | --color-error-bg |
--color-error |
已撤銷 |
RevokeButton
Layout: text/ghost button, --font-body
States:
| State | Visual Changes |
|---|---|
| Default (high-priv) | --color-error label "撤銷" |
| Hover | underline |
| Hidden (normal admin) | Not rendered — only high-privilege roles see it |
| Disabled (already revoked) | Not rendered for revoked rows |
EmptyState
Layout: flex vertical, center aligned, gap --space-md, padding 48px
Children: package icon (64px circle, --color-surface-muted), title "查無卡券",
description "找不到符合搜尋條件的卡券,請確認姓名、手機或序號是否正確。"
RevokeConfirmDialog
Layout: modal — full-screen overlay (--color-overlay) + centered dialog card
(radius 22px, dialog shadow, max-width 440px, padding --space-lg)
States:
| State | Visual Changes |
|---|---|
| Visible | Overlay + dialog shown |
| Submitting | Confirm button spinner, both buttons disabled |
| Already-revoked block | Replaces actions with notice "此卡券已撤銷" |
Children:
- Title "撤銷卡券確認"
- Voucher summary block: 顧客 / 卡券類型 / 卡券序號 / 剩餘 / 購買日期
- Warning text "撤銷後此卡券將於顧客端隱藏且無法復原,系統會記錄本次操作。"
- Actions: 取消 (secondary) + 確認撤銷 (destructive,
--color-errorfill)
Responsive Strategy
This is a desktop-first admin tool. Primary target is desktop (≥1024px).
Breakpoints
| Name | Min Width | Layout |
|---|---|---|
| Tablet | 768px | Content full-width with 24px gutter; table scrolls horizontally |
| Desktop | 1024px | Centered content container, max-width 1200px |
Layout Adaptations
| Component | Tablet | Desktop |
|---|---|---|
| SearchBar | Input + button stack-friendly, still inline | Inline, input grows |
| VoucherTable | Horizontal scroll inside card | All columns visible |
| RevokeConfirmDialog | 90% viewport width | Fixed 440px width |
Navigation
- Desktop: persistent top bar; admin sections reachable from top bar (out of scope here).
- The page itself is a single-view destination — no in-page navigation.
Interaction Spec: Manage Customer Vouchers
Generated from: proposal.md + design-spec.md DSL Version: draft-1 Last updated: 2026-05-17
Screens
Screen: VoucherManagement
States:
| State | Description | Entry Condition |
|---|---|---|
| Idle | Search bar shown, no query run yet | Initial visit |
| InvalidSubmit | Empty search submitted | Search clicked with blank input |
| Loading | Fetching voucher results | Valid search submitted |
| Results | Voucher table rendered with rows | Fetch success, ≥1 match |
| NoResults | Empty state shown | Fetch success, 0 matches |
| Error | Fetch failed notice | Fetch error |
Transitions:
Idle -> InvalidSubmit : search click with empty input
InvalidSubmit -> Loading : search click with non-empty input
Idle -> Loading : search click with non-empty input
Loading -> Results : fetch success (matches > 0)
Loading -> NoResults : fetch success (matches == 0)
Loading -> Error : fetch error
Error -> Loading : retry click
Results -> Loading : new search submitted
Branch scoping (FR-003) is applied server-side: results only ever contain vouchers of the administrator's own branch. Cross-branch matches never enter the Results state.
Component: RevokeConfirmDialog
States:
| State | Description | Entry Condition |
|---|---|---|
| Hidden | Not shown | Default |
| Visible | Overlay + dialog with voucher summary | Revoke clicked on an active row |
| Submitting | Confirm pressed, awaiting result | Confirm click |
| AlreadyRevoked | Notice "此卡券已撤銷", revoke blocked | Revoke attempted on an already-revoked voucher |
Transitions:
Hidden -> Visible : revoke click (row not revoked)
Hidden -> AlreadyRevoked : revoke click (row already revoked)
Visible -> Submitting : confirm revoke click
Visible -> Hidden : cancel click / overlay click
Submitting -> Hidden : revoke success (row updates to 已撤銷)
Submitting -> Visible : revoke error (inline error, re-enable)
AlreadyRevoked -> Hidden : dismiss click
Flows
Flow: Search a customer's vouchers (US-1)
Description: An administrator looks up a customer's vouchers to confirm balance.
Steps:
1. User types a name / phone / voucher serial into the search input
-> Search button becomes enabled
2. User clicks 搜尋
-> If input empty: stay on screen, show helper "請輸入搜尋條件"
-> If valid: show Loading state
3. System fetches branch-scoped voucher results
-> On success with matches: render VoucherTable (customer, type,
remaining, purchase date, expiry date, status)
-> On success with no match: render EmptyState "查無卡券"
-> On error: show Error notice with retry
Flow: Revoke a voucher (US-2)
Description: A high-privilege administrator voids an incorrect voucher.
Steps:
1. (Precondition) Revoke action is visible only to high-privilege roles
(store manager / HQ admin). Normal admins never see the 撤銷 button.
2. User clicks 撤銷 on an active voucher row
-> Open RevokeConfirmDialog (Visible) showing the voucher summary
-> If the voucher is already revoked: open AlreadyRevoked state instead
3. User reviews voucher info + warning text
-> Click 取消: close dialog, voucher unchanged
-> Click 確認撤銷: dialog enters Submitting (confirm spinner)
4. System soft-deletes the voucher and writes an audit log
(operator, timestamp, voucher identity, prior status)
-> On success: close dialog, row status updates to 已撤銷 (greyed, no action),
voucher is hidden on the customer side
-> On error: stay in dialog, show inline error, re-enable buttons
Gestures & Micro-interactions
| Trigger | Animation | Duration |
|---|---|---|
| Button press | Background color shift to hover/active | 150ms |
| Dialog appear | Overlay fade in + dialog scale 0.96 → 1.0 | 150ms |
| Row status change to 已撤銷 | Row text fades to disabled color | 200ms |
| Search submit | Search button shows inline spinner | — |
Responsive Interactions
| Interaction | Tablet | Desktop |
|---|---|---|
| Browse table columns | Horizontal scroll within table card | All columns visible at once |
| Revoke action | Tap 撤銷 → dialog at 90% width | Click 撤銷 → fixed 440px dialog |
| Search | Inline input + button | Inline input + button |