Appearance
UI Spec — Nâng cấp Voucher Management
Version: 1.0 Ngày: 2026-03-16 Tham chiếu:
docs/features/voucher-enhancement/prd.md
B1. Screen Map
/cms/voucher-management/
├── voucher/ # Hiện có
│ ├── /create # Hiện có — thêm field min_activation_hours (P1)
│ │ # thêm step Đối tác (P3)
│ ├── /:id # Hiện có
│ │ ├── Tab: Tổng quan # Hiện có
│ │ ├── Tab: Báo cáo # Hiện có
│ │ ├── Tab: Thống kê NV # MỚI (P2) — FR-P2-01, FR-P2-02, FR-P2-03
│ │ ├── Tab: Chu kỳ sử dụng # MỚI (P4) — FR-P4-01 đến FR-P4-05
│ │ ├── Tab: Danh sách KH # Hiện có
│ │ └── Tab: Lịch sử & Audit # Hiện có
│ └── /:id/edit # Hiện có — thêm field min_activation_hours (P1)
│ # thêm step Đối tác (P3)
├── voucher-campaign/ # Hiện có (không đổi)
├── override-dashboard/ # MỚI (P1) — FR-P1-04
│
└── [Settings module]
└── Voucher Settings # MỚI (P1) — FR-P1-05, FR-P1-06
[Màn hình tạo ĐH — module ecommerce]
└── Dialog chặn voucher # MỚI (P1) — FR-P1-02, FR-P1-03B2. Component Inventory
Phase 1 — Components mới/sửa
| Component | Loại | File | Mô tả |
|---|---|---|---|
MinActivationHoursField | Sửa inline | VoucherConfigForm.tsx | Input number trong form tạo chiến dịch |
VoucherTimeBlockDialog | Mới | VoucherTimeBlockDialog.tsx | Dialog chặn + nút "Yêu cầu duyệt" |
VoucherOverrideDashboard | Mới | VoucherOverrideDashboard.tsx | Danh sách override + filter |
VoucherSettingsSection | Mới | Settings module | Config mặc định thời gian chờ + hạn sử dụng |
Phase 2 — Components mới
| Component | Loại | File | Mô tả |
|---|---|---|---|
VoucherStaffStatisticsTab | Mới | tabs/VoucherStaffStatisticsTab.tsx | Tab thống kê NV + drill-down |
StaffDrillDownDialog | Mới | StaffDrillDownDialog.tsx | Chi tiết voucher per NV |
Phase 3 — Components mới
| Component | Loại | File | Mô tả |
|---|---|---|---|
AffiliateAssignStep | Mới | create-components/AffiliateAssignStep.tsx | Wizard step chọn đối tác + quota |
AffiliateSourceBadge | Mới | Order creation module | Badge "Voucher do KOL X phát" |
Phase 4 — Components mới
| Component | Loại | File | Mô tả |
|---|---|---|---|
VoucherUsageCycleTab | Mới | tabs/VoucherUsageCycleTab.tsx | KPI cards + bar chart + bảng |
CycleBucketChart | Mới | CycleBucketChart.tsx | Horizontal bar chart (Chart.js) |
B3. User Flows
Flow 1: Tạo chiến dịch với min_activation_hours (P1)
Admin mở /create
→ Step 1: Chọn loại voucher
→ Step 2: Cấu hình (THÊM field "Thời gian chờ tối thiểu")
└── Input: min_activation_hours (mặc định 24, range 0-720)
└── Hint: "Voucher phải chờ X giờ sau khi kích hoạt mới được dùng trong ĐH"
→ Step 3: Quota per channel
→ Step 4: Review
→ Step 5: Chọn Manager
→ Step 6: Chọn Đối tác (MỚI — P3, chỉ hiển thị nếu bật)
→ SubmitFlow 2: Tạo ĐH bị chặn bởi voucher time control (P1)
NV tạo ĐH → Chọn voucher → Submit
→ Backend check: activated_at + min_activation_hours > NOW()
→ CHẶN → Hiện VoucherTimeBlockDialog:
┌────────────────────────────────────────────┐
│ ⚠ Voucher chưa đủ thời gian sử dụng │
│ │
│ Mã voucher: CDLF42L4SB │
│ Kích hoạt: 15/03/2026 14:00 │
│ Thời gian chờ: 24 giờ │
│ Còn lại: 22 giờ 15 phút │
│ │
│ Voucher có thể sử dụng từ: │
│ 16/03/2026 14:00 │
│ │
│ [Yêu cầu Manager duyệt] [Đóng] │
└────────────────────────────────────────────┘
→ NV click "Yêu cầu Manager duyệt"
┌────────────────────────────────────────────┐
│ Yêu cầu duyệt sử dụng voucher sớm │
│ │
│ Lý do: [Khách VIP ▼] │
│ │
│ Ghi chú: [________________________] │
│ [________________________] │
│ (Bắt buộc) │
│ │
│ [Xác nhận duyệt] [Hủy] │
└────────────────────────────────────────────┘
(Chỉ hiện nút "Xác nhận duyệt" nếu user là Manager/Admin)
→ Manager approve → ĐH được tạo thành côngFlow 3: Xem thống kê nhân viên (P2)
Admin mở /voucher/:id → Tab "Thống kê NV"
→ Hiện filter bar (ngày, chi nhánh, loại NV/đối tác)
→ Hiện bảng thống kê
→ Click 1 hàng → Mở StaffDrillDownDialog
→ Click "Xuất Excel" → Download fileFlow 4: Xem chu kỳ sử dụng (P4)
Admin mở /voucher/:id → Tab "Chu kỳ sử dụng"
→ Hiện filter bar (ngày, chi nhánh, nhóm theo)
→ Hiện 4 KPI cards
→ Hiện horizontal bar chart (bucket distribution)
→ Hiện bảng chi tiết (breakdown theo dimension)
→ Chọn 2+ chiến dịch → chart chồng so sánhB4. Notification Spec
Notifications hiện có (không đổi)
| Event | Trigger Code | Kênh | Người nhận |
|---|---|---|---|
| Voucher kích hoạt (Print) | noti_voucher_activated_print | In-app | Customer |
| Voucher kích hoạt (Offline) | noti_voucher_activated_offline | In-app | Customer |
| Voucher claimed (Online) | noti_voucher_claimed_online | In-app | Customer |
Notifications mới
| Event | Trigger Code | Kênh | Người nhận | Template | Dedupe Rule | Phase |
|---|---|---|---|---|---|---|
| Override được tạo | noti_voucher_override_created | In-app | BOD, ITLeader | "Voucher {voucher_code} được duyệt sử dụng sớm bởi {approved_by_name} tại {branch_name}. Lý do: {reason_note}" | 1 lần / override | P1 |
| Voucher manual sắp hết hạn (3 ngày) | noti_voucher_manual_expiring | In-app + Push | Customer | "Voucher {voucher_name} ({voucher_code}) sẽ hết hạn vào {expired_at}. Hãy sử dụng trước khi quá hạn!" | 1 lần / voucher | P1 |
| Đối tác gần hết quota (>90%) | noti_affiliate_quota_warning | In-app | Admin, Campaign Managers | "Đối tác {affiliate_name} đã phát {distributed_count}/{quota} voucher chiến dịch {campaign_name}" | 1 lần / đối tác / chiến dịch | P3 |
Template Variables
| Variable | Mô tả | VD |
|---|---|---|
{voucher_code} | Mã voucher | CDLF42L4SB |
{voucher_name} | Tên voucher | 01 Suất chăm sóc da trắng sáng |
{expired_at} | Ngày hết hạn (dd/MM/yyyy) | 15/04/2026 |
{approved_by_name} | Tên Manager duyệt | Nguyễn Văn A |
{branch_name} | Tên chi nhánh | Chi nhánh Q.1 HCM |
{reason_note} | Ghi chú lý do override | Khách VIP đặc biệt |
{affiliate_name} | Tên đối tác | KOL Hương |
{distributed_count} | Số đã phát | 185 |
{quota} | Quota tối đa | 200 |
{campaign_name} | Tên chiến dịch | Tết Kim Cương 2026 |
B5. Permission Matrix
Màn hình hiện có (không đổi)
| Màn hình / Hành động | ITLeader | ITStaff | BOD | AccLeader | AccStaff | CSLeader | CSStaff | HRLeader | HRStaff | TSLeader | TSStaff |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Xem danh sách voucher | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tạo/Sửa/Duyệt campaign | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Tab Báo cáo / Lịch sử | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ |
Màn hình mới — Phase 1
| Màn hình / Hành động | ITLeader | ITStaff | BOD | AccLeader | Manager CN | Staff |
|---|---|---|---|---|---|---|
| Config min_activation_hours (tạo campaign) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Dialog chặn: Xem thông tin chặn | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dialog chặn: Nút "Yêu cầu duyệt" | ✅ | ❌ | ✅ | ❌ | ✅ (Branch) | ❌ |
| Override Dashboard: Xem danh sách | ✅ | ✅ | ✅ | ✅ | ✅ (Branch) | ❌ |
| Override Dashboard: Export | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Settings: Config thời gian chờ mặc định | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Settings: Config hạn sử dụng manual | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
Branch scoping: Manager CN chỉ thấy override tại chi nhánh mình quản lý (
branch_id = X-Hasura-Branch-Id)
Màn hình mới — Phase 2
| Màn hình / Hành động | ITLeader | ITStaff | BOD | AccLeader | Manager CN | Staff |
|---|---|---|---|---|---|---|
| Tab Thống kê NV: Xem bảng | ✅ All | ✅ All | ✅ All | ✅ All | ✅ Branch | ❌ |
| Tab Thống kê NV: Drill-down | ✅ | ✅ | ✅ | ✅ | ✅ (Branch) | ❌ |
| Tab Thống kê NV: Export Excel | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Partner App: Xem thống kê cá nhân | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ (Self) |
Self scoping: Staff trên Partner app chỉ thấy data của chính mình (
staff_id = X-Hasura-User-Id)
Màn hình mới — Phase 3
| Màn hình / Hành động | ITLeader | ITStaff | BOD | AccLeader | Manager CN | Affiliate |
|---|---|---|---|---|---|---|
| Wizard step Đối tác: Gán đối tác | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Wizard step Đối tác: Set quota | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Tạo ĐH: Xem badge "Do đối tác X phát" | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| Report: Filter theo đối tác | ✅ | ✅ | ✅ | ✅ | ✅ (Branch) | ❌ |
Màn hình mới — Phase 4
| Màn hình / Hành động | ITLeader | ITStaff | BOD | AccLeader | Manager CN | Staff |
|---|---|---|---|---|---|---|
| Tab Chu kỳ: Xem KPI + chart | ✅ | ✅ | ✅ | ✅ | ✅ (Branch) | ❌ |
| Tab Chu kỳ: So sánh chiến dịch | ✅ | ✅ | ✅ | ✅ | ✅ (Branch) | ❌ |
| Tab Chu kỳ: Export Excel | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
B6. State Matrix
Mỗi màn hình mới × 5 trạng thái
| Màn hình | Loading | Empty | Error | No Permission | Partial |
|---|---|---|---|---|---|
| Override Dashboard | Skeleton 5 rows | "Chưa có override nào" + icon trống | Toast "Lỗi tải dữ liệu" + nút Thử lại | Redirect về /voucher + toast "Bạn không có quyền" | Filter trả 0 kết quả → "Không tìm thấy override phù hợp" |
| Tab Thống kê NV | Skeleton 5 rows + KPI cards placeholder | "Chưa có dữ liệu thống kê. Voucher cần được phát trước khi có thống kê" | Toast "Lỗi tải thống kê" + nút Thử lại | Tab ẩn (không hiện trong tab list) | Filter trả 0 → "Không có NV phát voucher trong khoảng thời gian này" |
| Staff Drill-down | Spinner trong dialog | "NV chưa phát voucher nào" | Toast trong dialog + nút Thử lại | N/A (kế thừa từ tab cha) | Voucher chưa có ĐH → cột DT hiện "—" |
| Tab Chu kỳ | Skeleton cards + chart placeholder | "Chưa có dữ liệu chu kỳ. Cần ít nhất 1 voucher đã sử dụng" | Toast "Lỗi tải dữ liệu" + nút Thử lại | Tab ẩn | Tất cả voucher đang pending → chart hiện 100% "Chưa dùng", KPI hiện "—" |
| Wizard step Đối tác | Spinner loading danh sách đối tác | "Chưa có đối tác nào trong hệ thống. Vui lòng tạo đối tác trong module Affiliate trước" | Toast "Lỗi tải danh sách đối tác" | Step ẩn (chỉ hiện cho Full Access roles) | Một số đối tác bị deactivated → hiện nhưng disabled + tooltip "Đối tác đã ngừng hoạt động" |
| Dialog chặn voucher | N/A (data có sẵn từ response lỗi) | N/A | N/A | Nút "Yêu cầu duyệt" ẩn nếu không phải Manager/Admin | N/A |
| Voucher Settings | Spinner loading current settings | Hiện giá trị mặc định (24h, 30 ngày) | Toast "Lỗi tải cấu hình" | Redirect + toast "Bạn không có quyền" | N/A |
B7. Copy Text Dictionary
Phase 1 — Time Control
| Key | Vietnamese | Context |
|---|---|---|
voucher.config.min_activation_hours.label | Thời gian chờ tối thiểu (giờ) | Label input trong form tạo campaign |
voucher.config.min_activation_hours.hint | Số giờ tối thiểu từ khi kích hoạt đến khi voucher được dùng trong đơn hàng. 0 = không giới hạn | Hint text |
voucher.time_block.title | Voucher chưa đủ thời gian sử dụng | Title dialog chặn |
voucher.time_block.remaining | Còn lại: {hours} giờ {minutes} phút | Thời gian còn lại |
voucher.time_block.available_from | Voucher có thể sử dụng từ: | Thời điểm cho phép |
voucher.time_block.request_override | Yêu cầu Manager duyệt | Nút CTA |
voucher.override.title | Yêu cầu duyệt sử dụng voucher sớm | Title dialog override |
voucher.override.reason.label | Lý do | Label dropdown |
voucher.override.reason.vip_customer | Khách VIP | Option |
voucher.override.reason.special_event | Sự kiện đặc biệt | Option |
voucher.override.reason.manager_directive | Chỉ đạo cấp trên | Option |
voucher.override.reason.system_error | Lỗi hệ thống | Option |
voucher.override.reason.other | Khác | Option |
voucher.override.note.label | Ghi chú | Label textarea |
voucher.override.note.placeholder | Nhập lý do chi tiết (bắt buộc) | Placeholder |
voucher.override.confirm | Xác nhận duyệt | Nút confirm |
voucher.override.success | Override đã được duyệt | Toast success |
voucher.settings.min_hours.label | Thời gian chờ mặc định (giờ) | Settings label |
voucher.settings.min_hours.hint | Áp dụng cho voucher manual không thuộc chiến dịch | Settings hint |
voucher.settings.expiry_days.label | Hạn sử dụng voucher manual (ngày) | Settings label |
voucher.settings.expiry_days.hint | Số ngày kể từ khi tặng. 0 = không giới hạn | Settings hint |
Phase 2 — Thống kê NV
| Key | Vietnamese | Context |
|---|---|---|
voucher.staff_stats.tab | Thống kê NV | Tab label |
voucher.staff_stats.title | Thống kê phát voucher theo nhân viên | Page title |
voucher.staff_stats.filter.distributor_type | Loại người phát | Filter label |
voucher.staff_stats.filter.all | Tất cả | Option |
voucher.staff_stats.filter.internal | Nhân viên | Option |
voucher.staff_stats.filter.affiliate | Đối tác | Option |
voucher.staff_stats.col.staff_name | Nhân viên | Column header |
voucher.staff_stats.col.branch | Chi nhánh | Column header |
voucher.staff_stats.col.distributed | Đã phát | Column header |
voucher.staff_stats.col.redeemed | Đã dùng | Column header |
voucher.staff_stats.col.rate | Tỉ lệ % | Column header |
voucher.staff_stats.col.revenue | Doanh thu | Column header |
voucher.staff_stats.col.avg_days | TB ngày dùng | Column header |
voucher.staff_stats.empty | Chưa có dữ liệu thống kê | Empty state |
voucher.staff_stats.export | Xuất Excel | Button |
Phase 3 — Đối tác
| Key | Vietnamese | Context |
|---|---|---|
voucher.affiliate.step_title | Đối tác phát voucher | Wizard step title |
voucher.affiliate.enable | Cho phép đối tác phát voucher chiến dịch này | Checkbox label |
voucher.affiliate.select | Chọn đối tác | Dropdown label |
voucher.affiliate.quota | Quota | Column header |
voucher.affiliate.unlimited | Không giới hạn | Khi quota = NULL |
voucher.affiliate.source_badge | Voucher do {affiliate_name} phát | Badge trên tạo ĐH |
voucher.affiliate.quota_exceeded | Đối tác đã hết quota cho chiến dịch này | Error message |
Phase 4 — Chu kỳ
| Key | Vietnamese | Context |
|---|---|---|
voucher.cycle.tab | Chu kỳ sử dụng | Tab label |
voucher.cycle.title | Báo cáo chu kỳ sử dụng voucher | Page title |
voucher.cycle.avg | Trung bình | KPI label |
voucher.cycle.min | Nhanh nhất | KPI label |
voucher.cycle.median | Trung vị | KPI label |
voucher.cycle.rate | Tỉ lệ dùng | KPI label |
voucher.cycle.unit | ngày | Unit suffix |
voucher.cycle.bucket.0_3 | 0-3 ngày | Bucket label |
voucher.cycle.bucket.4_7 | 4-7 ngày | Bucket label |
voucher.cycle.bucket.8_14 | 8-14 ngày | Bucket label |
voucher.cycle.bucket.15_30 | 15-30 ngày | Bucket label |
voucher.cycle.bucket.31_60 | 31-60 ngày | Bucket label |
voucher.cycle.bucket.60_plus | 60+ ngày | Bucket label |
voucher.cycle.bucket.pending | Chưa dùng | Bucket label |
voucher.cycle.group_by.label | Nhóm theo | Filter label |
voucher.cycle.group_by.overall | Tổng quan | Option |
voucher.cycle.group_by.campaign | Chiến dịch | Option |
voucher.cycle.group_by.branch | Chi nhánh | Option |
voucher.cycle.group_by.staff | Nhân viên | Option |
B-Desktop. ASCII Wireframes
WF-01: Field min_activation_hours trong VoucherConfigForm (P1)
┌─────────────────────────────────────────────────────────────────┐
│ Cấu hình chiến dịch │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Tên chiến dịch: [Tết Kim Cương 2026________________] │
│ │
│ Thời gian chạy: [01/01/2026] → [28/02/2026] │
│ │
│ Hạn sử dụng voucher (ngày): [30_____] │
│ Hint: Số ngày voucher có hiệu lực kể từ khi kích hoạt │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ⚡ Kiểm soát thời gian │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ Thời gian chờ tối thiểu (giờ): [24______] │ │
│ │ Hint: Voucher phải chờ 24 giờ sau khi kích hoạt mới │ │
│ │ được dùng trong đơn hàng. 0 = không giới hạn │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Giá trị giảm: [500.000đ________] │
│ ... │
│ │
│ [Quay lại] [Tiếp theo →] │
└─────────────────────────────────────────────────────────────────┘WF-02: Override Dashboard (P1)
┌─────────────────────────────────────────────────────────────────────────┐
│ Giám sát Override Voucher │
├─────────────────────────────────────────────────────────────────────────┤
│ [Từ ngày: 01/03/2026 ▼] [Đến ngày: 16/03/2026 ▼] │
│ [Chi nhánh: Tất cả ▼] [Lý do: Tất cả ▼] │
│ [Xuất Excel] │
│ │
│ Tổng: 12 override | Nhiều nhất: Khách VIP (7) | CN Q.1 HCM (5) │
├────┬───────────┬──────────┬─────────────┬──────────┬──────────────────┤
│ # │ Người │ CN │ Mã Voucher │ Lý do │ Ghi chú │
│ │ duyệt │ │ │ │ │
├────┼───────────┼──────────┼─────────────┼──────────┼──────────────────┤
│ 1 │ Trần B │ Q.1 HCM │ CDLF42L4SB │ Khách │ KH thân thiết │
│ │ 15/03 │ │ │ VIP │ 5 năm │
│ │ 10:30 │ │ │ │ │
├────┼───────────┼──────────┼─────────────┼──────────┼──────────────────┤
│ 2 │ Nguyễn A │ Q.3 HCM │ CD4YCJ2VC4 │ Sự kiện │ Event khai │
│ │ 14/03 │ │ │ đặc biệt │ trương CN mới │
│ │ 16:45 │ │ │ │ │
├────┼───────────┼──────────┼─────────────┼──────────┼──────────────────┤
│ 3 │ Lê C │ Đà Nẵng │ CDXHYA5PAD │ Chỉ đạo │ BGĐ yêu cầu │
│ │ 13/03 │ │ │ cấp trên │ duyệt │
│ │ 09:15 │ │ │ │ │
├────┴───────────┴──────────┴─────────────┴──────────┴──────────────────┤
│ ← 1 / 1 → (20 hàng/trang) │
└───────────────────────────────────────────────────────────────────────┘WF-03: Tab Thống kê NV (P2)
┌─────────────────────────────────────────────────────────────────────────┐
│ Thống kê phát voucher theo nhân viên │
├─────────────────────────────────────────────────────────────────────────┤
│ [Từ ngày ▼] [Đến ngày ▼] [Chi nhánh ▼] [Loại: Tất cả ▼] │
│ [Xuất Excel] │
├────┬───────────┬──────────┬────────┬────────┬─────────┬───────┬───────┤
│ # │ NV │ CN │ Loại │ Đã phát│ Đã dùng │ Tỉ lệ│ DT │
├────┼───────────┼──────────┼────────┼────────┼─────────┼───────┼───────┤
│ 1 │ Nguyễn A │ Q.1 HCM │ NV │ 120 │ 45 │ 37.5% │ 22.5tr│
│ 2 │ KOL Hương │ — │ Đối tác│ 185 │ 62 │ 33.5% │ 31.0tr│
│ 3 │ Trần B │ Q.3 HCM │ NV │ 98 │ 31 │ 31.6% │ 15.2tr│
│ 4 │ PK Đông Y │ — │ Đối tác│ 76 │ 22 │ 28.9% │ 11.0tr│
│ 5 │ Lê C │ Đà Nẵng │ NV │ 75 │ 28 │ 37.3% │ 14.0tr│
├────┴───────────┴──────────┴────────┴────────┴─────────┴───────┴───────┤
│ Tổng: 554 voucher | 188 đã dùng | 33.9% | 93.7tr │
│ ← 1 / 3 → (20 hàng/trang) │
└───────────────────────────────────────────────────────────────────────┘WF-04: Staff Drill-down Dialog (P2)
┌─────────────────────────────────────────────────────────────────┐
│ Chi tiết phát voucher — Nguyễn A (Q.1 HCM) [X] │
├─────────────────────────────────────────────────────────────────┤
│ Tổng phát: 120 | Đã dùng: 45 (37.5%) | DT: 22.5 triệu │
├────┬───────────┬──────────────┬────────────┬────────┬──────────┤
│ # │ Mã VC │ Khách hàng │ Ngày phát │ Trạng │ DT (đ) │
│ │ │ │ │ thái │ │
├────┼───────────┼──────────────┼────────────┼────────┼──────────┤
│ 1 │ CDLF42L4S │ Trần Thị Mai │ 15/02/2026 │ ●Đã │ 500.000 │
│ │ │ 0901234567 │ │ dùng │ │
│ 2 │ CD4YCJ2VC │ Ngô Văn Hùng │ 16/02/2026 │ ●Kích │ — │
│ │ │ 0912345678 │ │ hoạt │ │
│ 3 │ CDXHYA5PA │ Lê Thị Lan │ 18/02/2026 │ ●Hết │ — │
│ │ │ 0923456789 │ │ hạn │ │
│ 4 │ CDVRGMLAK │ Phạm Anh Tuấn│ 20/02/2026 │ ●Đã │ 1.200k │
│ │ │ 0934567890 │ │ dùng │ │
├────┴───────────┴──────────────┴────────────┴────────┴──────────┤
│ ← 1 / 6 → (20 hàng/trang) │
└────────────────────────────────────────────────────────────────┘WF-05: Wizard Step Đối tác (P3)
┌─────────────────────────────────────────────────────────────────┐
│ Bước 6: Đối tác phát voucher │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ☑ Cho phép đối tác phát voucher chiến dịch này │
│ │
│ [Chọn đối tác ▼ (Tìm theo tên, mã...) ] [+ Thêm đối tác] │
│ │
├────┬──────────────┬──────────┬──────────────┬───────────────────┤
│ # │ Đối tác │ Loại │ Quota │ Hành động │
├────┼──────────────┼──────────┼──────────────┼───────────────────┤
│ 1 │ KOL Hương │ KOL │ [200_______] │ [Xóa] │
│ │ AFF-001 │ │ │ │
│ 2 │ PK Đông Y │ Phòng │ [500_______] │ [Xóa] │
│ │ AFF-012 │ │ khám │ │
│ 3 │ Đại lý Q.7 │ Đại lý │ [☑ Không ]│ [Xóa] │
│ │ AFF-023 │ │ [ giới hạn ]│ │
├────┴──────────────┴──────────┴──────────────┴───────────────────┤
│ Tổng: 3 đối tác | Quota: 700 + Không giới hạn │
│ │
│ [← Quay lại] [Hoàn tất →] │
└─────────────────────────────────────────────────────────────────┘WF-06: Tab Chu kỳ sử dụng (P4)
┌─────────────────────────────────────────────────────────────────────────┐
│ Báo cáo chu kỳ sử dụng voucher │
├─────────────────────────────────────────────────────────────────────────┤
│ [Từ ngày ▼] [Đến ngày ▼] [Chi nhánh ▼] [Chiến dịch ▼] │
│ Nhóm theo: [◉ Tổng quan ○ Chiến dịch ○ Chi nhánh ○ Nhân viên] │
│ [Xuất Excel] │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 📊 TB │ │ ⚡ Nhanh │ │ 📈 Trung │ │ ✅ Tỉ lệ │ │
│ │ 12 ngày │ │ nhất │ │ vị │ │ dùng │ │
│ │ │ │ 1 ngày │ │ 9 ngày │ │ 35.2% │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Phân bổ chu kỳ sử dụng │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 0-3 ngày ████████████████████████████ 125 (28%) │ │
│ │ 4-7 ngày ████████████████████ 89 (20%) │ │
│ │ 8-14 ngày ██████████████████ 76 (17%) │ │
│ │ 15-30 ngày ██████████████ 62 (14%) │ │
│ │ 31-60 ngày ██████████ 45 (10%) │ │
│ │ 60+ ngày ██████ 28 (6%) │ │
│ │ Chưa dùng ████ 25 (5%) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Chi tiết │
│ ┌────┬──────────────┬───────┬───────┬────────┬────────┬───────┐ │
│ │ # │ Chiến dịch │Đã KH │Đã dùng│ TB ngày│Trung vị│ Tỉ lệ│ │
│ ├────┼──────────────┼───────┼───────┼────────┼────────┼───────┤ │
│ │ 1 │ Tết Kim Cương│ 95 │ 38 │ 8.5 │ 6 │ 40.0%│ │
│ │ 2 │ Sinh nhật │ 51 │ 15 │ 14.2 │ 12 │ 29.4%│ │
│ │ 3 │ Game con ngựa│ 458 │ 158 │ 11.3 │ 8 │ 34.5%│ │
│ │ 4 │ TTCN 3-5 tr │ 1 │ 0 │ — │ — │ 0.0%│ │
│ └────┴──────────────┴───────┴───────┴────────┴────────┴───────┘ │
│ ← 1 / 2 → (20 hàng/trang) │
└──────────────────────────────────────────────────────────────────────┘WF-07: Badge nguồn đối tác trên màn hình tạo ĐH (P3)
┌──────────────────────────────────────────────┐
│ Chọn voucher cho đơn hàng │
├──────────────────────────────────────────────┤
│ Mã voucher: CDLF42L4SB │
│ Tên: 01 Suất chăm sóc da trắng sáng │
│ Giá trị: 500.000đ │
│ Trạng thái: ● Đã kích hoạt │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 🏷 Voucher do KOL Hương phát │ │
│ │ Mã đối tác: AFF-001 │ │
│ │ 💡 Gợi ý: gán affiliate cho ĐH │ │
│ └────────────────────────────────────┘ │
│ │
│ [Áp dụng] [Hủy] │
└──────────────────────────────────────────────┘B-Export. Export Specifications
Export 1: Override Dashboard (P1)
| # | Cột | Key | Format | Width |
|---|---|---|---|---|
| 1 | STT | index | Number | 8 |
| 2 | Người duyệt | approved_by_name | Text | 20 |
| 3 | Chi nhánh | branch_name | Text | 20 |
| 4 | Mã voucher | voucher_code | Text | 15 |
| 5 | Tên voucher | voucher_name | Text | 30 |
| 6 | Khách hàng | customer_name | Text | 25 |
| 7 | Lý do | reason_code_label | Text | 18 |
| 8 | Ghi chú | reason_note | Text | 35 |
| 9 | Giờ còn lại | hours_remaining | Number (2 decimal) | 12 |
| 10 | Ngày duyệt | created_at | dd/MM/yyyy HH:mm | 18 |
- File name:
Override_Voucher_{from}_{to}.xlsx - Sheet name:
Override - Row cap: 10.000 rows
- Async threshold: > 5.000 rows → dùng export-api
Export 2: Thống kê NV (P2)
| # | Cột | Key | Format | Width |
|---|---|---|---|---|
| 1 | STT | index | Number | 8 |
| 2 | Nhân viên | staff_name | Text | 25 |
| 3 | Chi nhánh | branch_name | Text | 20 |
| 4 | Loại | distributor_type_label | Text | 15 |
| 5 | Số đã phát | total_distributed | Number | 12 |
| 6 | Số đã kích hoạt | total_activated | Number | 15 |
| 7 | Số đã dùng | total_redeemed | Number | 12 |
| 8 | Số hết hạn | total_expired | Number | 12 |
| 9 | Tỉ lệ sử dụng (%) | conversion_rate | Percent (2 decimal) | 15 |
| 10 | Doanh thu (đ) | total_revenue | VND currency | 18 |
| 11 | TB ngày dùng | avg_days_to_redeem | Number (1 decimal) | 15 |
- File name:
Thong_ke_NV_Voucher_{campaign}_{from}_{to}.xlsx - Sheet 1:
Tong_hop(bảng summary trên) - Sheet 2:
Chi_tiet(drill-down tất cả NV — mã VC, khách hàng, ngày phát, trạng thái, DT) - Row cap: 50.000 rows
- Async threshold: > 5.000 rows
Export 3: Chu kỳ sử dụng (P4)
| # | Cột | Key | Format | Width |
|---|---|---|---|---|
| 1 | STT | index | Number | 8 |
| 2 | Nhóm | group_name | Text | 30 |
| 3 | Tổng kích hoạt | total_activated | Number | 15 |
| 4 | Tổng đã dùng | total_redeemed | Number | 15 |
| 5 | Tổng hết hạn | total_expired | Number | 15 |
| 6 | Đang chờ | total_pending | Number | 12 |
| 7 | TB ngày | avg_days_to_redeem | Number (1 decimal) | 12 |
| 8 | Nhanh nhất | min_days_to_redeem | Number | 12 |
| 9 | Lâu nhất | max_days_to_redeem | Number | 12 |
| 10 | Trung vị | median_days_to_redeem | Number (1 decimal) | 12 |
- File name:
Chu_ky_su_dung_Voucher_{from}_{to}.xlsx - Sheet 1:
Tong_hop(bảng summary trên) - Sheet 2:
Phan_bo(bucket distribution: group_name, bucket, count, percentage) - Row cap: 10.000 rows
- Async threshold: > 5.000 rows
B-Edge Cases
Phase 1 — Kiểm soát thời gian
| # | Edge Case | Hành vi mong đợi |
|---|---|---|
| EC-P1-01 | Voucher kích hoạt đúng boundary (VD: 23h59 trước midnight, min=24h) | So sánh bằng giờ thực, không round. 23h59 < 24h → vẫn chặn |
| EC-P1-02 | min_activation_hours = 0 (không giới hạn) | Bỏ qua check hoàn toàn, voucher dùng ngay được |
| EC-P1-03 | Voucher thuộc campaign ĐÃ KẾT THÚC nhưng chưa expired | Check min_activation_hours vẫn áp dụng. Campaign end ≠ voucher expire |
| EC-P1-04 | Override 2 lần cho cùng 1 voucher | Cho phép — mỗi override là 1 record riêng. ĐH đầu có thể bị hủy, cần override lần 2 |
| EC-P1-05 | Timezone: NV ở UTC+7, server ở UTC | Tất cả so sánh dùng Asia/Ho_Chi_Minh. activated_at đã lưu TIMESTAMPTZ |
| EC-P1-06 | Voucher manual không thuộc campaign, system default = 0 | Bỏ qua check, voucher dùng ngay. Nhưng expired_at vẫn set nếu default_expiry_days > 0 |
| EC-P1-07 | Manager override cho chính ĐH mình tạo | Cho phép — Manager vừa là người approve override vừa là người tạo ĐH |
| EC-P1-08 | Voucher kích hoạt trước khi feature deploy (không có min_activation_hours trong campaign cũ) | DEFAULT 24 từ migration. Tất cả campaign cũ tự động có min=24h. Nếu không muốn → Admin set 0 |
Phase 2 — Thống kê NV
| # | Edge Case | Hành vi mong đợi |
|---|---|---|
| EC-P2-01 | Voucher phát bởi NV đã nghỉ việc (account deactivated) | Vẫn hiện trong report với tên NV. Không xóa data lịch sử |
| EC-P2-02 | NV phát voucher rồi chuyển chi nhánh | Report theo branch_id tại thời điểm phát (lưu trong user_vouchers), không theo branch hiện tại |
| EC-P2-03 | staff_id NULL (voucher online tự claim) | Không hiện trong report NV. Filter staff_id IS NOT NULL |
| EC-P2-04 | NV phát 0 voucher trong khoảng thời gian filter | Không hiện hàng NV đó (chỉ hiện NV có activity) |
| EC-P2-05 | Doanh thu = 0 cho voucher đã dùng (voucher tặng miễn phí) | Hiện DT = 0đ (không ẩn). Voucher tặng dịch vụ miễn phí vẫn có giá trị |
| EC-P2-06 | Partner app: NV xem thống kê nhưng chưa phát voucher nào | Hiện empty state: "Bạn chưa phát voucher nào" |
Phase 3 — Đối tác
| # | Edge Case | Hành vi mong đợi |
|---|---|---|
| EC-P3-01 | Đối tác bị deactivated (is_active=false) giữa chiến dịch | Đối tác không thể phát thêm voucher. Voucher đã phát vẫn hợp lệ |
| EC-P3-02 | Đối tác phát voucher đúng lúc hết quota (concurrent) | Atomic increment. Chỉ 1 request thành công, request còn lại nhận lỗi "Đã hết quota" |
| EC-P3-03 | Admin tăng quota cho đối tác đang hết quota | Đối tác có thể phát tiếp ngay lập tức (upsert update quota) |
| EC-P3-04 | Xóa đối tác khỏi campaign đã phát voucher | Soft delete (deleted_at). Voucher đã phát vẫn giữ staff_id. Report vẫn hiện |
| EC-P3-05 | Campaign không gán đối tác nào | Step wizard bỏ qua. Flow phát voucher hoạt động bình thường (chỉ NV nội bộ) |
| EC-P3-06 | Đối tác phát voucher cho chiến dịch KHÔNG được gán | Từ chối: "Đối tác không được phép phát voucher chiến dịch này" |
Phase 4 — Chu kỳ
| # | Edge Case | Hành vi mong đợi |
|---|---|---|
| EC-P4-01 | Campaign 100% voucher pending (chưa ai dùng) | KPI avg/min/max/median hiện "—". Chart hiện 100% bucket "Chưa dùng" |
| EC-P4-02 | Voucher redeemed rồi restored (ĐH hủy) rồi redeemed lại | Dùng voucher_logs DISTINCT ON → lấy lần redeem cuối cùng. Cycle = lần redeem cuối - activated_at |
| EC-P4-03 | Voucher kích hoạt và dùng cùng ngày (cycle = 0) | Hợp lệ. Nằm trong bucket "0-3 ngày". KPI min = 0 |
| EC-P4-04 | Filter trả > 100k vouchers | DB-level aggregation. Frontend chỉ nhận kết quả đã aggregate (~10-50 rows). Không ảnh hưởng performance |
| EC-P4-05 | So sánh 2 chiến dịch: 1 có data, 1 không | Chiến dịch không data hiện "—" cho tất cả metrics. Bar chart hiện 0% tất cả buckets |
| EC-P4-06 | PERCENTILE_CONT trả NULL khi không có data redeem | Median hiện "—". Xử lý COALESCE(median, NULL) trong SQL |
Changelog
| Version | Ngày | Thay đổi |
|---|---|---|
| 1.0 | 2026-03-16 | Initial — B1-B7, Wireframes, Export Spec, Edge Cases (30 cases) |