Appearance
Đặc tả UI (UI Spec) — Insight Ghi Âm cho BOD
Phiên bản: 1.0 Ngày: 15/05/2026 Tác giả: PO/BA Mục đích: Hợp đồng màn hình/thay đổi/tương tác. KHÔNG mô tả lại business rule dài (xem prd.md).
Source of mockups: Tất cả ASCII mockups chi tiết đã có trong design doc gốc
../../superpowers/specs/2026-05-15-record-bod-insight-design.md(§4.2.1 đến §4.2.20). File này tóm tắt + thêm As-Is inventory + matrices required cho L profile.
B-PRE) Discovery context
B-PRE.1) Layout UI hiện tại (/e/record baseline)
Component path: diva-admin/src/modules/ecommerce/pages/Records.tsx + components/record/RecordTable.tsx
Bố cục hiện tại:
┌──────────────────────────────────────────────────────────────┐
│ Quản Lý Ghi Âm [🔔] [N] NGUYỄN SƠN THỌ│
├──────────────────────────────────────────────────────────────┤
│ [🔍 search...] [Bộ lọc] [Long Khánh ▼] [Tất cả ▼] [Tải] [Xóa]│
├──────────────────────────────────────────────────────────────┤
│ ┌──┬─────┬──────────┬──────────┬──────┬─────┬─────┬─────┐ │
│ │ │STT │FILE GHI │TIME START│TIME │THỜI │KÍCH │ ... │ │
│ │ │ │ÂM │ │END │LƯỢNG│THƯỚC│ │ │
│ ├──┼─────┼──────────┼──────────┼──────┼─────┼─────┼─────┤ │
│ │☐ │ 1 │DV230689_ │15/05 08:26│08:31 │4:57 │0.85M│ ... │ │
│ │☐ │ 2 │DV230689_ │14/05 13:08│13:40 │24:13│4.16M│ ... │ │
│ │... rows │ │
│ └──┴─────┴──────────┴──────────┴──────┴─────┴─────┴─────┘ │
│ [Pagination: Số hàng 20 ▼ 1 2 3 ... 6 ►] │
└──────────────────────────────────────────────────────────────┘B-PRE.2) Kiểm kê UI hiện tại (As-Is UI Inventory)
| Khu vực | Trường/Cột/CTA | Thứ tự | Hành vi |
|---|---|---|---|
| Tiêu đề | Tiêu đề "Quản Lý Ghi Âm" + breadcrumb | 1 | Tĩnh |
| Tiêu đề | Bell notification + user menu | 2 | Tái sử dụng pattern chung |
| Thanh lọc | Search input (tên file, KH, NV) | 3 | Lọc thời gian thực |
| Thanh lọc | "Bộ lọc" icon button | 4 | Mở popup lọc nâng cao |
| Thanh lọc | Branch select | 5 | Chọn nhiều |
| Thanh lọc | Status select (Tất cả / Đã tải lên / Chưa tải) | 6 | Lọc theo file.url null |
| Thanh thao tác | Checkbox chọn hàng loạt | 7 | Bật/tắt checkbox dòng |
| Thanh thao tác | "Tải xuống" button | 8 | Tải ZIP các file đã chọn |
| Thanh thao tác | "Xóa file" button | 9 | Xác nhận xóa + soft delete |
| Cột bảng | STT, FILE GHI ÂM, TIME START, TIME END, THỜI LƯỢNG, KÍCH THƯỚC, KHÁCH HÀNG, LỊCH HẸN, NGƯỜI TẠO, TRẠNG THÁI | 10-19 | Sắp xếp theo created_at DESC mặc định |
| Thao tác dòng | Play icon (▶) — open RecordForm modal | 20 | Modal audio HTML5 |
| Phân trang | Số hàng dropdown (20 default) + page numbers + arrows | 21 | Phân trang server-side |
B-PRE.3) Bản đồ tái sử dụng (Reuse Map)
| Asset hiện có | Sẽ reuse / extend cho insight |
|---|---|
RecordForm.tsx HTML5 audio player | ✅ Reuse via drill-down /e/record |
RecordTable.tsx filter state + GraphQL query | 🔧 Extend (parse URL query + 2 filter mới) |
DashboardCardItem.tsx | ✅ Reuse trong KPI Row |
Chart.js wrappers (BarChart, LineChart, PieChart) | ✅ Reuse cho Trend/TopStaff/Branch charts |
CustomerDasboardFilterType pattern | ✅ Adapt cho FilterBar |
Sidebar entry pattern (module.ts:2160) | ✅ Reuse cho sidebar "Insight Ghi Âm" |
B0) Ma trận chuẩn hóa (Required matrices)
B0.4) Field × Surface (mapping field hiển thị qua screens)
| Field nguồn | Trang chính (Dashboard) | Modal Lịch hẹn thiếu | Modal KH chờ | /e/record drill-down |
|---|---|---|---|---|
record.id | (chỉ là khóa) | khóa dòng | — | khóa dòng |
record.created_at | trục X biểu đồ trend, heatmap | Tóm tắt KPI, cột "Ngày giờ" | — | cột "TIME START" |
record.created_by (NV) | Biểu đồ Top NV | cột "NV TƯ VẤN" | cột "NV phụ trách" | cột "NGƯỜI TẠO" |
record.appointment_id | Tuân thủ % KPI | "STT" link → lịch hẹn | — | cột "LỊCH HẸN" link |
record.customer_id | — | cột "KHÁCH HÀNG" | cột "KH" | cột "KHÁCH HÀNG" |
appointment.branch_id | Biểu đồ chi nhánh, Heatmap tổng hợp | cột "CHI NHÁNH" | — | lọc chi nhánh |
appointment."from" | — | cột "NGÀY GIỜ" | cột "Lịch hẹn" | cột "TIME START" |
reference_file.duration | KPI #2 Thời lượng TB | — | — | column "THỜI LƯỢNG" |
reference_file.url | — | — | — | thao tác dòng phát |
account.display_name | Top Staff label, tooltip | hiển thị "NV TƯ VẤN" | hiển thị "NV phụ trách" | cột "NGƯỜI TẠO" display |
B0.5) State × Screen (lifecycle states)
| Trạng thái | Dashboard | Modal Lịch hẹn thiếu | Modal KH chờ | /e/record |
|---|---|---|---|---|
| Rỗng (không có dữ liệu trong phạm vi) | Trạng thái rỗng cho mỗi widget: "Chưa đủ dữ liệu" / "Không có cuộc tư vấn" | "✅ Tất cả lịch hẹn đều có ghi âm" | "✅ Không có KH chờ TV" | (hành vi cũ) |
| Đang tải (lần đầu / filter thay đổi) | Skeleton pulse cho 5 KPI + 4 chart | Spinner trong nội dung modal | Spinner | (cũ) |
| Đã tải (cache) | Hiện cache + chỉ báo cũ | (tức thì nếu có cache) | (tức thì nếu có cache) | (cũ) |
| Lỗi (query thất bại) | Banner đỏ nhạt cho mỗi widget + [Thử lại] | Modal hiện lỗi + thử lại | Modal hiện lỗi | (cũ) |
| Không có quyền (403) | Chuyển hướng /forbidden | (N/A — modal chỉ mở khi có quyền) | (N/A) | (cũ) |
| Mạng chậm (> 8s timeout) | Banner "Đang tải lâu, retry?" | Spinner "Đang tải..." > 3s | Spinner | (cũ) |
B0.6) Chất lượng Wireframe
Wireframes detail trong design doc §4.2.1 đến §4.2.20. Mọi mockup tuân thủ:
- ASCII chuẩn, canh lề monospace
- Tiếng Việt label tự nhiên
- Số liệu sample thực tế (vd 124 cuộc, 70 chi nhánh, 156 KH chờ TV)
- Chú thích chỉ rõ hành vi (click → navigate, hover → tooltip)
- Token màu rõ ràng (xanh lá, đỏ, vàng cam, gray neutral)
B0.7) Cặp song ngữ Việt-Anh
| Khái niệm | UI label (VI) | Code/tech term (EN) |
|---|---|---|
| Ghi âm tư vấn | Cuộc tư vấn, Ghi âm | record, voice consultation |
| Thời lượng TB | Thời lượng TB | avg_duration_sec |
| NV hoạt động | NV hoạt động | active_staff |
| Tuân thủ ghi âm | Tuân thủ ghi âm | compliance_rate |
| KH chờ TV | KH chờ TV | awaiting_consultation_count |
| Chi nhánh | Chi nhánh | branch |
| So tuần trước | so tuần trước | delta_wow_pct |
| Insight Dashboard | Insight Ghi Âm | record_insight_dashboard |
| Tải xuống | Tải xuống | download |
| Lịch hẹn | Lịch hẹn | appointment |
| Mở appointment | Mở appointment | open_appointment |
| Tải xuống audio | Tải xuống | download_audio |
B0.8) Đối chiếu Schema chéo
Tất cả fields hiển thị UI đều có corresponding DB column trong public.record, public.appointment, public.reference_file, hoặc account. KHÔNG có field UI nào không có nguồn DB. Cross-ref: dev-spec.md §C4 Data Model.
B0.9) Kiểm kê tương tác (10 loại canonical)
| Loại | Vị trí | Mô tả |
|---|---|---|
| Nhấn → Điều hướng | Biểu đồ Top NV, Trend point, Heatmap cell, Branch bar, KPI title (P2) | Drill-down /e/record với query params |
| Nhấn → Mở Modal | Anomaly card CTA | Mở MissingRecordsModal / AnomalyMissingTVModal |
| Nhấn ▼ → Mở/đóng dropdown | Filter chips (Branch ▼, Date ▼, Staff ▼, Sort ▼), Page size selector ▼, So sánh ▼ | Hiển/ẩn dropdown menu. Nhấn ra ngoài hoặc ESC → đóng |
| Di chuột → Tooltip | KPI label ⓘ, chart segments, footer "Cập nhật lần cuối" | Hiển thị thông tin theo ngữ cảnh |
| Di chuột → Đổi trạng thái | Bar chart bar, table row | Viền nổi bật, cursor pointer |
| Ô tìm kiếm | Filter dropdown (Branch, Staff), Modal search box | Lọc thời gian thực với debounce 300ms |
| Phím tắt | Modal: ESC → close; Player: space → play/pause | Hành vi chuẩn của trình duyệt/component |
| Kéo/thay đổi kích thước | (KHÔNG có) | — |
| Chọn nhiều | Branch dropdown, Staff dropdown chips | Thêm/xóa item |
| Phân trang | Modal pagination, /e/record pagination | ← / → / page number / jump |
B-VISUAL) Mockup chuẩn (copy từ design doc — nguồn duy nhất cho FE)
Bộ mockup tự túc cho FE Dev implement P0 mà KHÔNG cần đọc design doc song song. Nguồn gốc:
docs/superpowers/specs/2026-05-15-record-bod-insight-design.md§4.2.1 / 4.2.6 / 4.2.7 / 4.2.8 / 4.2.10 / 4.2.15.c.
B-VISUAL.1) Bố cục desktop đầy đủ — 1280px (chuẩn canonical)
╔══════════════════════════════════════════════════════════════════════════════════════════╗
║ [≡] Diva Admin › Ecommerce › Quản Lý Ghi Âm › Insight [🔔3] [N] ▾ ║
╠══════════════════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Insight Ghi Âm ║
║ Theo dõi mức độ tuân thủ ghi âm và hiệu suất tư vấn của toàn hệ thống ║
║ ║
║ ┌────────────────────────────────────────────────────────────────────────────────────┐ ║
║ │ Chi nhánh Khoảng thời gian Nhân viên │ ║
║ │ [🏢 70 chi nhánh ▼] [📅 7 ngày qua ▼] [👤 Tất cả NV ▼] │ ║
║ │ │ ║
║ │ [🔄 Làm mới] │ ║
║ └────────────────────────────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌────────────┐║
║ │ CUỘC TƯ VẤN ⓘ│ │ THỜI LƯỢNG TB │ │ NV HOẠT ĐỘNG │ │ TUÂN THỦ GHI ÂM│ │ KH CHỜ TV ⓘ│║
║ │ 124 │ │ 15p 36s │ │ 8 │ │ 94% │ │ 12 │║
║ │ ▲ 12% so tuần │ │ ▲ 8% so tuần │ │ ─ Không đổi │ │ ▼ 2% so tuần │ │ ⚠ Cần xem │║
║ │ trước │ │ trước │ │ │ │ trước │ │ │║
║ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ └────────────┘║
║ (xanh lá) (xanh lá) (xám trung tính) (đỏ cam — chú ý) (đỏ alert) ║
║ ║
║ ┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────┐║
║ │ Số cuộc tư vấn theo ngày ⓘ⤡ │ │ Top 10 nhân viên tư vấn ⓘ⤡ │║
║ │ 40 ┤ ● │ │ Nguyễn T. Lam ████████████ 32 │║
║ │ │ ╲ ● │ │ Võ T. Hiếu ████████ 24 │║
║ │ 30 ┤ ╲ ╱ ╲ │ │ Trần V. Hùng ██████ 18 │║
║ │ │ ● ╱ ╲ ● │ │ ... │║
║ │ 20 ┤ ╲ ╱ ● ╱ │ │ Click vào tên → drill /e/record │║
║ │ │ ●────────● ╲ ╱ │ │ │║
║ │ 10 ┤ ●────────● │ │ │║
║ │ 0 └──┬──┬──┬──┬──┬──┬──┬─ │ │ │║
║ │ 9/5 10 11 12 13 14 15 │ │ │║
║ │ Click 1 điểm → drill /e/record ngày đó │ │ │║
║ └──────────────────────────────────────────────┘ └─────────────────────────────────────┘║
║ ║
║ ┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────┐║
║ │ Heatmap: giờ × thứ ⓘ⤡ │ │ Top 10 chi nhánh (trên 70 CN) ⓘ⤡ │║
║ │ T2 T3 T4 T5 T6 T7 CN │ │ Long Khánh ████████████ 156 (28%) │║
║ │ 8h ░░ ░░ ▒▒ ▒▒ ▓▓ ░░ ░░ │ │ Quận 1 ██████████ 124 (22%) │║
║ │ 9h ▒▒ ▓▓ ▓▓ ▓▓ ▓▓ ▒▒ ░░ │ │ Quận 3 ████████ 98 (17%) │║
║ │ 10h ▓▓ ██ ██ ██ ██ ▓▓ ▒▒ │ │ Tân Phú █████ 62 (11%) │║
║ │ 11h ██ ██ ██ ██ ██ ▓▓ ▒▒ │ │ ... │║
║ │ ... │ │ ─────────────────────────────────── │║
║ │ Ít ░░ ▒▒ ▓▓ ██ Nhiều │ │ Khác (60 CN) ██ 76 (14%) │║
║ │ 0 1-3 4-7 8+ │ │ │║
║ │ Tổng hợp qua 70 CN đã chọn │ │ [Xem tất cả 70 chi nhánh →] │║
║ └──────────────────────────────────────────────┘ └─────────────────────────────────────┘║
║ ║
║ ┌────────────────────────────────────────────────────────────────────────────────────┐ ║
║ │ 🚨 Cảnh báo & bất thường │ ║
║ │ 🔴 X lịch hẹn tư vấn KHÔNG có ghi âm [Xem X lịch hẹn →] │ ║
║ │ Cao nhất: NV ... (N cases) — Chi nhánh ... │ ║
║ │ │ ║
║ │ 🟡 Y khách hàng có lịch hẹn nhưng chưa được TV trong khoảng │ ║
║ │ [Xem danh sách Y KH →] │ ║
║ └────────────────────────────────────────────────────────────────────────────────────┘ ║
║ ║
║ Cập nhật lần cuối: DD/MM/YYYY HH:MM:SS | Tự động làm mới mỗi 60s ║
╚══════════════════════════════════════════════════════════════════════════════════════════╝
Reading flow (cảm giác BOD khi mở trang):
| Vùng | Câu hỏi BOD | Component trả lời |
|---|---|---|
| Trên cùng (3 giây) | "What's happening now?" | 5 KPI cards — số liệu + delta WoW |
| Giữa trên | "Where's the trend?" | Trend chart + Top staff bar |
| Giữa dưới | "When/where?" | Heatmap + Branch top bar |
| Cuối trang | "What needs my attention?" | Anomaly alerts (2 cards) với CTA |B-VISUAL.2) Thanh lọc — 4 trạng thái chuẩn
Trạng thái 1 — Đóng (mặc định):
┌──────────────────────────────────────────────────────────────────────────┐
│ [🏢 70 chi nhánh ▼] [📅 7 ngày qua ▼] [👤 Tất cả NV ▼] [🔄 Làm mới] │
└──────────────────────────────────────────────────────────────────────────┘
Trạng thái mặc định cho BOD (có view_all): "70 chi nhánh" (= all)
Trạng thái mặc định cho BranchManager: "Long Khánh" (= branches user được phép)
Quy ước chip compact:
- 0 chọn → "Chọn chi nhánh"
- 1 chọn → "Long Khánh"
- 2-3 chọn → "Long Khánh, Quận 1, +1"
- >3 chọn → "5 chi nhánh đã chọn"
- tất cả → "70 chi nhánh"Trạng thái 2 — Dropdown Chi nhánh mở (search + multi-select + bulk + region group):
┌────────────────────────────────────────────┐
│ 🔍 Tìm chi nhánh... [✕] │ ← Lọc thời gian thực, debounce 150ms
│ ──────────────────────────────────────────│
│ Đã chọn 70/70 chi nhánh │
│ [Chọn tất cả] [Bỏ chọn tất cả] │ ← Thao tác hàng loạt
│ ──────────────────────────────────────────│
│ ▾ 📍 TP. Hồ Chí Minh (28 CN) │ ← Nhóm theo khu vực (mở/đóng được)
│ ☑ Long Khánh │
│ ☑ Quận 1 │
│ ☑ Quận 3 │
│ ... (25 chi nhánh nữa, scroll) │
│ │
│ ▾ 📍 Hà Nội (24 CN) │
│ ☑ Hà Đông │
│ ... (23 nữa) │
│ │
│ ▸ 📍 Đà Nẵng (12 CN) │ ← Nhóm đang đóng
│ ▸ 📍 Hải Phòng (3 CN) │
│ ▸ 📍 Cần Thơ (3 CN) │
│ │
│ [Hủy] [Áp dụng 70/70] │
└────────────────────────────────────────────┘
Fallback nếu BE chưa có column region (PD-001) → flat list 70 CN với search:
┌────────────────────────────────────────────┐
│ 🔍 Tìm chi nhánh... [✕] │
│ Đã chọn 70/70 │
│ [Chọn tất cả] [Bỏ chọn tất cả] │
│ ──────────────────────────────────────────│
│ ☑ Long Khánh │
│ ☑ Quận 1 │
│ ... (66 chi nhánh nữa, scroll) │
│ [Hủy] [Áp dụng 70/70] │
└────────────────────────────────────────────┘
Hành vi:
- Dropdown max height ~500px, scroll bên trong
- Tìm kiếm lọc thời gian thực (debounce 150ms) — show "Tìm thấy X chi nhánh"
- Tiêu đề nhóm có checkbox 3 trạng thái (☑ all / ☒ partial / ☐ none)
- "Chọn tất cả" / "Bỏ chọn tất cả" áp dụng TOÀN BỘ
- Nút "Áp dụng" hiển thị số đếm "(X/70)"
- Nhấn ngoài → đóng dropdown KHÔNG áp dụng (= Hủy)Trạng thái 3 — Dropdown khoảng thời gian (preset + custom):
┌──────────────────────────────────┐
│ ○ Hôm nay │
│ ○ Hôm qua │
│ ● 7 ngày qua ✓ │
│ ○ 30 ngày qua │
│ ○ Tháng này │
│ ○ Tháng trước │
│ ○ Quý này │
│ ○ Tùy chỉnh... │
│ ──────────────────────────────── │
│ Từ: [09/05/2026] Đến: [15/05] │
│ [Hủy] [Áp dụng] │
└──────────────────────────────────┘
Hard limit: range > 365 ngày → disable [Áp dụng] + tooltip "Tối đa 365 ngày"Trạng thái 4 — Dropdown Nhân viên (chế độ tìm-only, 700 NV):
Mặc định rỗng — KHÔNG render 700 NV upfront. BOD type để search.
┌────────────────────────────────────────────┐
│ 🔍 Tìm NV theo tên hoặc số điện thoại... │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ Đã chọn: 0 NV │ │
│ │ (Để trống = Tất cả NV) │ │
│ └────────────────────────────────────────┘ │
│ │
│ ────────── Trạng thái rỗng ────────────── │
│ Nhập tên NV để tìm │
│ (Không hiển thị danh sách 700 NV mặc định) │
│ │
│ Hoặc lọc nhanh: │
│ [Top 10 NV active 7 ngày qua] │
│ [NV của các chi nhánh đã chọn] │
└────────────────────────────────────────────┘
Khi type "lam" (debounce 300ms):
┌────────────────────────────────────────────┐
│ 🔍 lam [✕] │
│ Đã chọn: 0 NV │
│ Tìm thấy 12 NV (hiển thị 10 đầu): │
│ ☐ Nguyễn T. Lam — Long Khánh │
│ ☐ Lâm V. Cường — Quận 1 │
│ ☐ Trần Thanh Lam — Quận 3 │
│ ... (8 nữa) │
│ [Tải thêm 2 NV] │ ← Tải lazy
│ [Hủy] [Áp dụng] │
└────────────────────────────────────────────┘
Khi đã chọn 3 NV (chips):
┌────────────────────────────────────────────┐
│ 🔍 Tìm thêm NV... │
│ ┌────────────────────────────────────────┐ │
│ │ [Nguyễn T. Lam ✕] [Lâm V. Cường ✕] │ │
│ │ [Trần Thanh Lam ✕] │ │
│ └────────────────────────────────────────┘ │
│ Filter chip "Tất cả NV" → "3 NV đã chọn" │
│ [Hủy] [Áp dụng (3)] │
└────────────────────────────────────────────┘B-VISUAL.3) Trạng thái Đang tải / Rỗng / Lỗi
Đang tải (filter thay đổi / lần đầu):
┌──────────────────────────────────────────────────────────────────────────┐
│ Insight Ghi Âm │
│ ─────────────────────────────────────────────────────────────────────── │
│ [filter bar — vẫn click được, không bị disable] │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ ░░░░░░░ │ │ ░░░░░░░ │ │ ░░░░░░░ │ │ ░░░░░░░ │ │ ░░░░░░░ │ │ ← Skeleton
│ │ ░░░░░ │ │ ░░░░░ │ │ ░░░░░ │ │ ░░░░░ │ │ ░░░░░ │ │ pulse 1.2s
│ │ ░░░░ │ │ ░░░░ │ │ ░░░░ │ │ ░░░░ │ │ ░░░░ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ ░░░ Đang tải ░░░ │ │ ░░░ Đang tải ░░░ │ │
│ └────────────────────────┘ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘Trạng thái rỗng (không có data trong phạm vi):
┌──────────────────────────────────────────────────────────────────────────┐
│ Insight Ghi Âm │
│ [Chi nhánh: Tân Phú ▼] [📅 Hôm qua ▼] [👤 Tất cả ▼] │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐│
│ │ 🎙 ││
│ │ ││
│ │ Không có dữ liệu ghi âm trong khoảng đã chọn ││
│ │ Có thể chi nhánh chưa phát sinh tư vấn hoặc lọc quá hẹp. ││
│ │ ││
│ │ [Mở rộng khoảng thời gian] [Xem tất cả chi nhánh] ││
│ └──────────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────────┘Trạng thái lỗi (1 chart bị lỗi, phần còn lại OK):
┌──────────────────────────────────────────────────────────────────────────┐
│ ┌──────────────────────────────────────────────────────────────────────┐│
│ │ ⚠ Không tải được dữ liệu trend [Thử lại] [✕]││ ← Banner đỏ nhạt
│ │ Connection timeout (8s). Các phần khác vẫn hiển thị bình thường. ││ chỉ chart fail
│ └──────────────────────────────────────────────────────────────────────┘│ không phá toàn page
│ [KPI row + các chart khác vẫn render bình thường] │
└──────────────────────────────────────────────────────────────────────────┘B-VISUAL.4) Modal Lịch hẹn thiếu ghi âm — Phân trang + tìm + sắp xếp + lọc CN
┌──────────────────────────────────────────────────────────────────────────┐
│ 87 lịch hẹn tư vấn không có ghi âm — 7 ngày qua [✕] │
│ ───────────────────────────────────────────────────────────────────── │
│ [🔍 Tìm KH hoặc NV...] [Sort: Ngày giờ ▼] [Lọc CN ▼] │ ← Search + filter
│ │
│ ┌─────┬────────────────┬──────────────┬──────────┬─────────────┬──────┐│
│ │ STT │ KHÁCH HÀNG │ NV TƯ VẤN │ NGÀY GIỜ │ CHI NHÁNH │ ACT ││
│ ├─────┼────────────────┼──────────────┼──────────┼─────────────┼──────┤│
│ │ 1 │ NGUYỄN T. A │ Trần V. Hùng │ 14/05 │ Long Khánh │ [Mở] ││
│ │ 2 │ TRẦN T. B │ Trần V. Hùng │ 13/05 │ Long Khánh │ [Mở] ││
│ │ 3 │ LÊ V. C │ Trần V. Hùng │ 12/05 │ Long Khánh │ [Mở] ││
│ │ 4 │ PHẠM T. D │ Lam │ 11/05 │ Quận 1 │ [Mở] ││
│ │ 5 │ HỒ V. E │ Hiếu │ 10/05 │ Quận 3 │ [Mở] ││
│ │ ... │ (5 nữa trong trang này) ││
│ │ 10 │ ĐỖ V. J │ Mai │ 09/05 │ Long Khánh │ [Mở] ││
│ └─────┴────────────────┴──────────────┴──────────┴─────────────┴──────┘│
│ │
│ Top NV vi phạm: Trần V. Hùng (12 cases), Lam (8), Hiếu (7) │
│ Top CN vi phạm: Long Khánh (23), Quận 1 (15), Quận 3 (12) │
│ │
│ Hiển thị 1-10 / 87 | [Page size: 10 ▼] | ← 1 2 3 ... 9 → │
│ │
│ [Đóng] │ ← P0 KHÔNG có Export
└──────────────────────────────────────────────────────────────────────────┘
Hành vi:
- Phân trang server-side với offset/limit
- Page size: 10 (default), 20, 50
- Total count luôn hiển thị
- Numeric pagination smart truncation > 7 pages
- ← / → disabled state edge
- Search debounce 300ms → reset page 1
- Sort options: Ngày giờ mới/cũ; KH A-Z/Z-A; NV; CN
- [Mở] → tab mới /appointment/<id>
- Trạng thái rỗng: "✅ Tất cả lịch hẹn đều có ghi âm"
- P0 KHÔNG có button Export Excel (defer P2 theo DEC-014)B-VISUAL.5) Modal KH chờ tư vấn — 156 KH
┌──────────────────────────────────────────────────────────────────────────┐
│ ℹ 156 khách hàng có lịch hẹn nhưng chưa được tư vấn — 7 ngày qua [✕]│
│ ────────────────────────────────────────────────────────────────────── │
│ │
│ 💡 Những KH này đã đặt lịch nhưng KHÔNG có record được tạo. Có thể: │
│ • KH không đến (no-show) │
│ • NV chưa upload ghi âm (compliance gap) │
│ • Cuộc gọi không thành công │
│ │
│ Phân loại tự động (chỉ tham khảo) │
│ ┌────────────────────┬───────┐ │
│ │ Lý do (đoán) │ Số KH │ │
│ ├────────────────────┼───────┤ │
│ │ No-show │ 72 │ │
│ │ Compliance gap │ 53 │ │
│ │ Pending follow-up │ 31 │ │
│ └────────────────────┴───────┘ │
│ │
│ [🔍 Tìm KH/NV...] [Lọc CN ▼] [Lọc lý do ▼] [Sort ▼] │
│ │
│ ┌────┬──────────────────┬──────────────┬─────────────┬────────────────┐ │
│ │ # │ KH │ NV phụ trách │ Lịch hẹn │ HÀNH ĐỘNG │ │
│ ├────┼──────────────────┼──────────────┼─────────────┼────────────────┤ │
│ │ 1 │ NGUYỄN V. AN │ Lam │ 14/05 09:00 │ [Mở appt][KH] │ │
│ │ 2 │ TRẦN T. BÌNH │ Hiếu │ 13/05 14:30 │ [Mở appt][KH] │ │
│ │ 3 │ LÊ V. CƯỜNG │ Hùng │ 12/05 10:15 │ [Mở appt][KH] │ │
│ │ ...│ (7 nữa trong trang này) │ │
│ │ 10 │ ĐẶNG V. P │ Mai │ 09/05 16:00 │ [Mở appt][KH] │ │
│ └────┴──────────────────┴──────────────┴─────────────┴────────────────┘ │
│ │
│ [Mở appt] → tab mới /appointment/<id> (view-only) │
│ [KH] → tab mới /crm/customer/<id> (view-only) │
│ │
│ Hiển thị 1-10 / 156 | [Page size: 10 ▼] | ← 1 2 3 ... 16 → │
│ │
│ [Đóng] │ ← P0 KHÔNG có Export
└──────────────────────────────────────────────────────────────────────────┘
Hành vi: tương tự MissingRecordsModal + Filter lý do (3 options).
P0 KHÔNG có button Export Excel.B-VISUAL.6) Bảng pattern URL drill-down (chuẩn canonical)
Tất cả click chart segment → navigate /e/record với URL query params. RecordTable parse trong onMounted (xem dev-spec.md §C5.2).
| Trigger | Source widget | Navigate URL pattern | Filter applied trong RecordTable |
|---|---|---|---|
| Click bar NV "Nguyễn T. Lam" | Top Staff chart | /e/record?staffId=<acc_lam_uuid>&from=<YYYY-MM-DD>&to=<YYYY-MM-DD>&branchId=<scope> | created_by + date range + branch |
| Click 1 điểm dot ngày 12/05/2026 | Trend Chart | /e/record?dateExact=2026-05-12&branchId=<scope> | from = to = 12/05/2026 |
| Click ô T5 lúc 10h | Heatmap | /e/record?dateExact=2026-05-15&branchId=<scope> (chính xác đến NGÀY — DEC-006 P0 tradeoff) | date single ngày T5 gần nhất |
| Click bar "Long Khánh" | Branch Top chart | /e/record?branchId=<lk_uuid>&from=<>&to=<> | branch + date range |
Click row trong MissingRecordsModal [Mở] | Modal | /appointment/<id> (new tab) | (appointment detail page) |
Click row trong AnomalyMissingTVModal [Mở appt] | Modal | /appointment/<id> (new tab) | (appointment detail page) |
Click row [KH] | Modal | /crm/customer/<id> (new tab) | (customer detail page) |
Click [Xem tất cả 70 chi nhánh →] | Branch Top CTA | /e/record/insights/staff (P2 — defer) HOẶC /e/record?from=<>&to=<> (P0 fallback) | (full ranking page P2 hoặc list /e/record P0) |
Hành vi nút Quay lại trình duyệt: Quay lại dashboard với filter state preserved (qua URQL cache + URL state).
B-PRD) Liên kết FR → Màn hình
| FR | Section/Component | File |
|---|---|---|
| FR-001 KPI cards | RecordInsightKPIRow | Design doc §4.2.1 + §4.2.2 |
| FR-002 Trend Chart | RecordTrendChart | §4.2.1 + §4.2.3 |
| FR-003 Top Staff | RecordTopStaffChart | §4.2.1 |
| FR-004 Heatmap | RecordHourHeatmap | §4.2.1 + §4.2.4 |
| FR-005 Branch Top | RecordBranchTopChart | §4.2.1 |
| FR-006 Missing modal | MissingRecordsModal | §4.2.8 |
| FR-007 KH chờ TV modal | AnomalyMissingTVModal | §4.2.15.c |
| FR-008 Filter bar | RecordInsightFilterBar | §4.2.6 |
| FR-009 Audio drill-down | (drill-down navigation pattern) | §4.2.12 + §4.2.13/14/18 |
| FR-010 Permission v2 | (sidebar + button visibility) | §4.2.9 + §4.5 |
| FR-011 /e/record extend | RecordTable.tsx (modified) | §4.4.5 |
B1) Bản đồ màn hình (Screen Map)
[Sidebar "Insight Ghi Âm" entry — chỉ hiển thị với view_insight permission]
│
▼
SCR-01: /e/record/insights (RecordInsightPage)
│
├──► Click anomaly 🔴 Missing → SCR-02 MissingRecordsModal (overlay)
├──► Click anomaly 🟡 KH chờ TV → SCR-03 AnomalyMissingTVModal (overlay)
├──► Click Biểu đồ Top NV → /e/record?staffId=&from=&to=
├──► Click Trend point → /e/record?dateExact=
├──► Click Heatmap cell → /e/record?dateExact=
├──► Click Branch bar → /e/record?branchId=
└──► Click "📥 Xuất báo cáo ▾" (P2) → ExportDropdown
└──► Click "🔗 Sao chép link" (P0) → clipboard + toast
SCR-02: MissingRecordsModal (overlay)
│
├──► Click row [Mở] → /appointment/:id (new tab)
└──► Click [Xuất Excel] (P2) → download .xlsx
SCR-03: AnomalyMissingTVModal (overlay)
│
├──► Click row [Mở appt] → /appointment/:id (new tab)
├──► Click row [KH] → /crm/customer/:id (new tab)
└──► Click [Xuất Excel] (P2) → download .xlsxB2) Kiểm kê (mức Component)
Detailed mockups: design doc §4.2.1 đến §4.2.20.
B2.7E) Tương tác phụ (8 quy tắc)
| Tương tác | Trigger | Hành vi |
|---|---|---|
| Trạng thái đang tải | Mount lần đầu + filter thay đổi | Skeleton nhấp nháy 1.2 giây lặp |
| Trạng thái rỗng | Query trả 0 dòng | Thông báo riêng cho mỗi widget |
| Trạng thái lỗi | Mạng lỗi / 500 server | Banner đỏ nhạt + retry button |
| Chỉ báo dữ liệu cũ | Dữ liệu cache > 60 giây | Badge "Đang cập nhật..." |
| Di chuột hiện tooltip | ⓘ icon, chart segment | Hiện sau 300ms khi di chuột |
| Phím ESC | Modal open | Close modal |
| Nhấn ra ngoài | Dropdown open | Close dropdown (KHÔNG apply if "Áp dụng" required) |
| Nút quay lại trình duyệt | Drill-down /e/record | Return to dashboard with filter state preserved |
B2.8) Thay đổi cấp Field (cho RecordTable extend)
| Field/Filter | Trước (existing) | Sau (extended) | Phase |
|---|---|---|---|
| Phân tích URL query | Không sync URL | Parse branchId, from, to, staffId, customerId, durationGt, durationLt, dateExact khi mount | P0 |
| Lọc theo thời lượng | Không có | Thêm 2 input "Thời lượng từ" / "đến" trong advanced filter | P0 |
| Banner bất thường | Không có | Banner "Đang xem drill-down từ Insight" với button "Quay lại Insight" | P1 |
| Lọc theo giờ trong ngày | Không có | Optional dropdown "Giờ trong ngày" | P1 |
B4) Thông báo (KHÔNG ÁP DỤNG P0)
P0 không có notification. Schedule email báo cáo defer P2 (xem prd.md A5 FR-008 P2).
B5) Ma trận quyền (Permission Matrix)
B2.8) Ma trận quyền (canonical, enum phản hồi khi từ chối)
Denied feedback enum:
hidden(ẩn hoàn toàn) /disabled(hiển thị nhưng disable) /redirect(chuyển hướng tới forbidden page).
| Role | Sidebar entry | Button trên /e/record | Route guard | Branch scope | Denied feedback |
|---|---|---|---|---|---|
| BOD | ✅ Hiện | ✅ Hiện | ✅ Đạt | Toàn 70 CN | (N/A — có quyền) |
| ITLeader | ✅ Hiện | ✅ Hiện | ✅ Đạt | Toàn 70 CN | (N/A — có quyền) |
Admin (ROLE_MODERATOR) | ✅ Hiện | ✅ Hiện | ✅ Đạt (bypass) | All | (N/A — có quyền) |
| BranchManager | ❌ Ẩn | ❌ Ẩn | ❌ Không đạt → redirect /forbidden | (P2: own branches) | hidden (sidebar + button KHÔNG xuất hiện); nếu type URL → redirect /forbidden |
| Staff (NV) | ❌ Ẩn | ❌ Ẩn | ❌ Không đạt → redirect /forbidden | N/A | hidden (sidebar + button KHÔNG xuất hiện); nếu type URL → redirect /forbidden |
| POS / CRM portal | ❌ Ẩn | (N/A — không có /e/record) | ❌ Không đạt | N/A | hidden (entire module ẩn) |
Permission check pattern (canonical):
typescript
const canViewInsight = globalStore.hasPermission('voice_recording_management', 'view_insight');
// Render conditionally
v-if="canViewInsight"Route guard:
typescript
beforeEnter: (to, from, next) => {
if (!globalStore.hasPermission('voice_recording_management', 'view_insight')) {
return next({ name: 'forbidden' });
}
next();
}Hasura permission cho 3 materialized views (P0 — moved từ P1 sau code review B4):
- Role
userSELECT cho 3 views (record_daily_summary,_staff_summary,_hourly_summary) - Filter session-claim:
branch_id: { _in: X-Hasura-Allowed-Branches } - BE middleware pre-compute claim theo permission user (PD-011 — NEW P0)
B6) Ma trận trạng thái (theo widget)
| Widget | Mặc định | Đang tải | Rỗng | Lỗi | Không có quyền |
|---|---|---|---|---|---|
| RecordInsightFilterBar | render filter chips | (N/A — filter bar always available) | (N/A) | retry button | (N/A — route guard handles) |
| RecordInsightKPIRow | 5 numbers + delta | 5 skeleton cards | 5 cards với "0" | Banner đỏ + retry | (N/A) |
| RecordTrendChart | line chart with data | skeleton chart area | "Chưa đủ dữ liệu để vẽ xu hướng (cần ≥ 2 ngày)" | Banner riêng cho widget này + retry | (N/A) |
| RecordTopStaffChart | top 10 bars | skeleton bars | "Không có nhân viên tư vấn trong khoảng đã chọn" | Banner + retry | (N/A) |
| RecordHourHeatmap | grid với colors | skeleton grid | "Chưa đủ dữ liệu để vẽ heatmap" | Banner + retry | (N/A) |
| RecordBranchTopChart | top 10 + "Khác" | skeleton | "Không có dữ liệu chi nhánh" | Banner + retry | (N/A) |
| RecordAnomalyAlerts | 2 cards với count | skeleton cards | "✅ Không phát hiện bất thường" | Banner + retry | (N/A) |
| MissingRecordsModal | table với pagination | spinner trong modal body | "✅ Tất cả lịch hẹn đều có ghi âm" | Modal hiện lỗi + thử lại | (N/A) |
| AnomalyMissingTVModal | table tương tự | spinner | "✅ Không có KH chờ TV" | Modal error + retry | (N/A) |
B7) Nội dung hiển thị (UI Copy Contract)
B7.1) Tiêu đề trang
- Title: "Insight Ghi Âm"
- Subtitle: "Theo dõi mức độ tuân thủ ghi âm và hiệu suất tư vấn của toàn hệ thống"
B7.2) Nhãn KPI (chữ hoa, 12px, gray-600)
- CUỘC TƯ VẤN
- THỜI LƯỢNG TB
- NV HOẠT ĐỘNG
- TUÂN THỦ GHI ÂM
- KH CHỜ TV
B7.3) Chỉ báo delta
- Positive:
▲ X% so tuần trước(xanh lá #4CAF50) - Negative:
▼ X% so tuần trước(đỏ #E53935) - No change:
─ Không đổi(gray #9E9E9E) - New (previous = 0):
▲ Mới(xanh)
B7.4) Thông báo Rỗng / Lỗi / Đang tải
| Trạng thái | Copy |
|---|---|
| Rỗng tất cả widget | "Không có dữ liệu ghi âm trong khoảng đã chọn" |
| Rỗng biểu đồ trend | "Chưa đủ dữ liệu để vẽ xu hướng (cần ≥ 2 ngày)" |
| Rỗng top NV | "Không có nhân viên tư vấn trong khoảng đã chọn" |
| Rỗng heatmap | "Chưa đủ dữ liệu để vẽ heatmap" |
| Rỗng chi nhánh | "Không có dữ liệu chi nhánh" |
| Rỗng bất thường | "✅ Không phát hiện bất thường — Tất cả lịch hẹn đều được ghi âm đầy đủ" |
| Đang tải | "Đang tải..." |
| Lỗi mạng | "⚠ Không tải được dữ liệu — Connection timeout (8s). Các phần khác vẫn hiển thị bình thường." |
| Tải chậm > 3s | "Đang tải lâu — Mạng có thể chậm. Tiếp tục chờ hay [Thử lại]?" |
| Không có quyền | (Redirect — không có message inline) |
| Modal rỗng | "✅ Tất cả lịch hẹn đều có ghi âm" |
B7.5) Chân trang
- "Cập nhật lần cuối: DD/MM/YYYY HH:MM:SS | Tự động làm mới mỗi 60s"
- Stale badge khi cache > 60s: "Đang cập nhật..."
B8) Phân tích sự kiện (Analytics event tracking)
| Event name | Trigger | Properties |
|---|---|---|
record_insight_viewed | Page mount sau khi pass permission | user_id, role, branch_filter_count, time_range_preset |
record_insight_filter_changed | Filter dropdown apply | filter_type (branch/date/staff), value_count |
record_insight_chart_clicked | Click chart segment | chart_type (trend/top_staff/heatmap/branch), target_filter |
record_insight_anomaly_drilldown | Click anomaly CTA | anomaly_type (missing/awaiting), count |
record_insight_export_clicked | Click export (P2) | format (excel/pdf), scope |
record_insight_drilldown_navigated | Drill-down sang /e/record | from_widget, query_params |
record_insight_audio_played | Play audio sau drill-down (event trên /e/record) | record_id, source (insight_drilldown) |
B9) Từ điển Tooltip
| Màn hình | Trường/Icon | Nội dung tooltip | Điều kiện hiện |
|---|---|---|---|
| Insight Dashboard | KPI #1 ⓘ | "Tổng số ghi âm tư vấn đã tạo trong khoảng + chi nhánh đã chọn. Nguồn: COUNT(record) WHERE disabled=false" | Di chuột icon ⓘ |
| Insight Dashboard | KPI #2 ⓘ | "Thời lượng trung bình mỗi cuộc tư vấn (phút:giây). Công thức: AVG(EXTRACT(EPOCH FROM rf.duration))/60. Insight: TB quá ngắn (<5p) có thể NV chưa tư vấn đủ; quá dài (>30p) có thể KH có vấn đề phức tạp" | Di chuột icon ⓘ |
| Insight Dashboard | KPI #3 ⓘ | "Số nhân viên unique có ít nhất 1 cuộc tư vấn được ghi âm trong period. Nguồn: COUNT(DISTINCT record.created_by)" | Di chuột icon ⓘ |
| Insight Dashboard | KPI #4 ⓘ | "Tỷ lệ % cuộc tư vấn đã có ghi âm so với tổng số lịch hẹn loại 'Tư vấn dịch vụ' trong khoảng. Công thức: (appointment có record) / (tổng appt TV) × 100" | Di chuột icon ⓘ |
| Insight Dashboard | KPI #5 ⓘ | "Số khách hàng có lịch hẹn loại tư vấn nhưng chưa được tạo ghi âm. Đoán nguyên nhân: no-show / NV chưa upload / pending follow-up" | Di chuột icon ⓘ |
| Insight Dashboard | Trend chart ⓘ | "Số cuộc tư vấn theo ngày. Click 1 điểm để xem chi tiết trong /e/record" | Di chuột icon ⓘ |
| Insight Dashboard | Heatmap ⓘ | "Mật độ cuộc tư vấn theo giờ × thứ trong tuần. Tổng hợp qua các chi nhánh đã chọn. Click ô để drill-down ngày tương ứng" | Di chuột icon ⓘ |
| Insight Dashboard | Top Staff ⓘ | "Top 10 nhân viên có nhiều cuộc tư vấn nhất trong period. Click bar để xem chi tiết records của NV đó" | Di chuột icon ⓘ |
| Insight Dashboard | Branch Top ⓘ | "Top 10 chi nhánh có nhiều cuộc tư vấn nhất, các CN còn lại gộp 'Khác'. Tổng: 70 chi nhánh" | Di chuột icon ⓘ |
| Insight Dashboard | Footer "Cập nhật lần cuối" | "Thời điểm dashboard refresh data gần nhất. Tự động làm mới mỗi 60s khi tab active" | Di chuột label |
| Anomaly card 🔴 | Card title | "Compliance gap — appointment đã đến nhưng NV chưa upload audio. Cần BOD theo dõi tỷ lệ và delegate Manager xử lý" | Di chuột card |
| Anomaly card 🟡 | Card title | "Coverage gap — KH đặt lịch nhưng không có record. Có thể: no-show / compliance gap / pending follow-up" | Di chuột card |
| Thanh lọc | Branch chip "70 chi nhánh" | "BOD đang xem dữ liệu của toàn bộ 70 chi nhánh trong hệ thống. Click để thay đổi" | Di chuột chip |
| Thanh lọc | Date chip "7 ngày qua" | "Hôm nay + 6 ngày trước (rolling window)" | Di chuột chip |
| Modal Missing | Header dynamic count | "Số lịch hẹn loại tư vấn trong period mà không có record được tạo (compliance gap)" | Di chuột tiêu đề |
| Modal KH chờ TV | Header dynamic count | "Số KH có lịch hẹn loại tư vấn nhưng chưa được tạo ghi âm (coverage gap)" | Di chuột tiêu đề |
| Modal pagination | "Hiển thị 1-10 / N" | "Số rows đang hiển thị trên tổng số rows toàn dataset" | Di chuột label |
B-Desktop) Bố cục desktop (mặc định 1280px)
Mockup chi tiết: Design doc §4.2.1 (Full Desktop View).
Grid system:
- 12-column grid, 24px gutter
- KPI row: 5 cards × 2.4 columns (= 12)
- Charts row 1: 2 widgets × 6 columns
- Charts row 2: 2 widgets × 6 columns
- Anomaly section: full-width
- Container max-width: 1440px (responsive cap)
Breakpoint: Web-only, không có mobile P0. Tablet 768px → 2 columns layout (P2 optional).
B-EdgeCases) Trường hợp biên (12 nhóm)
| G | Edge case | Behavior |
|---|---|---|
| G1 — Data | Period rỗng (filter quá hẹp) | All widgets hiển thị empty state riêng |
| G1 | 1 ngày có data, 6 ngày empty | Trend chart hiển thị 1 dot + axis 7 days |
| G1 | Tất cả reference_file.duration IS NULL | KPI #2 hiển thị — + warning text "X cuộc không có metadata duration" |
| G2 — Network | Slow query > 8s | Banner per widget "Đang tải lâu" + retry; không phá toàn page |
| G2 | Network completely fail | Toast "Mất kết nối — Đang thử lại..." |
| G3 — Permission | User mất quyền giữa session | Next API call 403 → redirect /forbidden + toast "Quyền truy cập đã thay đổi" |
| G4 — Filter | Branch filter chọn 0 CN | Show empty state "Vui lòng chọn ít nhất 1 chi nhánh" |
| G4 | Date range > 365 ngày | Disable Apply button + tooltip "Tối đa 365 ngày" |
| G4 | Date "from" > "to" | Validation error trên picker |
| G5 — Concurrency | 2 tabs cùng filter khác → URL conflict | Mỗi tab độc lập, không sync state |
| G6 — Volume | 156 records trong modal Missing | Pagination 10/page → 16 pages |
| G6 | 0 records | Trạng thái rỗng hiển thị |
| G7 — Audio | File audio bị xóa (404 khi play) | Player toast "File audio không khả dụng. Có thể đã hết hạn lưu trữ" |
| G8 — Pagination | Click page out-of-range | Auto clamp to last valid page |
| G9 — Filter persistence | Reload page với URL query params | RecordTable parse và apply filters |
| G10 — Compare mode | (Defer P1) | — |
| G11 — Export (P2) | Click export khi loading | Disable button + tooltip "Đang tải data..." |
| G12 — Backwards compat | User cũ vào URL /e/record không thay đổi behavior | RecordTable hoạt động như cũ nếu không có URL query params |
B-i18n) Quốc tế hóa (i18n)
- Tiếng Việt là ngôn ngữ chính (như Diva hiện hữu)
- Date format:
DD/MM/YYYYtrong UI;YYYY-MM-DDtrong URL query - Number format:
1,234.56(period decimal, comma thousand) - Currency: KHÔNG áp dụng (feature không liên quan tiền)
- Timezone:
Asia/Ho_Chi_Minhcho tất cả tính toán date_trunc
B-Microcopy) Quy ước microcopy
- Câu ngắn: chủ ngữ + động từ + đối tượng. VD: "Click 1 điểm → drill-down
/e/record" - Avoid passive voice: "Filter được áp dụng" → "Áp dụng filter"
- Avoid technical jargon trên label: "duration_sec" → "Thời lượng" (UI); để tooltip chứa technical context
- Số có đơn vị: "124 cuộc", "32h 15m", "94%", "70 chi nhánh"
- Plural / Singular: tiếng Việt không phân biệt → dùng số trước noun: "1 cuộc / 2 cuộc / 156 KH"
B-Voice) Giọng và tone
- Tone: Chuyên nghiệp executive, không thân mật. Mục tiêu: BOD thấy thông tin nghiêm túc, data-driven.
- Avoid: emoji thái quá (chỉ dùng 🔴🟡 cho anomaly level + ▲▼─ cho delta), exclamation mark "!"
- Encourage: rõ ràng, số liệu cụ thể, actionable insight trong tooltip
B-Versioning) Quy ước version UI
- Footer hiển thị version build (P2): "Insight Dashboard v1.0.0"
- Khi BE schema thay đổi (vd thêm field) → bump minor version + show "Mới có" badge trên field
B-Help) Trợ giúp & Tài liệu
- ⓘ tooltips cho mọi KPI + chart (B9)
- "?" icon ở header → link tới docs (P2)
- Nội dung trạng thái rỗng gợi ý user thao tác ("Mở rộng khoảng thời gian" / "Xem tất cả chi nhánh")
B-POST) Kiểm chứng sau implement
| # | Verify item | Method |
|---|---|---|
| 1 | All 11 FR có UI component tương ứng | Cross-check prd.md A11 traceability |
| 2 | Tất cả permission checks dùng hasPermission(module, action) v2 | grep hasPermissions([UserRole → 0 results |
| 3 | Drill-down URL pattern consistent | Test 5 drill-down paths với staging |
| 4 | Pagination behavior (search/filter reset page 1; P0 KHÔNG có Export button trong modal) | Manual test |
B-QUALITY) Sổ rủi ro UI (30+ risks)
Chi tiết trong design doc §4.6 Xử lý lỗi + §4.11 Hiệu năng. Top 10 rủi ro:
| # | Rủi ro | Giảm thiểu |
|---|---|---|
| 1 | TTFMP > 3s với 30 ngày × 70 CN | Tier 1 indexes mandatory + URQL stale-while-revalidate (DEC-012) |
| 2 | BOD không nhận diện được anomaly critical | Color đỏ #E53935 + icon 🔴 + đặt cuối page (high attention) |
| 3 | Filter dropdown 70 CN quá dài | Search realtime + group region + bulk select (DEC-009) |
| 4 | Staff dropdown 700 NV crash page | Search-only mode + lazy load 10/lần (DEC-009) |
| 5 | Branch pie 70 slices không readable | Đổi sang Bar top 10 + "Khác" (DEC-008) |
| 6 | Modal list dài (>50 rows) không pagination | Pagination 10/20/50 (DEC-013) |
| 7 | User mất context khi drill-down /e/record | Anomaly banner P1 + browser back giữ state |
| 8 | Audio file 404 (đã hết hạn) | Toast "File hết hạn lưu trữ" graceful |
| 9 | Permission denied giữa session | Toast + redirect, không crash UI |
| 10 | Date range > 365 ngày | Disable Apply + tooltip warning |
| ... 20+ more | Xem design doc §4.6 + §4.11 | — |
Tham chiếu chéo:
- PRD:
./prd.md- Source of Truth:
./SOURCE_OF_TRUTH.md- Dev Spec:
./dev-spec.md(next)- Design doc gốc:
../../superpowers/specs/2026-05-15-record-bod-insight-design.md