Appearance
Bug Fix: Wording + Filter status + BUG format + TZ — Báo cáo Doanh số/Thực thu & Chu kỳ khách hàng
v1.1 — 12/05/2026
| Thay đổi | Section | Ảnh hưởng |
|---|---|---|
🐛 BUG P0 — Fix format *_amount_rate hiển thị VND thay vì % | FR-008 | FE |
FR-009 — Thống nhất TZ trong SQL convertion_visits_cte | FR-009 | BE SQL |
DEC-T06 — Dùng allowlist tường minh cho isAmount check | Z3 Decisions | FE |
DEC-T07 — Đồng nhất AT TIME ZONE thay vì + INTERVAL '7 hour' | Z3 Decisions | BE SQL |
| DEC-007 — Tooltip card 2 nói rõ "bao gồm đơn còn công nợ" | Z1 Decisions | FE/Content |
| DEC-008 — BUG P0 nâng severity cả package thành high | Z1 Decisions | All |
C3.1.b — Sửa code isAmount line 138 ở component | C3) Component fixes | FE |
| C6 — SQL migration mới fix TZ | C6) SQL migration | BE SQL |
| TC-10/11/12 — Test cases mới cho BUG format + TZ fix | D) QA Test Cases | QA |
Tooltip conversion_rate thêm clarification về paid_amount | C2) i18n wording + C3.1.a | FE |
Quick Reference cho QA
| Trên dashboard | Trước | Sau |
|---|---|---|
| 🐛 Card "Tỉ lệ đóng góp thực thu Telesale" & "...CRM" | Hiển thị 0đ, "Tăng 4đ" (sai — đang format VND) | Hiển thị 4,00%, "Tăng 4%" (đúng) |
| Card "Tỉ lệ khách phát sinh doanh số" — tooltip | "Tổng số khách hàng phát sinh / Tổng số khách hàng" | "Tổng số lượt khách phát sinh doanh số / Tổng số lượt khách đến" |
| Card "Tỉ lệ khách phát sinh doanh số từ Telesale" — tooltip | nhắc "khách" | nhắc "đơn hàng có hoa hồng Telesale" |
| Card "Tỉ lệ khách phát sinh doanh số từ CRM" — tooltip | nhắc "khách" | nhắc "đơn hàng có hoa hồng CRM (bao gồm Telesale)" |
| Báo cáo Chu kỳ — Card đầu | "Số lượng khách bắt đầu mua" | "Số khách có phát sinh đơn trong kỳ" + icon ℹ️ tooltip |
| Báo cáo Chu kỳ — Số khách (7.281) | Gồm cả đơn order_new/prepaid_new chưa thanh toán | Chỉ gồm đơn đã thanh toán (status whitelist) |
| Typo "Tống số khách" trong vi.ts | "Tống" | "Tổng" |
SQL convertion_visits_cte (TZ) | visit_date::timestamp + INTERVAL '7 hour' | AT TIME ZONE 'Asia/Ho_Chi_Minh' (đồng bộ) |
Tác động dự kiến:
- Số
7.281trên báo cáo Chu kỳ sẽ giảm sau khi fix C1 (chưa biết bao nhiêu — cần chạy SQL kiểm chứng ở Phụ lục C-1 trước khi triển khai để communicate stakeholder). - Card Telesale/CRM amount rate hiện đang sai 100% — sau fix sẽ hiển thị
%đúng (số thực thu không đổi).
Z) Decisions
Z1) Business Decisions
| ID | Quyết định | Lý do | Status |
|---|---|---|---|
| DEC-001 | Tooltip 3 chỉ số chuyển đổi (conversion_rate, telesale_conversion_rate, crm_conversion_visits_rate) ghi đúng đơn vị đo (lượt visit / đơn hàng), không gọi chung "khách hàng" | Tránh BoD/PO hiểu nhầm metric → KPI sai | ✅ Chốt |
| DEC-002 | Label "Số lượng khách bắt đầu mua" → "Số khách có phát sinh đơn trong kỳ" + thêm tooltip giải thích | Cụm "bắt đầu mua" gợi ý là khách mới (first-time) — sai bản chất | ✅ Chốt |
| DEC-003 | Card này đo "khách thực sự mua" (đã có giao dịch tiền tệ) — KHÔNG gồm đơn chưa thanh toán | Intent business chuẩn: PO chốt | ✅ Chốt |
| DEC-004 | Giữ nguyên order_kind = prepaid (mua thẻ trả trước) trong bộ đếm — coi là "đã mua" | Khách đã thanh toán thật, có thu tiền | ✅ Chốt |
| DEC-005 | Filter ngày khác nhau giữa các chỉ số (visit_date / order.created_at / invoice.paid_at) — chỉ document, không sửa logic | Mỗi field phục vụ mục đích nghiệp vụ khác nhau | ✅ Chốt |
| DEC-006 | Mỗi tooltip có 1 ví dụ số ngắn (1 dòng) để PO/BA hiểu nhanh | Giảm bounce-off khi đọc dashboard | ✅ Chốt |
| DEC-007 | Tooltip card conversion_rate nói rõ "phát sinh DS = đã chốt đơn (total > 0, không phải bảo hành); bao gồm cả đơn còn công nợ chưa thanh toán" | is_zero_order ở BE không check paid_amount — manager dễ hiểu nhầm "phát sinh DS = đã có tiền vào quỹ" | ✅ Chốt |
| DEC-008 | 🐛 BUG P0 — *_amount_rate đang bị FE format thành VND. PRIORITY HIGHEST trong package, fix cùng wave wording | Hiển thị 0đ, Tăng 4đ → BoD/manager đọc số 100% SAI; không thể giải thích | ✅ Chốt |
Z3) Technical Decisions
| ID | Quyết định | Lý do |
|---|---|---|
| DEC-T01 | KHÔNG rename i18n key (vd conversion_rate, divisionValue) — chỉ đổi value | Giảm ripple sang component khác |
| DEC-T02 | KHÔNG đổi công thức tính (SQL function, Go logic của conversion_rate) | Wording fix only |
| DEC-T03 | KHÔNG đổi GraphQL field name, type, structure | Giữ contract API |
| DEC-T04 | Filter status mới (DEC-003) dùng whitelist thay vì NIN | Rõ ràng + dễ maintain |
| DEC-T05 | Component ActualRevenueReportDetailEfficiency.tsx đang dùng tooltipContents hardcoded — phải sửa trực tiếp mảng đó, không chỉ sửa i18n | i18n description block hiện là dead code trong flow này |
| DEC-T06 | isAmount check ở ActualRevenueReportDetailEfficiency.tsx:138 dùng allowlist tường minh (avg_payment_amount_per_visit only), không dùng key.includes("amount") lỏng | Bug đã xảy ra do match lỏng — telesale_amount_rate/crm_amount_rate chứa "amount" → bị format thành VND |
| DEC-T07 | Fix TZ cho convertion_visits_cte trong SQL search_dashboard_telesale_amount: dùng AT TIME ZONE 'Asia/Ho_Chi_Minh' (đồng nhất với CTE khác trong cùng file) | + INTERVAL '7 hour' là hard-code shift, hiện chỉ tồn tại ở CTE này → tử và mẫu của conversion_rate đang dùng 2 cách convert khác nhau cho cùng cột visit_date |
Z4) QA Decisions
| ID | Quyết định |
|---|---|
| DEC-Q01 | Trước khi triển khai C1: chạy SQL kiểm chứng (Phụ lục C-1) trên prod để estimate số 7.281 sẽ giảm bao nhiêu |
| DEC-Q02 | Visual regression trên 2 dashboard, đảm bảo wording đúng vị trí + không vỡ layout |
| DEC-Q03 | Grep negative coverage để không sót wording cũ ở module khác |
A) PRD
A1) Blueprint
| Hạng mục | Giá trị |
|---|---|
| Profile | S (bug-fix compact) |
| Module | report (FE) + ecommerce-api (BE Go) + controller/migrations/ecommerce (BE SQL) |
| Mode | Incremental Enhancement + Bug fix |
| Reuse strategy | Extend (i18n + 4 component edits + 1 Go filter change + 1 SQL TZ migration) |
| Backwards compatibility | 100% — không đổi GraphQL contract / công thức tổng quát |
| Layers ảnh hưởng | FE (4 thay đổi) · BE Go (2 thay đổi) · BE SQL (1 migration mới) |
A2) Context
PO/BA + BoD đặt câu hỏi về cách hiểu chỉ số Tỉ lệ khách phát sinh doanh số = 35,33% trên dashboard Doanh số/Thực thu. Audit code phát hiện:
- Tooltip hiện tại mô tả sai bản chất công thức — code chia theo lượt visit, tooltip lại nói khách hàng (xem
ActualRevenueReportDetailEfficiency.tsx:32-39vàvi.ts:246-290). - Card "Số lượng khách bắt đầu mua" gợi ý là khách mới — thực ra đếm distinct customer có đơn không-huỷ trong kỳ (gồm cả khách cũ).
- Card đó gồm cả đơn
order_new/prepaid_newchưa thanh toán — không match intent business "khách thực sự mua".
A3) Goals
| ID | Mục tiêu | Đo lường |
|---|---|---|
| G1 | PO/BA/BoD đọc tooltip hiểu đúng đơn vị đo | 100% tooltip ghi đúng "lượt" hoặc "đơn hàng" hoặc "khách" theo bản chất công thức |
| G2 | Card "Số khách có phát sinh đơn" chỉ đếm khách đã thực sự mua | 0% khách trong số đếm có đơn duy nhất ở status order_new/prepaid_new |
| G3 | Không có thuật ngữ "bắt đầu mua" / "Tống" còn sót | 0 grep match (xem TC-09) |
| G4 | 🐛 Card 3 & 5 (Telesale/CRM amount rate) hiển thị đúng % | TC-10 pass; không có instance hiển thị Tăng Xđ cho 2 card này |
| G5 | conversion_rate không bao giờ > 100% ở biên ngày | TC-12 pass; query manual khớp với API |
A4) Functional Requirements (FR) & Acceptance Criteria (AC)
FR-001 — Sửa tooltip conversion_rate (Báo cáo Doanh số/Thực thu)
Khi user hover icon ℹ️ trên card "Tỉ lệ lượt khách phát sinh doanh số" trong /r/reports/actual_revenue_report_group, hệ thống phải hiển thị tooltip:
- Tiêu đề:
Tỉ lệ lượt khách phát sinh doanh số - Tử số:
Tổng số lượt khách phát sinh doanh số - Mẫu số:
Tổng số lượt khách đến - Mô tả: "Tỉ lệ chuyển đổi tại điểm chạm: trong tổng lượt khách ghé, có bao nhiêu lượt phát sinh đơn. Một khách ghé nhiều lần được đếm nhiều lượt. VD: 20.184 lượt ghé, 7.131 lượt có đơn → 35,33%."
AC:
- AC-001.1: Tooltip hiển thị đủ 3 dòng + 1 mô tả + ví dụ số.
- AC-001.2: Không còn cụm "Tổng số khách hàng" làm mẫu số tooltip này.
- AC-001.3: Typo "Tống" đã sửa thành "Tổng".
FR-002 — Sửa tooltip telesale_conversion_rate
Tương tự FR-001 nhưng cho card Telesale:
- Tiêu đề:
Tỉ lệ đơn hàng có hoa hồng Telesale - Tử số:
Số đơn hàng có nhân viên nhóm Telesale nhận hoa hồng - Mẫu số:
Tổng số đơn hàng (service/cosmetic/prepaid, chưa huỷ) trong kỳ - Mô tả: "Tỉ trọng đơn hàng có sự đóng góp của team Telesale. Tính theo đơn hàng, không phải khách hay lượt."
AC-002.1: Tooltip không còn cụm "khách có đơn hàng".
FR-003 — Sửa tooltip crm_conversion_visits_rate
Tương tự FR-002 cho card CRM:
- Tiêu đề:
Tỉ lệ đơn hàng có hoa hồng CRM - Tử số:
Số đơn hàng có nhân viên CRM (bao gồm Telesale) nhận hoa hồng - Mẫu số:
Tổng số đơn hàng (service/cosmetic/prepaid, chưa huỷ) trong kỳ - Mô tả: "Tỉ trọng đơn hàng có sự đóng góp của team CRM (bao gồm Telesale). Tính theo đơn hàng."
AC-003.1: Tooltip nhắc rõ "CRM bao gồm Telesale".
FR-004 — Sửa server-side title
Response GraphQL reportSalesRevenue phải trả title đúng:
conversion_rate.title="Tỉ lệ lượt khách phát sinh doanh số"telesale_conversion_rate.title="Tỉ lệ đơn hàng có hoa hồng Telesale"crm_conversion_visits_rate.title="Tỉ lệ đơn hàng có hoa hồng CRM"
AC-004.1: Response API chứa title mới khi gọi từ Postman/curl.
FR-005 — Đổi label + thêm tooltip card Chu kỳ
Trong /r/reports/customer_cycle_report_group tab Chu Kỳ Mua Hàng, card đầu:
- Label mới:
Số khách có phát sinh đơn trong kỳ - Icon ℹ️ bên cạnh label
- Tooltip: "Đếm số khách hàng khác nhau (distinct) đã có ít nhất 1 đơn hàng dịch vụ/mỹ phẩm/thẻ trả trước (chưa huỷ, đã thanh toán) trong khoảng thời gian lọc. Bao gồm cả khách cũ mua lại, KHÔNG chỉ khách mới. VD: 7.281 khách có đơn trong tháng 5/2026."
AC-005.1: Label hardcoded cũ "Số lượng khách bắt đầu mua" đã xoá, thay bằng t('report.label.customer_cycle_purchase.titleTolal.total_customer'). AC-005.2: Icon ℹ️ render đúng vị trí, hover hiện tooltip.
FR-006 — Fix filter status (Cảnh báo 1)
reportPurchaseCycle backend phải chỉ đếm đơn đã thanh toán:
- Loại:
order_new,prepaid_new(chưa thanh toán) - Giữ:
order_in_progress,order_completed,order_force_completed,prepaid_inprogress,prepaid_completed,prepaid_force_completed - Vẫn loại (như cũ):
order_canceled,prepaid_canceled
AC-006.1: Số trên card "Số khách có phát sinh đơn" sau fix bằng giá trị từ query whitelist (so với Phụ lục C-1). AC-006.2: Bảng chu kỳ phía dưới (tỷ lệ chưa quay lại / khách có khả năng rời bỏ) cập nhật mẫu số mới.
FR-007 — Tooltip note cảnh báo filter ngày (Cảnh báo 3)
Thêm 1 dòng note nhỏ ở footer 2 báo cáo:
ℹ️ Báo cáo này filter theo [visit_date / order.created_at / paid_at]. Khi đối chiếu chéo với các báo cáo khác, một bản ghi có thể rơi vào kỳ khác nhau tùy field nào được filter.
AC-007.1: Note hiển thị ở cuối mỗi báo cáo, font nhỏ, màu mờ.
FR-008 — 🐛 BUG P0: Fix format *_amount_rate hiển thị VND thay vì %
Vị trí: diva-admin/src/modules/report/components/actual-revenue-report/ActualRevenueReportDetailEfficiency.tsx:138
Gốc rễ:
ts
// Hiện tại (line 138):
const isAmount = key.includes("amount") || key === "avg_payment_amount_per_visit";→ telesale_amount_rate, crm_amount_rate chứa chuỗi "amount" nên bị match lỏng → fall vào nhánh isAmount = true → render giá trị không nhân ×100 + đơn vị đ.
Hệ quả vận hành: BE trả 0.04 (= 4%) → FE hiển thị 0đ, Tăng 4đ thay vì 4,00%, Tăng 4%. BoD/manager đọc dashboard SAI 100%.
Khi dashboard render card telesale_amount_rate hoặc crm_amount_rate, hệ thống phải:
- Hiển thị giá trị nhân ×100, đuôi
% valueChangecũng hiển thị%- Card
avg_payment_amount_per_visitvẫn hiển thịđ(không regression)
AC:
- AC-008.1: Card 3 (Tỉ lệ đóng góp thực thu Telesale) hiển thị giá trị
%(vd4,00%), không phảiđ. - AC-008.2: Card 5 (Tỉ lệ đóng góp thực thu CRM) tương tự AC-008.1.
- AC-008.3: Dòng
Tăng Xhiển thịTăng X%không phảiTăng Xđ. - AC-008.4: Card 1
avg_payment_amount_per_visitvẫn hiển thịđ— không bị regression.
FR-009 — Thống nhất TZ trong SQL search_dashboard_telesale_amount
Vị trí: diva-backend/services/controller/migrations/ecommerce/<new>_fix_tz_telesale_amount/up.sql (migration mới)
Gốc rễ: Trong cùng SQL function search_dashboard_telesale_amount, convertion_visits_cte (line ~78 trong migration 1761037948000) đang dùng:
sql
AND acv.visit_date::timestamp + INTERVAL '7 hour' BETWEEN _from AND _toTrong khi các CTE khác và search_dashboard_customer_visits.all_visits (line ~233) dùng:
sql
AND (acv.visit_date::timestamp AT TIME ZONE 'Asia/Ho_Chi_Minh') BETWEEN _from AND _to→ Cùng cột visit_date, hai cách convert TZ khác nhau. Tử (conversion_visits từ convertion_visits_cte) và mẫu (total_customer_visits từ all_visits) của card conversion_rate đang dùng 2 cách lọc thời gian khác nhau. Ở biên giới ngày, có thể đếm khác số → hiếm trường hợp conversion_rate > 100%.
Fix: Đổi convertion_visits_cte (và mọi nơi còn dùng + INTERVAL '7 hour') sang AT TIME ZONE 'Asia/Ho_Chi_Minh' cho đồng nhất.
AC:
- AC-009.1: Sau migration,
EXPLAIN2 CTE (convertion_visits_ctevàall_visits) cùng cách filter TZ. - AC-009.2:
conversion_ratekhông vượt 100% trên bất kỳ data range nào (test với_from=2026-05-01 00:00:00+07,_to=2026-05-01 23:59:59+07). - AC-009.3: So sánh kết quả
conversion_ratetrước/sau migration trên 1 kỳ test — chênh lệch nếu có chỉ xảy ra ở biên ngày và ≤ 1% data.
A5) Risks
| Rủi ro | Mức | Mitigation |
|---|---|---|
Stakeholder quen số 7.281 thắc mắc khi số giảm sau fix C1 | 🟡 Medium | Chạy Phụ lục C-1 trước, communicate impact > 10% với stakeholder + ghi vào release note |
Sau khi fix FR-008, số trên card 3/5 sẽ "nhỏ đi 100 lần" (vd hiện Tăng 4đ → sau Tăng 4%) | 🟡 Medium | Release note nhấn rõ "đây là fix bug format, số thực thu không đổi"; trước/sau số THỰC vẫn nhất quán |
| Sót wording cũ ở module khác | 🟢 Low | TC-09 grep coverage |
| Vỡ layout do chữ tooltip dài hơn | 🟢 Low | QTooltip đã có maxWidth: 400px |
| Bảng chu kỳ phía sau cũng bị ảnh hưởng (vì mẫu số đổi) | 🟡 Medium | QA verify cả bảng pur_cycle_table không chỉ card overview |
| FR-009 migration đụng prod, không có rollback data | 🟢 Low | SQL CREATE OR REPLACE FUNCTION idempotent; nếu cần rollback chạy lại migration cũ 1761037948000 |
C) Dev Spec — Code Changes
C1) Filter status fix (Cảnh báo 1)
File: diva-backend/services/ecommerce-api/action/report_purchase_cycle.go
Trước (line 266-271):
go
whereOrder := map[string]interface{}{
"created_at": store.Comparison{store.GTE: input.Data.FromDate, store.LTE: input.Data.ToDate},
"order_service_status": store.Comparison{store.NIN: []store.OrderServiceStatus{
store.OrderServiceStatusCanceled,
store.OrderPrepaidCanceled,
}},
"order_kind": store.Comparison{store.IN: []string{"service", "cosmetic", "prepaid"}},
}Sau:
go
whereOrder := map[string]interface{}{
"created_at": store.Comparison{store.GTE: input.Data.FromDate, store.LTE: input.Data.ToDate},
"order_service_status": store.Comparison{store.IN: []store.OrderServiceStatus{
store.OrderServiceStatusInProgress, // "order_in_progress"
store.OrderServiceStatusCompleted, // "order_completed"
store.OrderServiceStatusForceCompleted, // "order_force_completed"
store.OrderPrepaidInProgress, // "prepaid_inprogress"
store.OrderPrepaidCompleted, // "prepaid_completed"
store.OrderPrepaidForceCompleted, // "prepaid_force_completed"
}},
"order_kind": store.Comparison{store.IN: []string{"service", "cosmetic", "prepaid"}},
}Lý do: chỉ giữ đơn đã thanh toán hoặc đã hoàn tất, loại các đơn còn ở trạng thái new (chưa thanh toán).
C2) i18n wording fix
File: diva-admin/src/modules/report/i18n/vi.ts
Block actual_revenue_report — sửa 12 chuỗi (line ~246-290)
ts
actual_revenue_report: {
titleTolal: {
avg_payment_amount_per_visit: "Thực thu trung bình lượt khách đến",
conversion_rate: "Tỉ lệ lượt khách phát sinh doanh số", // ← sửa
telesale_amount_rate: "Tỉ lệ đóng góp thực thu Telesale",
telesale_conversion_rate: "Tỉ lệ đơn hàng có hoa hồng Telesale", // ← sửa
crm_amount_rate: "Tỉ lệ đóng góp thực thu CRM",
crm_conversion_visits_rate: "Tỉ lệ đơn hàng có hoa hồng CRM", // ← sửa
},
divisionValue: {
avg_payment_amount_per_visit: "Tổng thực thu phát sinh",
conversion_rate: "Tổng số lượt khách phát sinh doanh số", // ← sửa + fix typo "Tống"
telesale_amount_rate: "Tổng doanh số hoa hồng của nhân viên thuộc nhóm Telesale",
telesale_conversion_rate: "Số đơn hàng có nhân viên nhóm Telesale nhận hoa hồng", // ← sửa
crm_amount_rate: "Tổng doanh số hoa hồng của nhân viên thuộc nhóm CRM",
crm_conversion_visits_rate: "Số đơn hàng có nhân viên CRM (bao gồm Telesale) nhận hoa hồng", // ← sửa
},
dividedValue: {
avg_payment_amount_per_visit: "Tổng số lượt khách đến",
conversion_rate: "Tổng số lượt khách đến", // ← sửa
telesale_amount_rate: "Tổng thực thu phát sinh",
telesale_conversion_rate: "Tổng số đơn hàng trong kỳ", // ← sửa
crm_amount_rate: "Tổng thực thu phát sinh",
crm_conversion_visits_rate: "Tổng số đơn hàng trong kỳ", // ← sửa
},
description: {
avg_payment_amount_per_visit:
"Thực thu trung bình mỗi lượt khách ghé cửa hàng trong kỳ.",
conversion_rate:
"Tỉ lệ chuyển đổi tại điểm chạm: trong tổng lượt khách ghé, có bao nhiêu lượt phát sinh đơn (đã chốt đơn, total > 0, không phải bảo hành; **bao gồm cả đơn còn công nợ chưa thanh toán**). Một khách ghé nhiều lần được đếm nhiều lượt. VD: 20.184 lượt ghé, 7.131 lượt có đơn → 35,33%.",
telesale_amount_rate:
"Tỉ lệ doanh thu (thực thu) đến từ đơn hàng có hoa hồng cho nhân viên nhóm Telesale.",
telesale_conversion_rate:
"Tỉ trọng đơn hàng có sự đóng góp của team Telesale. Tính theo đơn hàng, không phải khách hay lượt.",
crm_amount_rate:
"Tỉ lệ doanh thu (thực thu) đến từ đơn hàng có hoa hồng cho nhân viên nhóm CRM (bao gồm Telesale).",
crm_conversion_visits_rate:
"Tỉ trọng đơn hàng có sự đóng góp của team CRM (bao gồm Telesale). Tính theo đơn hàng.",
},
},Note: titleTolal/divisionValue/dividedValue được component dùng qua t(...) → sửa ở đây có hiệu lực. Riêng description block hiện là dead code (component dùng mảng tooltipContents hardcoded — xem C3). Vẫn nên sửa cho đồng bộ.
Thêm block mới customer_cycle_purchase
ts
customer_cycle_purchase: {
titleTolal: {
total_customer: "Số khách có phát sinh đơn trong kỳ",
},
description: {
total_customer:
"Đếm số khách hàng khác nhau (distinct) đã có ít nhất 1 đơn hàng dịch vụ/mỹ phẩm/thẻ trả trước (chưa huỷ, đã thanh toán) trong khoảng thời gian lọc. Bao gồm cả khách cũ mua lại, KHÔNG chỉ khách mới. VD: 7.281 khách có đơn trong tháng 5/2026.",
},
},C3) Component fixes
C3.1) ActualRevenueReportDetailEfficiency.tsx — sửa tooltipContents hardcoded + 🐛 FIX BUG isAmount
File: diva-admin/src/modules/report/components/actual-revenue-report/ActualRevenueReportDetailEfficiency.tsx
C3.1.a) Sửa mảng tooltipContents (line 32-39)
tsx
const tooltipContents = [
`Số tiền thực tế khách hàng mang lại mỗi lượt ghé cửa hàng trong kỳ.`,
`Tỉ lệ chuyển đổi tại điểm chạm: trong tổng lượt khách ghé, có bao nhiêu lượt phát sinh đơn (đã chốt đơn, total > 0, không phải bảo hành; bao gồm cả đơn còn công nợ chưa thanh toán). Một khách ghé nhiều lần được đếm nhiều lượt. VD: 20.184 lượt ghé, 7.131 lượt có đơn → 35,33%.`,
`Tỉ lệ doanh thu (thực thu) đến từ đơn hàng có hoa hồng cho nhân viên nhóm Telesale.`,
`Tỉ trọng đơn hàng có sự đóng góp của team Telesale. Tính theo đơn hàng, không phải khách hay lượt.`,
`Tỉ lệ doanh thu (thực thu) đến từ đơn hàng có hoa hồng cho nhân viên nhóm CRM (bao gồm Telesale).`,
`Tỉ trọng đơn hàng có sự đóng góp của team CRM (bao gồm Telesale). Tính theo đơn hàng.`,
] as const;C3.1.b) 🐛 BUG FIX — sửa isAmount check (line 138)
Trước (BUG):
tsx
const isAmount = key.includes("amount") || key === "avg_payment_amount_per_visit";→ telesale_amount_rate, crm_amount_rate chứa "amount" nên bị bắt nhầm.
Sau (FIX):
tsx
const AMOUNT_KEYS = new Set<string>(["avg_payment_amount_per_visit"]);
// ...trong setup() hoặc đầu component:
const isAmount = AMOUNT_KEYS.has(key);Hoặc inline (giữ scope đơn dòng):
tsx
const isAmount = key === "avg_payment_amount_per_visit";Lý do dùng allowlist tường minh: mọi card mới thêm sau này mặc định là %, chỉ những key explicit nằm trong allowlist mới hiển thị đ. Tránh tái phát bug match lỏng.
C3.1.c) (Optional follow-up) Refactor về i18n
Tại line 152 thay description={tooltipContents[idx]} bằng description={t(\report.label.actual_revenue_report.description.${key}`)}rồi xoá mảng hardcoded. Khi đó blockdescription` ở C2 mới có hiệu lực. Khuyến nghị cho follow-up PR, không gộp vào fix này để giữ scope nhỏ + dễ revert.
C3.2) CustomerCyclePurchaseViewChart.tsx — đổi label + thêm icon tooltip
File: diva-admin/src/modules/report/components/customer-cycle/CustomerCyclePurchaseViewChart.tsx, line 143.
Trước:
tsx
<QText class="q-my-sm">Số lượng khách bắt đầu mua</QText>Sau:
tsx
<QRow style={{ alignItems: "center" }}>
<QText class="q-my-sm">
{t("report.label.customer_cycle_purchase.titleTolal.total_customer")}
</QText>
<QIcon name={ICON_INFORMATION} size="20px" class="q-ml-sm" style={{ cursor: "pointer" }}>
<QTooltip self="top middle" style={{ background: "#141414", maxWidth: "400px" }}>
<QText>{t("report.label.customer_cycle_purchase.description.total_customer")}</QText>
</QTooltip>
</QIcon>
</QRow>C4) Server-side titles
File: diva-backend/services/ecommerce-api/action/report_sales_revenue.go
go
// line 368
output.ConversionRate.Title = "Tỉ lệ lượt khách phát sinh doanh số"
// line 370
output.TelesaleConversionVisitsRate.Title = "Tỉ lệ đơn hàng có hoa hồng Telesale"
// line 372
output.CrmConversionVisitsRate.Title = "Tỉ lệ đơn hàng có hoa hồng CRM"C5) Footer note (Cảnh báo 3 — FR-007)
Thêm component note ở cuối mỗi báo cáo (FE). Wording đề xuất ở FR-007. Component có thể dùng <QText class="text-grey-7 text-caption"> hoặc tương tự.
C6) SQL migration — Fix TZ trong convertion_visits_cte (FR-009)
File mới: diva-backend/services/controller/migrations/ecommerce/<timestamp>_fix_tz_telesale_amount_convertion_visits/up.sql
Nội dung: Re-create search_dashboard_telesale_amount với CTE convertion_visits_cte đã đổi TZ filter.
Diff (chỉ phần thay đổi trong CTE):
sql
-- Trước (migration 1761037948000 line ~78):
convertion_visits_cte AS (
SELECT COUNT(*) AS cnt
FROM all_customer_visits acv
WHERE (_branch_ids IS NULL OR acv.branch_id::text = ANY(_branch_ids))
AND acv.visit_date::timestamp + INTERVAL '7 hour' BETWEEN _from AND _to -- ❌
AND acv.is_zero_order = false
)
-- Sau:
convertion_visits_cte AS (
SELECT COUNT(*) AS cnt
FROM all_customer_visits acv
WHERE (_branch_ids IS NULL OR acv.branch_id::text = ANY(_branch_ids))
AND (acv.visit_date::timestamp AT TIME ZONE 'Asia/Ho_Chi_Minh') BETWEEN _from AND _to -- ✅
AND acv.is_zero_order = false
)down.sql: Copy nguyên function từ migration 1761037948000_update_func_search_dashboard_sales_revenue/up.sql (CTE giữ + INTERVAL '7 hour').
Lưu ý: CREATE OR REPLACE FUNCTION idempotent; deploy không cần downtime. Không có data migration.
D) QA Test Cases
| # | Setup | Kỳ vọng |
|---|---|---|
| TC-01 | Hover icon ℹ️ trên card "Tỉ lệ lượt khách phát sinh doanh số" tại /r/reports/actual_revenue_report_group | Tooltip hiện đúng tử/mẫu + ví dụ "20.184 / 7.131 → 35,33%". KHÔNG còn "Tống" hay "Tổng số khách hàng" |
| TC-02 | Hover tooltip telesale_conversion_rate | Tử = "Số đơn hàng có NV Telesale nhận hoa hồng", mẫu = "Tổng số đơn hàng trong kỳ" |
| TC-03 | Hover tooltip crm_conversion_visits_rate | Mô tả nhắc rõ "CRM bao gồm Telesale" |
| TC-04 | Gọi GraphQL reportSalesRevenue từ Postman | Response 3 title mới khớp với FE tooltip |
| TC-05 | Vào /r/reports/customer_cycle_report_group tab Chu Kỳ Mua Hàng | Label card đầu = "Số khách có phát sinh đơn trong kỳ", có icon ℹ️ |
| TC-06 | Hover icon ℹ️ card vừa kiểm | Tooltip mô tả distinct customer + nhắc rõ "đã thanh toán" + ví dụ "7.281 khách" |
| TC-07 | Filter kỳ 01/05–31/05/2026 trên prod, so sánh số card trước/sau fix C1 | Số sau fix ≤ số trước fix (vì loại đơn order_new/prepaid_new). Confirm với Phụ lục C-1 |
| TC-08 | Bảng chu kỳ phía dưới card | Mẫu số tỷ lệ chưa quay lại / khách rời bỏ tính theo TotalCustomer mới — không có hàng nào > 100% |
| TC-09 | Grep negative coverage trên diva-admin/ | rg "Tỉ lệ khách phát sinh doanh số|Số lượng khách bắt đầu mua|Tống số khách|Tỉ trọng khách phát sinh doanh số|Tỉ lệ chuyển đổi khách hàng, phần trăm khách hàng đã mua dịch vụ" → 0 match (trừ docs/) |
| TC-10 | 🐛 BUG fix verify — mở dashboard kỳ có dữ liệu Telesale (vd 01/05-31/05/2026), nhìn card 3 (Tỉ lệ đóng góp thực thu Telesale) và card 5 (CRM) | Hiển thị 4,00% (hoặc giá trị %), KHÔNG phải 0đ hay 4đ. Đơn vị bên dưới là %, không phải đ. Dòng "Tăng X" hiển thị Tăng X% |
| TC-11 | Regression check — card 1 avg_payment_amount_per_visit | Vẫn hiển thị giá trị đ (vd 1.250.000đ), không bị fix lệch sang % |
| TC-12 | FR-009 TZ fix — filter sát biên ngày _from = 2026-05-01 00:00:00+07, _to = 2026-05-01 23:59:59+07. Tính tay trong DB: SELECT COUNT(*) FROM all_customer_visits WHERE visit_date = '2026-05-01' AND is_zero_order = false so với conversion_visits từ API | Khớp 100%. conversion_rate không bao giờ > 1 (100%) trong bất kỳ kỳ test nào |
E) Go-Live
- Sửa trên branch dev → PR review → merge.
- Trước merge: chạy Phụ lục C-1 query, communicate stakeholder nếu impact > 10%.
- Release note ghi rõ:
"🐛 Bug fix: Card 'Tỉ lệ đóng góp thực thu Telesale/CRM' đã hiển thị đúng
%thay vìđ(lỗi format FE — số thực không đổi).Chuẩn hoá tooltip + label báo cáo doanh số/chu kỳ. Đồng thời fix filter status để card 'Số khách có phát sinh đơn' chỉ đếm đơn đã thanh toán (loại đơn
newchưa thanh toán). Số liệu trên card này có thể giảm so với trước, các tỷ lệ trong bảng chu kỳ cập nhật mẫu số mới.Cải thiện kỹ thuật: chuẩn hoá TZ filter trong SQL
search_dashboard_telesale_amount— tránh hiếm trường hợpconversion_rate > 100%ở biên ngày." - Rollback:
- FE (FR-001..FR-008): revert commits, không có schema/data migration.
- BE filter status (FR-006): revert Go code 1 file.
- BE SQL TZ (FR-009): chạy lại migration
1761037948000(idempotent).
Phụ lục C-1 — SQL kiểm chứng tác động C1
Chạy query này trên prod để estimate số 7.281 sẽ giảm bao nhiêu sau fix:
sql
SELECT
COUNT(DISTINCT customer_id) FILTER (
WHERE order_service_status NOT IN ('order_canceled', 'prepaid_canceled')
) AS current_total_customer, -- = ~7.281
COUNT(DISTINCT customer_id) FILTER (
WHERE order_service_status IN (
'order_in_progress', 'order_completed', 'order_force_completed',
'prepaid_inprogress', 'prepaid_completed', 'prepaid_force_completed'
)
) AS fixed_total_customer, -- số mới sau fix
COUNT(DISTINCT customer_id) FILTER (
WHERE order_service_status IN ('order_new', 'prepaid_new')
) AS pending_only_customer -- khách CHỈ có đơn pending
FROM "order"
WHERE created_at BETWEEN '2026-05-01' AND '2026-05-31 23:59:59'
AND order_kind IN ('service', 'cosmetic', 'prepaid')
AND customer_id IS NOT NULL;Decision rule:
- Nếu
pending_only_customer / current_total_customer < 5%→ fix luôn không cần thông báo riêng. - Nếu 5-15% → release note đủ.
- Nếu > 15% → email/Slack stakeholder trước khi merge, giải thích lý do.
Phụ lục C-2 — Status enum order
Từ diva-backend/pkg/store/order.go:170-192:
| Status | Coi là "đã mua" sau fix C1? |
|---|---|
order_new | ❌ Loại |
order_in_progress | ✅ Giữ |
order_completed | ✅ Giữ |
order_force_completed | ✅ Giữ |
order_canceled | ❌ (đang loại) |
prepaid_new | ❌ Loại |
prepaid_inprogress | ✅ Giữ |
prepaid_completed | ✅ Giữ |
prepaid_force_completed | ✅ Giữ |
prepaid_canceled | ❌ (đang loại) |
Phụ lục C-3 — Audit findings (3 cảnh báo gốc)
Cảnh báo 1 (đã FIX trong C1)
report_purchase_cycle.go:269 cũ chỉ loại 2 status order_canceled, prepaid_canceled → bao gồm cả đơn order_new/prepaid_new chưa thanh toán. Không khớp intent business "khách thực sự mua".
Cảnh báo 2 (GIỮ NGUYÊN)
order_kind = prepaid (mua thẻ trả trước) được tính là "đã mua". PO chốt: đã thanh toán = đã mua. Không sửa code, ghi rõ trong tooltip C2 (description block).
Cảnh báo 3 (DOCUMENT ONLY — FR-007)
3 filter ngày khác nhau:
all_customer_visits.visit_date→ Lượt khách / Số khách / Conversion rateorder.created_at→ Số khách có phát sinh đơninvoice.paid_at→ Doanh số / Thực thu / Telesale_amount / CRM_amount
Mỗi field phục vụ mục đích nghiệp vụ khác nhau (đo traffic / đo ý định mua / đo thu tiền thực). Không fix code, chỉ thêm tooltip note ở footer mỗi báo cáo (FR-007).