Appearance
Tài liệu yêu cầu sản phẩm (PRD) — Insight Ghi Âm cho BOD
Phiên bản: 1.0 Ngày: 15/05/2026 Tác giả: PO/BA (NGUYỄN SƠN THỌ, IT Leader) Trạng thái: Sẵn sàng review Mục đích: Hợp đồng nghiệp vụ — scope, DEC, FR/AC, formulas, rủi ro. KHÔNG chứa RACI/timeline (xem handoff.md).
Đọc trước:
decision-brief.md(cửa vào) →SOURCE_OF_TRUTH.md(truth canonical) → file này. Mọi quyết định nghiệp vụ phải tham chiếu Decision Log (Z section) trước khi diễn giải FR.
Z) Nhật ký quyết định (Decision Log)
15 quyết định canonical đã khóa qua 4 vòng iteration design. Mọi FR/UI/Dev/QA phải nhất quán với Z section. Conflict → check
SOURCE_OF_TRUTH.mdthắng.
Z1) Quyết định nghiệp vụ (Business decisions)
| ID | Quyết định | Lý do | Tác động |
|---|---|---|---|
| DEC-001 | Tracking-only stance — Dashboard chỉ xem + nghe + drill-down + export. KHÔNG có action: gọi điện, gửi SMS, gửi nhắc nhở, tạo task, ghi chú audit, đánh dấu trạng thái, xóa file | BOD chỉ giám sát; xử lý NV/KH thuộc Manager/HR qua channel khác | Loại bỏ ~10 action features, giảm scope 65% so design v1 |
| DEC-004 | 5 KPI cards với KPI #2 = "Thời lượng TB / cuộc" (KHÔNG phải "Tổng thời lượng") | Thời lượng TB là quality indicator hơn raw sum | 5 KPI: Cuộc TV, Thời lượng TB, NV hoạt động, Tuân thủ ghi âm, KH chờ TV |
| DEC-005 | Anomaly chỉ 2 mức — 🔴 Missing records + 🟡 KH chờ TV. Loại bỏ Long calls (>60p) + Short calls (<1p) | Long/Short calls là quality control thuộc Manager scope; BOD không micromanage chất lượng từng cuộc | Bỏ 2 anomaly cards + 2 modal liên quan (AnomalyLongCallModal, AnomalyShortCallModal) |
| DEC-015 | Time range default 7 ngày qua, max hard limit 365 ngày. Auto-poll 60s khi tab active, pause khi background, disable khi range > 30 ngày | Cân bằng UX freshness vs server load | URQL pollInterval logic |
Z2) Quyết định UX (User Experience decisions)
| ID | Quyết định | Lý do | Tác động |
|---|---|---|---|
| DEC-002 | Route mới /e/record/insights với role-based guard. KHÔNG modify route /e/record hiện hữu (chỉ extend filter) | Tách rõ insight (BOD) vs list (NV); không break user existing | Sidebar entry "Insight Ghi Âm" + button [📊 Insight →] trên /e/record cho role phù hợp |
| DEC-006 | Drill-down navigate /e/record cho mọi click chart (Top Staff bar, Trend point, Heatmap cell, Branch slice). KHÔNG mở modal phức tạp | Tận dụng list view + player có sẵn ở /e/record; tránh build 5 modal analytics rich | Chỉ giữ 2 modal: MissingRecords + AnomalyMissingTV (case không reuse /e/record được) |
| DEC-008 | Branch chart = horizontal bar top 10 + "Khác" gộp (KHÔNG pie 70 slices) | 70 slices trên pie không readable | Reuse Chart.js BarChart wrapper |
| DEC-009 | Filter dropdown — Branch: search realtime + group by region (nếu có) + bulk select. Staff: search-only + lazy load (không render 700 NV upfront) | Scale 70 CN × 700 NV | Region grouping cần BE confirm (PD-001) |
| DEC-013 | Pagination chuẩn cho modal có list dài — server-side, 10/20/50 rows/page, total count, search/filter reset page 1. P0 KHÔNG có Export Excel (defer P2 theo DEC-014). | Scale có thể từ vài chục đến vài trăm rows | Apply 2 modal P0 + Full Staff Ranking P2 + /e/record |
Z3) Quyết định kỹ thuật (Technical decisions)
| ID | Quyết định | Lý do | Tác động |
|---|---|---|---|
| DEC-003 | Reuse RecordForm.tsx HTML5 audio player. KHÔNG build AudioPlayerCompact/Inline/FullModal mới | Component đã production, props rõ ràng (modelValue: { url, name, mime_type }); tránh duplicate code | Tiết kiệm 3 components mới + waveform lib + audio store |
| DEC-010 | Permission Dynamic v2 — action view_insight mới cho module voice_recording_management. Seed cho BOD + ITLeader qua schema v2: INSERT vào module_permission_action + array_append(role_module.actions, 'view_insight'). Cập nhật FE ALL_PERMISSION_ACTIONS trong permissions.ts. Dùng hasPermission(module, action) API | Tách quyền dashboard insight khỏi quyền list records; tuân thủ Dynamic Permission v2 chuẩn Diva | Verified migration 1761808767073_perm_v2: module_permission_action table + role_module.actions TEXT[] array. KHÔNG dùng schema cũ |
| DEC-011 | Schema chuẩn — public.record, public.appointment, public.reference_file (KHÔNG ecommerce.*). Join rf.reference_id = r.id (KHÔNG có entity_type). Duration extract qua EXTRACT(EPOCH FROM rf.duration) | Verified trên migrations thật; tránh schema gotcha | Tất cả SQL view, GraphQL query phải tuân thủ |
| DEC-012 | Performance P0 mandatory — 4 indexes Tier 1 (3 B-tree compound + 1 BRIN) + Hasura @cached(ttl:60) + URQL stale-while-revalidate + 3 materialized views P0 (record_daily_summary, _staff_summary, _hourly_summary — moved từ P1 sau code review B5; inline aggregation fetch 105k rows sẽ fail TTFMP target). Refresh schedule mỗi 15 phút | Scale 105k records 30 ngày × 70 CN với TTFMP target 2-3s | Migration indexes + materialized views + benchmark gate trước go-live |
| DEC-007 | Comparison mode defer P1 — P0 chỉ delta WoW trên KPI card | Implementation phức tạp (2x query + overlay) | P0 ship lean; P1 build full comparison overlay nếu BOD cần |
| DEC-014 | Export defer P2 — Excel/PDF + schedule email defer P2. P0 chỉ "Copy link dashboard" | Cheap P0; export tech stack cần BE assess (PD-009) | Defer 3 modals + 1 scheduler + 1 PDF action |
Z4) Quyết định QA (Quality assurance decisions)
| ID | Quyết định | Lý do | Tác động |
|---|---|---|---|
| Z4-001 | Performance gate trước go-live P0 — TTFMP < 2s với 7 ngày × 70 CN, < 3s với 30 ngày | DEC-012 mandatory | Staging benchmark + EXPLAIN ANALYZE không có Seq Scan |
| Z4-002 | Permission test — BOD/ITLeader có quyền; Staff truy cập URL trực tiếp phải 403 | DEC-010 enforce | Cypress E2E test |
| Z4-003 | Audio playback — drill-down /e/record → click play → audio stream < 1s | DEC-003 reuse RecordForm | Manual UI test |
| Z4-004 | Pagination correctness — page navigation, total count, search/filter reset page 1. P0 KHÔNG có Export Excel (defer P2 theo DEC-014; nếu P2 build sẽ Export = TOÀN BỘ) | DEC-013 + DEC-014 | Integration test |
A) PRD (Product Requirements Document)
A1) Blueprint chức năng
┌────────────────────────────────────────────────────────────────────┐
│ /e/record/insights — BOD Insight Dashboard │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Filter Bar: Branch (70 CN) | Date | Staff (700) | Refresh │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 5 KPI Cards ──────────────────────────────────────────────────┐│
│ │ Cuộc TV | Thời lượng TB | NV active | Tuân thủ % | KH chờ TV ││
│ │ (+ delta WoW indicator) ││
│ └────────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Trend (số cuộc/ngày) ─┐ ┌─ Top 10 NV bar ───────────────────┐ │
│ │ single line, click → drill │ │ click → drill /e/record?staffId│ │
│ └────────────────────────────┘ └─────────────────────────────────┘│
│ │
│ ┌─ Heatmap (giờ × thứ) ──┐ ┌─ Top 10 CN bar + "Khác" ─────────┐ │
│ │ aggregate 70 CN │ │ click → drill /e/record?branchId│ │
│ │ click → drill /e/record │ │ │ │
│ └──────────────────────────┘ └─────────────────────────────────┘│
│ │
│ ┌─ Anomaly Alerts (2 cards) ────────────────────────────────────┐ │
│ │ 🔴 X lịch hẹn thiếu record → MissingRecordsModal │ │
│ │ 🟡 Y KH chờ TV → AnomalyMissingTVModal │ │
│ └────────────────────────────────────────────────────────────────┘│
│ │
│ Footer: Cập nhật lần cuối ... | Tự động làm mới 60s │
└────────────────────────────────────────────────────────────────────┘A2) Bối cảnh (Context)
Trang /e/record hiện tại đáp ứng nhu cầu thao tác file ghi âm cho NV/Manager. Diva có 70 chi nhánh active với ~700 NV tư vấn, ~3.500 records/ngày, ~105.000 records/30 ngày. BOD (Board of Directors) hiện không có view tổng quan executive về compliance recording + coverage tư vấn. Phải gọi Manager hỏi từng CN → không scale.
A3) Mục tiêu (Goals)
| Mục tiêu | Đo lường |
|---|---|
| BOD nắm tình hình ghi âm toàn 70 CN trong < 5 giây sau khi mở dashboard | Time-to-comprehension test với 3 BOD users |
| Phát hiện compliance gap (lịch hẹn thiếu record) ngay khi xảy ra | Anomaly section hiển thị critical alert đỏ |
| Phát hiện coverage gap (KH chờ TV) để BOD delegate xử lý | Anomaly section hiển thị info alert |
Hỗ trợ drill-down chi tiết qua /e/record để verify | Click chart → navigate URL params correct |
| Performance load TTFMP < 2s/7d, < 3s/30d với 70 CN | Staging benchmark trước go-live |
A4) Personas
| Persona | Mô tả | Quyền action P0 | Branch scope |
|---|---|---|---|
| BOD (NGUYỄN SƠN THỌ + executive team) | Board of Directors / CEO / CFO — quyết định chiến lược | access, view_all, view_insight | All 70 |
| IT Leader | Quản lý infrastructure | access, view_all, view_insight, delete | All 70 |
Admin (ROLE_MODERATOR) | System admin bypass tất cả checks | All actions tự động | All |
| Branch Manager | Quản lý 1-N chi nhánh | access, view_all (?), KHÔNG view_insight P0 | Own branches |
| Staff (NV) | NV tư vấn — chỉ thao tác list records | access only | Self |
A5) Yêu cầu chức năng + tiêu chí chấp nhận (FR/AC)
FR-001: Dashboard Page với 5 KPI cards
Mô tả: Khi user có quyền voice_recording_management:view_insight truy cập route /e/record/insights, hệ thống hiển thị dashboard với header "Insight Ghi Âm" + filter bar + 5 KPI cards.
5 KPI cards:
| # | Tên | Công thức | Đơn vị | Delta indicator |
|---|---|---|---|---|
| 1 | CUỘC TƯ VẤN | COUNT(record) WHERE disabled=false AND in filter | Số | WoW % (so tuần trước) |
| 2 | THỜI LƯỢNG TB | AVG(EXTRACT(EPOCH FROM rf.duration)) / 60 | Phút:giây hoặc giờ:phút | WoW % |
| 3 | NV HOẠT ĐỘNG | COUNT(DISTINCT record.created_by) | Số NV | WoW absolute (+/-) |
| 4 | TUÂN THỦ GHI ÂM | (appointment có record) / (tổng appointment loại tư vấn) (PD-003) | % | WoW điểm phần trăm |
| 5 | KH CHỜ TV | COUNT(appointment_không_có_record WHERE type=tư vấn) | Số KH | ⚠ Cần xem (warning indicator) |
AC:
- AC-001-1: Khi page load lần đầu trong < 2s, 5 KPI cards hiển thị skeleton loading rồi render số liệu
- AC-001-2: Khi filter change, KPI cards re-fetch và update với loading state intermediate
- AC-001-3: Delta indicator hiển thị ▲green/▼red/─gray với % và label "so tuần trước"
- AC-001-4: Hover ⓘ icon → tooltip giải thích công thức + sample con số (xem ui-spec B9)
- AC-001-5: KPI #4 "Tuân thủ ghi âm" hiển thị
—nếu mẫu số = 0 (không có appointment loại tư vấn) - AC-001-6: KPI #2 "Thời lượng TB" hiển thị warning "X cuộc không có metadata duration" nếu có records bị NULL
FR-002: Trend Chart (số cuộc theo ngày)
Mô tả: Single-line chart hiển thị số cuộc tư vấn theo ngày trong period đã filter.
AC:
- AC-002-1: Y-axis = số cuộc; X-axis = ngày trong khoảng filter
- AC-002-2: Hover 1 điểm → tooltip "Thứ X, DD/MM/YYYY: N cuộc"
- AC-002-3: Click 1 điểm → navigate
/e/record?dateExact=YYYY-MM-DD&branchId=<scope> - AC-002-4: Empty state khi < 2 ngày có data: "Chưa đủ dữ liệu để vẽ xu hướng"
- AC-002-5: Time range default 7 ngày; max 365 ngày
FR-003: Top 10 Nhân viên bar chart
Mô tả: Horizontal bar chart top 10 NV theo số cuộc, sorted desc.
AC:
- AC-003-1: Bar dài tỷ lệ với số cuộc; hiển thị số cuộc bên cạnh
- AC-003-2: Click bar → navigate
/e/record?staffId=<acc_uuid>&from=<>&to=<>&branchId=<scope> - AC-003-3: Empty state khi không có data: "Không có nhân viên tư vấn trong khoảng đã chọn"
- AC-003-4: Footer "Click vào tên → drill /e/record" hint
FR-004: Heatmap giờ × thứ trong tuần
Mô tả: Grid 24h × 7 weekday hiển thị mật độ cuộc tư vấn. Aggregate qua các CN đã filter.
AC:
- AC-004-1: Cell color theo amber scale: 0 (gray), 1-3 (amber-100), 4-7 (amber-500), 8+ (amber-900)
- AC-004-2: Hover cell → tooltip "Thứ X lúc Hh: N cuộc, TB Y phút"
- AC-004-3: Click cell → navigate
/e/record?dateExact=<recent_matching_date>&branchId=<scope>(chính xác đến NGÀY, không đến GIỜ — DEC-006 tradeoff) - AC-004-4: Auto-hide giờ không có hoạt động (vd ban đêm 22h-7h)
- AC-004-5: Legend "Ít / Nhiều" với color scale
FR-005: Top 10 Chi nhánh bar + "Khác" gộp
Mô tả: Horizontal bar chart top 10 CN theo số cuộc; CN xếp sau gộp thành "Khác (N CN)".
AC:
- AC-005-1: Bar hiển thị số cuộc + % tổng (vd "Long Khánh 156 (28%)")
- AC-005-2: Row "Khác (60 CN)" gộp tất cả CN ngoài top 10
- AC-005-3: Click bar → navigate
/e/record?branchId=<branch_uuid>&from=<>&to=<> - AC-005-4: CTA "[Xem tất cả 70 chi nhánh →]" defer P2 (link tới full ranking view)
FR-006: Anomaly — Missing Records (lịch hẹn thiếu ghi âm) → Modal
Mô tả: Card 🔴 critical hiển thị số lịch hẹn tư vấn KHÔNG có record được tạo. Click CTA → mở modal pagination.
AC:
- AC-006-1: Card hiển thị số count + summary "Cao nhất: NV X (N cases) — Chi nhánh Y"
- AC-006-2: Click
[Xem N lịch hẹn →]→ mởMissingRecordsModalvới pagination - AC-006-3: Modal có: title dynamic số count, search box, sort dropdown, filter CN dropdown, table 10 rows/page, total count, page selector. Footer chỉ có
[Đóng]— KHÔNG có Export Excel ở P0 (defer P2, chốt theo DEC-014) - AC-006-4: Empty state: "✅ Tất cả lịch hẹn đều được ghi âm đầy đủ"
- AC-006-5: Baseline đếm phụ thuộc PD-003 — phải align với BE team trước implement
FR-007: Anomaly — KH chờ TV → Modal
Mô tả: Card 🟡 info hiển thị số KH có lịch hẹn loại tư vấn nhưng chưa có record.
AC:
- AC-007-1: Card hiển thị số count + section "Phân loại tự động" (no-show / compliance gap / pending follow-up)
- AC-007-2: Click
[Xem danh sách N KH →]→ mởAnomalyMissingTVModalvới pagination tương tự - AC-007-3: Modal có thêm filter "Lọc lý do" (3 options phân loại)
- AC-007-4: Row action:
[Mở appt](tab mới appointment detail) +[KH](tab mới customer detail)
FR-008: Filter Bar (70 CN + 700 NV scale)
Mô tả: Filter bar với 3 dropdown — Chi nhánh, Khoảng thời gian, Nhân viên — và button làm mới.
AC:
- AC-008-1: Branch dropdown: chip compact ("Long Khánh +2" / "70 chi nhánh") + search realtime + group by region (nếu BE có column region — PD-001) + bulk action "Chọn tất cả" / "Bỏ chọn tất cả"
- AC-008-2: Date range dropdown: preset (Hôm nay/Hôm qua/7 ngày/30 ngày/Tháng này/Tháng trước/Quý này/Tùy chỉnh) + custom date picker
- AC-008-3: Staff dropdown: search-only mode (KHÔNG render 700 NV upfront), debounce 300ms, lazy load 10 results/lần, quick filter "Top 10 NV active 7 ngày qua"
- AC-008-4: Button "[🔄 Làm mới]" → force re-fetch tất cả query bypassing cache
- AC-008-5: Filter change → reset pagination về page 1 ở tất cả widget có pagination
- AC-008-6: Default scope: BOD/Admin/ITLeader có
view_all→ "70 chi nhánh"; BranchManager/Staff → branch của họ
FR-009: Audio playback qua drill-down /e/record
Mô tả: Khi BOD muốn nghe sample audio, drill-down sang /e/record để dùng player có sẵn (RecordForm.tsx).
AC:
- AC-009-1: Mọi click chart segment (Top Staff bar, Trend point, Heatmap cell, Branch slice) → navigate
/e/recordvới query params phù hợp - AC-009-2:
/e/recordparse query params trongonMounted, apply vàoselectedref - AC-009-3: Player HTML5 hiện hữu hoạt động không thay đổi
- AC-009-4: Browser back → quay lại dashboard với state filter giữ nguyên (qua URQL cache + URL state)
FR-010: Permission v2 — action view_insight
Mô tả: Tách quyền xem dashboard insight khỏi quyền list records hiện hữu.
AC:
- AC-010-1: Sidebar entry "Insight Ghi Âm" CHỈ hiển thị khi
hasPermission('voice_recording_management', 'view_insight')returns true - AC-010-2: Button
[📊 Insight →]trên/e/recordCHỈ hiển thị với điều kiện như trên - AC-010-3: Route
/e/record/insightscóbeforeEnterguard — fail thì redirectforbidden - AC-010-4: Migration seed schema v2: INSERT row mới
(module_id='voice_recording_management', action='view_insight')vàomodule_permission_actioncatalog +array_append(role_module.actions, 'view_insight')cho role BOD + ITLeader; FE updateALL_PERMISSION_ACTIONStrongpermissions.ts - AC-010-5: Admin (
ROLE_MODERATOR) bypass tất cả (theouseGlobalStore.ts:169-181)
FR-011: Extend /e/record — URL query params + 2 filter mới
Mô tả: RecordTable parse URL query params khi mount + thêm 2 filter mới (durationGt/Lt) cho drill-down anomaly tương lai.
AC:
- AC-011-1: Khi mount,
RecordTableđọcuseRoute().queryvà apply:branchId,from,to,staffId,customerId,durationGt,durationLt,dateExact - AC-011-2: Nếu
dateExactcó → setfrom = dateExact, to = dateExact(overrides range) - AC-011-3: Filter
durationGt+durationLtextend GraphQL query vớifiles.duration { _gt/_lt }(thời gian theo PostgreSQLtimetype) - AC-011-4: KHÔNG break existing behavior — nếu không có query params, RecordTable hoạt động như cũ
A6) Giả định (Assumptions)
| Giả định | Tác động nếu sai |
|---|---|
| 70 chi nhánh là số chính xác và stable trong 6 tháng tới | Nếu scale lên 150+ CN, có thể cần re-design filter dropdown |
| ~3.500 records/ngày là estimate hợp lý | Nếu thực tế 10.000+/ngày, materialized view refresh 15p có thể chậm → escalate Tier 4 daily snapshot (P2) sớm hơn |
reference_file.duration được populate đủ cho records mới | Nếu NULL nhiều → metric "Thời lượng TB" skewed (PD-004) |
appointment."from" field exists và đáng tin cậy | Nếu field khác (vd scheduled_at) → SQL view phải update |
BE chấp nhận seed action view_insight theo pattern hiện hữu | Nếu BE muốn rename → update tất cả AC + migration |
A7) Rủi ro & giảm thiểu (Risks & Mitigations)
| Rủi ro | Mức | Giảm thiểu |
|---|---|---|
| Performance TTFMP > 3s với 30 ngày × 70 CN | 🟠 | Tier 1 indexes mandatory P0 (DEC-012); benchmark staging trước go-live (PD-008) |
| Appointment baseline cho "Tuân thủ" chưa rõ (PD-003) | 🟠 | BLOCK FR-006 query cho đến khi PO/BE align; fallback temporary all appointments |
| Duration NULL nhiều (PD-004) | 🟡 | View aggregation expose records_without_duration; UI warning "X cuộc không tính được"; backfill script nếu > 5% |
| JWT claim size 70 branches (PD-006) | 🟡 | Test JWT size < 8KB; optimize bằng view_all flag nếu cần |
| Materialized view refresh fail (P0 monitor) | 🟡 | Sentry alert nếu refresh fail 3 lần liên tiếp; manual REFRESH MATERIALIZED VIEW qua admin. Stale data tối đa 15 phút — acceptable cho tracking dashboard. KHÔNG fallback inline (sẽ fail TTFMP). |
JWT claim x-hasura-allowed-branches chưa có code Diva (PD-011) | 🔴 | BE middleware implement P0 mandatory trước FE deploy. Test JWT decode 3 role; verify Hasura filter _in: [] cho Staff trả 0 rows |
| Region grouping (PD-001) chưa có | 🟢 | Fallback flat list 70 CN với search realtime; OK cho P0 |
| Audio retention policy (PD-002) | 🟢 | UI handle "File hết hạn lưu trữ" gracefully |
A8) Metrics đo lường thành công
| Metric | Target | Cách đo |
|---|---|---|
| BOD adoption (active users) | ≥ 3/5 BOD members weekly trong tháng đầu | Analytics event record_insight_viewed |
| TTFMP performance | < 2s với 7d, < 3s với 30d | Sentry transaction tracking |
| Compliance gap detection rate | BOD phát hiện gap trong < 24h sau khi xảy ra | Anomaly card hiển thị + BOD click rate |
| Drill-down usage | ≥ 50% session có ít nhất 1 drill-down /e/record | Analytics event record_drilldown_clicked |
| Permission setup | 100% BOD + ITLeader được seed view_insight trước go-live | Migration audit log |
A9) Bảng thuật ngữ (Glossary)
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Ghi âm tư vấn | Voice consultation recording (Record) | Entity public.record chứa metadata + FK đến audio file | ≠ Ghi âm cuộc gọi (telephony) |
| Insight Ghi Âm | Record Insight Dashboard | Tên feature canonical | ≠ "BOD Dashboard" / "Recording Analytics" |
| Tuân thủ ghi âm | Recording compliance | Tỷ lệ % appointment loại tư vấn có ghi âm vs tổng appointment loại tư vấn trong period | ≠ Coverage (KH đã được TV) |
| KH chờ TV | Customers awaiting consultation | KH có lịch hẹn loại tư vấn nhưng KHÔNG có record được tạo | ≠ "KH chưa được phục vụ" |
Drill-down /e/record | Drill-down to record list page | Navigate sang /e/record với URL query params filter pre-applied | ≠ "Mở modal" |
| Tracking-only | Tracking-only stance | Dashboard chỉ xem + nghe + drill-down + export; KHÔNG action management | — |
| Audit | Audit (verb — kiểm tra) | "Cần xem xét cuộc tư vấn" — verb chung | ≠ "Audit notes" (đã bị bỏ DEC-001) |
| Drill-down navigate | Drill-down navigation pattern | Click chart → navigate /e/record với query params; KHÔNG mở modal | ≠ Modal drill-down |
| Cuộc tư vấn | Consultation call / Voice consultation session | 1 cuộc gặp/gọi giữa NV và KH có ghi âm | — |
| Period | Khoảng thời gian filter | Date range được user chọn (default 7 ngày) | — |
| Compliance gap | Compliance gap (lịch hẹn thiếu record) | Appointment loại tư vấn không có record được tạo | ≠ Coverage gap (KH chờ TV) |
| Coverage gap | Coverage gap (KH chưa được TV) | KH có lịch nhưng không có record (lý do: no-show / compliance gap / pending) | ≠ Compliance gap |
A10) Công thức kinh doanh (Business formulas — single source)
Mọi công thức nghiệp vụ canonical ở đây. Dev-spec C3 chỉ ghi SQL implementation delta, KHÔNG duplicate định nghĩa nghiệp vụ.
FORMULA-001: Số cuộc tư vấn
- Mô tả: Tổng số ghi âm tư vấn đã tạo trong period × scope branch
- Công thức:
count = COUNT(record) WHERE record.disabled = false AND appointment.branch_id IN $branchIds AND record.created_at >= $from AND record.created_at < $to + 1 day - Đơn vị: Số (integer)
- Ví dụ: Period 7 ngày × 70 CN có 124 records active → count = 124
- Edge cases:
- $branchIds rỗng → trả về 0
- Period > 365 ngày → cap hard limit 365
FORMULA-002: Thời lượng trung bình / cuộc
- Mô tả: Thời lượng audio trung bình mỗi cuộc tư vấn
- Công thức:
avg_duration_sec = AVG(EXTRACT(EPOCH FROM rf.duration)) WHERE rf.duration IS NOT NULL AND record matches scope - Đơn vị: Giây → format "Xp Ys" hoặc "Xh Ym"
- Biến số:
rf.duration: column trực tiếppublic.reference_file.duration(PostgreSQLtimetype, HH:MM:SS)
- Ví dụ: 124 cuộc với tổng duration 1.935.000 giây (32h 15m) / 124 = 15.605 giây ≈ 15p 36s
- Edge cases:
- Tất cả records có
duration IS NULL→ hiển thị "—" + warning - Records có duration NULL được EXCLUDE khỏi AVG, không tính 0
- Tất cả records có
FORMULA-003: NV hoạt động
- Mô tả: Số nhân viên unique có ít nhất 1 cuộc tư vấn trong period × scope
- Công thức:
active_staff = COUNT(DISTINCT record.created_by) WHERE record matches scope - Đơn vị: Số NV
- Ví dụ: Period 7 ngày × 70 CN có 124 records từ 8 NV unique → active = 8
FORMULA-004: Tỷ lệ tuân thủ ghi âm
- Mô tả: % appointment loại tư vấn có ghi âm vs tổng appointment loại tư vấn
- Công thức:
compliance_rate = ( COUNT(appointment có record) / COUNT(appointment loại "tư vấn" trong period × scope) ) × 100 - Đơn vị: % (2 decimal)
- Biến số:
- "appointment có record": JOIN appointment với record qua
appointment_id, có ≥ 1 record không disabled - "appointment loại tư vấn": baseline cần PD-003 — tạm thời all appointments có
"from" < now()(columnfromtrongpublic.appointment, là PostgreSQL reserved keyword nên phải quote)
- "appointment có record": JOIN appointment với record qua
- Ví dụ: Period có 50 appointment loại tư vấn, 47 có record → 47/50 × 100 = 94.00%
- Edge cases:
- Mẫu số = 0 (không có appointment loại TV) → hiển thị "—", không 0%/NaN%
FORMULA-005: KH chờ TV
- Mô tả: Số khách hàng có lịch hẹn loại tư vấn trong period nhưng chưa được tạo ghi âm
- Công thức (schema correction):
awaiting_count = COUNT(DISTINCT au.user_id) FROM appointment_user au JOIN appointment a ON a.id = au.appointment_id WHERE a."from" >= $from AND a."from" < $to + 1 day AND a.branch_id IN $branchIds AND a loại "tư vấn" (PD-003) AND NOT EXISTS (record r WHERE r.appointment_id = a.id AND r.disabled = false)Schema note:
appointmentKHÔNG có columncustomer_iddirect (verified migration1662366406542). Customer extraction qua pivotappointment_user.user_id→ecommerce_user. Trong context tư vấn, mỗi appointment thường có 1 customer trong pivot. - Đơn vị: Số KH unique
- Ví dụ: Period 7 ngày có 156 KH thỏa điều kiện → awaiting = 156
- Edge cases:
- Không tính KH đã rebook (chỉ trong period đang xem)
- Phân loại "lý do" (no-show / compliance gap / pending) là heuristic — chỉ tham khảo
FORMULA-006: Delta WoW (Week-over-Week)
- Mô tả: % thay đổi so với khoảng so sánh (mặc định tuần trước)
- Công thức:
delta_pct = ((current - previous) / previous) × 100 - Biến số:
current: KPI value của period hiện tạiprevious: KPI value của period so sánh (default: shift back theo độ dài period — vd period 7 ngày → previous 7 ngày liền trước)
- Đơn vị: % với indicator ▲ (positive) / ▼ (negative) / ─ (= 0%)
- Ví dụ: Hiện tại 124 cuộc / Trước 111 cuộc → delta = (124-111)/111 × 100 = +11.71% → "▲ 12% so tuần trước"
- Edge cases:
previous = 0→ hiển thị "▲ Mới" (không tính delta vô cực)- Đối với KPI #3 (NV active) → dùng absolute delta thay vì % (vd "─ Không đổi" hoặc "▲ +2 NV")
A11) Traceability — FR → AC → DEC → File downstream
Đảm bảo 100% coverage. Mọi FR phải có ≥ 1 AC + reference DEC + map đến file downstream.
| FR | AC | DEC ref | UI file ref | Dev file ref | QA file ref |
|---|---|---|---|---|---|
| FR-001 | AC-001-1 đến 6 | DEC-004, DEC-011 | ui-spec §B-KPI | dev-spec §C3 (FORMULA-001 đến 005) | qa-test-plan §D-KPI |
| FR-002 | AC-002-1 đến 5 | DEC-006 | ui-spec §B-Trend | dev-spec §C5 query | qa-test-plan §D-Chart |
| FR-003 | AC-003-1 đến 4 | DEC-006 | ui-spec §B-TopStaff | dev-spec §C5 | qa-test-plan §D-Chart |
| FR-004 | AC-004-1 đến 5 | DEC-006 | ui-spec §B-Heatmap | dev-spec §C5 + §C9 custom SVG | qa-test-plan §D-Chart |
| FR-005 | AC-005-1 đến 4 | DEC-008 | ui-spec §B-Branch | dev-spec §C5 | qa-test-plan §D-Chart |
| FR-006 | AC-006-1 đến 5 | DEC-005, DEC-013 | ui-spec §B-MissingRecordsModal | dev-spec §C5 query #7 + §C7 pagination | qa-test-plan §D-Modal |
| FR-007 | AC-007-1 đến 4 | DEC-005, DEC-013 | ui-spec §B-AnomalyMissingTVModal | dev-spec §C5 + §C7 | qa-test-plan §D-Modal |
| FR-008 | AC-008-1 đến 6 | DEC-009 | ui-spec §B-FilterBar | dev-spec §C5 staff lazy + branch group | qa-test-plan §D-Filter |
| FR-009 | AC-009-1 đến 4 | DEC-003, DEC-006 | ui-spec §B-DrillDown | dev-spec §C5 navigation logic | qa-test-plan §D-DrillDown |
| FR-010 | AC-010-1 đến 5 | DEC-010 | ui-spec §B-Permission | dev-spec §C8 + §C7 migration seed | qa-test-plan §D-Permission |
| FR-011 | AC-011-1 đến 4 | DEC-002 | ui-spec §B-RecordTableExtend | dev-spec §C5 RecordTable mod + GraphQL extend | qa-test-plan §D-Extend |
A12) Tooltip & microcopy chính (UI copy contract)
UI label ngắn tự nhiên; định nghĩa chặt nghĩa + công thức + scope đưa vào tooltip. Detail full:
ui-spec.md§B9 Tooltip Dictionary.
| Vị trí | Label (ngắn) | Tooltip (chi tiết) |
|---|---|---|
| KPI #1 | CUỘC TƯ VẤN | Tổng số ghi âm tư vấn đã tạo trong khoảng thời gian + chi nhánh đã chọn |
| KPI #2 | THỜI LƯỢNG TB | Thời lượng trung bình mỗi cuộc tư vấn. Insight: TB quá ngắn (< 5p) có thể NV chưa tư vấn đủ; TB quá dài (> 30p) có thể KH có vấn đề phức tạp |
| KPI #3 | NV HOẠT ĐỘNG | Số nhân viên unique có ít nhất 1 cuộc tư vấn được ghi âm trong period |
| KPI #4 | TUÂN THỦ GHI ÂM | 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 thời gian |
| KPI #5 | KH CHỜ TƯ VẤN | Số khách hàng có lịch hẹn loại tư vấn nhưng chưa được tạo ghi âm |
| Anomaly 🔴 | "X lịch hẹn tư vấn KHÔNG có ghi âm" | Compliance gap — appointment đã đến nhưng NV chưa upload audio. Cần BOD theo dõi tỷ lệ và delegate Manager xử lý |
| Anomaly 🟡 | "Y khách hàng có lịch hẹn nhưng chưa được tư vấn" | Coverage gap — KH đặt lịch nhưng không có record. Có thể: no-show / compliance gap / pending follow-up |
| 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 |
| Filter 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 |
| Filter date chip | "7 ngày qua" | Hôm nay + 6 ngày trước (rolling window) |
| Pagination footer | "Hiển thị X-Y / TOTAL" | Số rows đang hiển thị trên tổng số rows toàn dataset |
Tham chiếu chéo:
- Decision Brief:
./decision-brief.md- Source of Truth:
./SOURCE_OF_TRUTH.md- Evidence Pack:
./EVIDENCE_PACK.md- Design doc gốc:
../../superpowers/specs/2026-05-15-record-bod-insight-design.md- UI Spec:
./ui-spec.md(next file)- Dev Spec:
./dev-spec.md- QA Test Plan:
./qa-test-plan.md- Go-Live:
./go-live-checklist.md- Handoff:
./handoff.md