Skip to content

v1.4 — 27/03/2026

Thay đổiSectionẢnh hưởng
Đổi label "Commission tư vấn" → "Hoa hồng tư vấn", loại trừ thanh toán bằng ví (DEC-012)Z) Decision Log, A5 FR-003/005, A9 Glossary, C3 FormulasBE, FE

Báo cáo doanh số cá nhân

Version: 1.2 Date: 2026-03-18 Author: PO/BA Type: New Feature Complexity: M Module: Report


Changelog

VersionDateAuthorThay đổi
1.02026-03-18PO/BAInitial — chuyển từ design doc Mode A
1.22026-03-23PO/BABổ sung truy thu commission (wallet DB) + drill-down popup chi tiết ngày
1.32026-03-23PO/BAGom thành hub "Báo cáo doanh số cá nhân" — 3 tabs, ẩn 2 cards cũ, redirect route
1.42026-03-27PO/BAĐổi label "Commission tư vấn" → "Hoa hồng tư vấn"; loại trừ thanh toán bằng ví (wallet + wallet_promotion) khỏi commission (DEC-012)

Hướng dẫn đọc (RACI)

AudienceĐọc sectionsTrách nhiệm
PO/BAZ, A, BApprove requirements, UX flows
Tech LeadZ, A, CApprove architecture, review dev spec
FE DevB, C (C1-C5)Implement UI
BE DevC (C1-C11)Implement SQL functions, Hasura metadata
QAA (A5), DWrite + execute test cases

Executive Summary (TL;DR)

Tạo hub báo cáo mới "Báo cáo doanh số cá nhân" thay thế 3 cards riêng lẻ (Báo cáo doanh thu NV, Báo cáo tour, Báo cáo hoa hồng → chỉ gom 2 cards đầu, hoa hồng giữ riêng) trong Report module. Hub gồm 3 tabs:

  1. Doanh số theo ngày (default, build mới): Pivot table NV × ngày cho commission, tour, truy thu — filter, drill-down, export Excel
  2. Doanh thu theo đơn hàng: Import EmployeeRevenueReport component hiện có
  3. Tiền tour: Import TourIncomeReport component hiện có

Mục tiêu: Giảm nhầm lẫn cho HCNS (1 entry point thay vì 3 cards), giảm thao tác tổng hợp thủ công.

4 nhóm tính năng:

  1. Hub 3 tabs: 1 card "Báo cáo doanh số cá nhân" trên trang Reports, ẩn 2 cards cũ, redirect route cũ
  2. Pivot Table: 3 SQL functions (commission + tour + truy thu) trả cùng shape + pivot table động
  3. Drill-down: Click ô số tiền → popup chi tiết danh sách giao dịch (bao gồm bút toán âm truy thu)
  4. Export: Xuất Excel giống y hệt view trên màn hình

Milestones

MilestoneTargetOwnerĐiều kiện
BE — SQL functions + Hasura (ecommerce + project)T+3 ngàyBE DevDeploy 2 migrations + metadata
BE — SQL function clawback + Hasura (wallet)T+4 ngàyBE DevDeploy wallet migration + metadata
FE — Tab container + import 2 báo cáo cũ + redirect routeT+5 ngàyFE DevSong song với BE
FE — Pivot table (tab "Doanh số theo ngày") + filterT+7 ngàyFE DevSau khi BE deploy xong
FE — Drill-down popupT+9 ngàyFE DevSau khi table hoạt động
FE — Export ExcelT+10 ngàyFE DevSau khi popup hoạt động
QA + Go-LiveT+13 ngàyQAQA pass

Trạng thái Sign-off

DomainNgườiStatus
BusinessPO✅ Approved (via design doc review)
TechTech LeadPending
QAQA LeadPending

Pending Decisions

IDNội dungOwnerDeadlineStatus
PD-001Xác nhận transaction_request.code có auto-generate cho refund_commission không. Nếu không → dùng UUID fallbackBE DevTrước FE popupOpen

Backlog Phase 2 (Out-of-scope)

#Tính năngLý do defer
1So sánh giữa các tháng (month-over-month)Scope riêng, cần thêm UI
2Biểu đồ (chart) trực quanPivot table đủ cho yêu cầu hiện tại
3Thêm loại doanh số khác (dịch vụ, mỹ phẩm)Data structure khác, cần analysis riêng
4KPI target vs actualCần tích hợp module KPI
5Drill-down click vào ô → xem chi tiết đơn hàngĐã chuyển vào scope v1.1 (DEC-010)
6Dropdown "Doanh số thực nhận" (commission − truy thu gộp)Cần cross-DB aggregation
7Highlight ô có truy thu trong view commissionNice-to-have
8Filter chức vụ cho "Truy thu commission"Wallet DB không có department data
9Auto-generate transaction_request.code cho refund_commissionHiện dùng UUID fallback

Z) Decision Log

IDCategoryQuyết địnhLý doNgàyStatus
DEC-001TechnicalTạo SQL functions mới (Approach B), không sửa query hiện cóTách biệt, deploy độc lập, không ảnh hưởng report cũ2026-03-18Locked
DEC-002Technical2 SQL functions trả cùng shape (user_id, employee_code, display_name, report_date, total_amount)Frontend dùng 1 pivot component, chỉ switch query theo dropdown — giảm complexity2026-03-18Locked
DEC-003TechnicalTour function đọc trực tiếp project_task_assignee + project_task (DB project), KHÔNG wrap search_report_tour_incomeTránh cross-database function call (commission ở ecommerce, tour ở project)2026-03-18Locked
DEC-004TechnicalCommission function ở DB ecommerce, tour function ở DB projectMỗi function nằm cùng DB với data source — tránh cross-database2026-03-18Locked
DEC-005UXKhông phân trang — hiển thị tất cả NV, scroll verticalHCNS cần nhìn toàn cảnh, export cũng cần tất cả data2026-03-18Locked
DEC-006TechnicalMonthPickerWithButton import cross-module từ salaryComponent đã mature, không cần duplicate2026-03-18Locked
DEC-007TechnicalHasura track 2 functions as custom function (root-level query)Giống pattern search_dashboard_sales_revenue đã có2026-03-18Locked
DEC-008BusinessTruy thu commission ghi nhận theo ngày duyệt hoàn (real-time), không sửa ngược ngày gốcĐúng nguyên tắc kế toán phát sinh, báo cáo ổn định không "nhảy số"2026-03-23Locked
DEC-009UXTruy thu commission là 1 option riêng trong dropdown (không gộp vào ô commission)Tránh cross-DB (wallet vs ecommerce); truy thu hiếm, tách riêng tránh nhiễu2026-03-23Locked
DEC-010UXDrill-down popup khi click ô số tiền — chuyển từ Phase 2 vào Phase 1PO yêu cầu bổ sung; cần thiết để HCNS đối soát chi tiết2026-03-23Locked
DEC-011UXGom thành hub "Báo cáo doanh số cá nhân" — 3 tabs, ẩn 2 cards cũ (Doanh thu NV + Tour), redirect route cũHCNS nhầm lẫn giữa 3+ cards báo cáo liên quan NV; 1 entry point giảm cognitive load, dễ so sánh cross-tab2026-03-23Locked
DEC-012BusinessLoại trừ thanh toán bằng ví (wallet + wallet_promotion) khỏi hoa hồng tư vấn. Đổi label "Commission tư vấn" → "Hoa hồng tư vấn"Giống logic view report_employee_result hiện có (payment_method_id <> 'wallet' AND <> 'wallet_promotion'); hoa hồng từ ví không tính vào doanh số NV2026-03-27Locked

A) PRD

A1) Blueprint

FieldValue
FeatureBáo cáo doanh số cá nhân (Hub 3 tabs: Doanh số theo ngày + Doanh thu theo ĐH + Tiền tour)
TypeNew Feature + Restructure
PlatformWeb Admin (diva-admin)
Module ảnh hưởngreport (FE — hub + pivot mới), controller/ecommerce (BE), controller/project (BE), controller/wallet (BE)

A2) Context

As-Is

  • EmployeeRevenueReport (/r/reports/employee_revenue_report_group): hiển thị doanh số theo danh sách đơn hàng (ngày, mã đơn, NV, khách, số tiền) — không có view tổng hợp theo ngày
  • EmployeeProfileCommission: xem commission từng NV riêng lẻ — phải mở từng profile
  • EmployeeProfileTourIncome: xem tiền tour từng NV riêng lẻ — phải mở từng profile
  • HCNS tổng hợp thủ công trên Google Sheets: export data → paste → pivot → format

To-Be

  • 1 card báo cáo mới "Báo cáo doanh số cá nhân" thay thế 2 cards cũ (Doanh thu NV + Tour)
  • 3 tabs trong 1 page:
    • Tab "Doanh số theo ngày" (default): pivot table NV × ngày, filter, drill-down, export
    • Tab "Doanh thu theo đơn hàng": import EmployeeRevenueReport component (giữ nguyên logic)
    • Tab "Tiền tour": import TourIncomeReport component (giữ nguyên logic)
  • Ẩn 2 cards cũ khỏi trang Reports grid
  • Route cũ redirect về tab tương ứng (bookmark safe)
  • HCNS chỉ cần nhớ 1 entry point

A3) Goals & Success Metrics

GoalMetricTarget
HCNS xem tổng quan doanh số NV nhanhThời gian từ "muốn xem" → có data< 10 giây (hiện tại: 30-60 phút)
Giảm công sức tổng hợp thủ côngSố lần HCNS export + pivot thủ công / tháng0 lần (hiện tại: 2-4 lần/tháng)
Export chính xácTỉ lệ data match giữa hệ thống vs Excel thủ công100%

A4) Personas

PersonaVai tròJTBDFrequency
HCNSQuản lý nhân sự, đánh giá hiệu suấtXem tổng doanh số commission/tour NV theo ngày để đánh giá, báo cáo BODHàng tháng
Quản lý chi nhánhTheo dõi NV trong branchXem doanh số NV chi nhánh mình để coachingHàng tuần

A5) Functional Requirements

FR-001: Pivot table doanh số NV theo ngày (Ref: DEC-001, DEC-002, DEC-005)

Priority: Must | SCR: SCR-01

AC:

  • [ ] Trang báo cáo hiển thị bảng pivot: hàng = NV (Mã NV + Họ tên), cột = từng ngày trong tháng (01, 02, ..., 28-31), cột cuối = Tổng
  • [ ] Giá trị ô = tổng commission/tour NV ngày đó (VND)
  • [ ] Format số: xxx.xxx.xxx (không decimal)
  • [ ] Ô không có data hiển thị 0
  • [ ] Hiển thị tất cả NV match filter, scroll vertical (không phân trang)
  • [ ] Sort mặc định: employee_code ASC
  • [ ] Cột Tổng = sum hàng ngang (frontend tính)

FR-002: Filter chi nhánh + chức vụ + tháng (Ref: DEC-006)

Priority: Must | SCR: SCR-01

AC:

  • [ ] MonthPicker: chọn tháng, nút prev/next, format MM/YYYY, default = tháng hiện tại
  • [ ] BranchSelect: multi-select chi nhánh, default = tất cả
  • [ ] JobPositionSelect: multi-select chức vụ, default = tất cả
  • [ ] Thay đổi filter → auto fetch lại data

FR-003: Dropdown loại doanh số (Ref: DEC-002, DEC-003, DEC-009)

Priority: Must | SCR: SCR-01

AC:

  • [ ] Dropdown QSelect: 3 option: "Hoa hồng tư vấn" (default) | "Tiền tour" | "Truy thu commission"
  • [ ] Chọn "Hoa hồng tư vấn" → gọi SearchEmployeeDailyCommission
  • [ ] Chọn "Tiền tour" → gọi SearchEmployeeDailyTourIncome
  • [ ] Chọn "Truy thu commission" → gọi SearchEmployeeDailyCommissionClawback
  • [ ] Switch loại → fetch lại data, giữ nguyên filter tháng/chi nhánh
  • [ ] Khi chọn "Truy thu commission" → ẩn/disable filter chức vụ (wallet DB không có department data)

FR-004: Export Excel (Ref: DEC-005)

Priority: Must | SCR: SCR-01

AC:

  • [ ] Nút "Tải xuống" trên filter bar
  • [ ] Export file .xlsx format giống y hệt pivot table trên màn hình
  • [ ] Header: bold, background color
  • [ ] Data: số align right, format currency VND
  • [ ] Tên file: doanh-so-nv-{MM-YYYY}_{YYYYMMDDHHmmss}.xlsx
  • [ ] Export tất cả NV (không chỉ viewport)

FR-007: SQL function truy thu commission (Ref: DEC-008, DEC-009)

Priority: Must | SCR:

AC:

  • [ ] Function search_employee_daily_commission_clawback ở DB wallet
  • [ ] Input: _from date, _to date, _branch_ids uuid[] (không có _job_positions — wallet DB không có department)
  • [ ] Output: user_id, employee_code, display_name, report_date, total_amount (cùng shape FR-005/006)
  • [ ] Data source: transaction JOIN transaction_request (giống pattern view order_commission_refund)
  • [ ] Filter: behavior_id = 'refund_commission', status = 'S', wallet_type_id = 'COMMISSION'
  • [ ] Ngày ghi nhận: transaction_request.updated_at (ngày duyệt hoàn) với timezone Asia/Ho_Chi_Minh
  • [ ] Employee info từ wallet_user (code, display_name, branch_id)
  • [ ] Group by: user_id + date

FR-008: Drill-down popup chi tiết ngày (Ref: DEC-010)

Priority: Must | SCR: SCR-01, SCR-02

AC:

  • [ ] Click vào ô số tiền trong pivot table → mở QDialog popup
  • [ ] Popup title: "Chi tiết doanh số — {Họ tên NV} — {DD/MM/YYYY}"
  • [ ] Hiển thị danh sách giao dịch giống trang EmployeeProfileRevenue
  • [ ] Cột: Ngày TT, Mã đơn hàng, Loại ĐH, Mã giao dịch, Nhóm giao dịch, Khách hàng, Doanh thu tư vấn
  • [ ] Bút toán truy thu: Loại ĐH = "Truy thu commission", Mã GD = mã withdraw request, amount hiện số âm màu đỏ
  • [ ] Data source: 2 query song song (ecommerce + wallet) merge frontend — tránh cross-database
  • [ ] Click cột Tổng → popup hiện tất cả giao dịch cả tháng
  • [ ] Ô = 0 click vào → popup hiện "Không có giao dịch"

FR-009: Hub "Báo cáo doanh số cá nhân" — Tab container + redirect (Ref: DEC-011)

Priority: Must | SCR: SCR-00

AC:

  • [ ] 1 card mới "Báo cáo doanh số cá nhân" trên trang Reports, route /r/reports/employee_daily_commission_group
  • [ ] Page container với QTabs: 3 tabs — "Doanh số theo ngày" (default) | "Doanh thu theo đơn hàng" | "Tiền tour"
  • [ ] Tab "Doanh số theo ngày" → render pivot table component (SCR-01)
  • [ ] Tab "Doanh thu theo đơn hàng" → import + render EmployeeRevenueReport component hiện có
  • [ ] Tab "Tiền tour" → import + render TourIncomeReport component hiện có
  • [ ] Ẩn 2 cards cũ khỏi trang Reports: "Báo cáo doanh thu nhân viên" (employee_revenue_report_group) + "Báo cáo tour" (tour_income_report_group)
  • [ ] Route cũ /r/reports/employee_revenue_report_group → redirect đến /r/reports/employee_daily_commission_group?tab=revenue
  • [ ] Route cũ /r/reports/tour_income_report_group → redirect đến /r/reports/employee_daily_commission_group?tab=tour
  • [ ] Tab active state đồng bộ với query param ?tab=daily|revenue|tour (default: daily)
  • [ ] Permission: hiển thị card nếu user có quyền ít nhất 1 trong 2 report cũ

FR-005: SQL function hoa hồng tư vấn (Ref: DEC-001, DEC-004, DEC-012)

Priority: Must | SCR:

AC:

  • [ ] Function search_employee_daily_commission ở DB ecommerce
  • [ ] Input: _from date, _to date, _branch_ids uuid[], _job_positions text[]
  • [ ] Output: user_id, employee_code, display_name, report_date, total_amount
  • [ ] Data source: order_commission_user JOIN invoice (dùng paid_at với timezone Asia/Ho_Chi_Minh)
  • [ ] Loại trừ thanh toán bằng ví: invoice.payment_method_id NOT IN ('wallet', 'wallet_promotion') (Ref: DEC-012)
  • [ ] Filter soft delete: account.deleted_at IS NULL, department_user.deleted_at IS NULL
  • [ ] Group by: user_id + date

FR-006: SQL function tiền tour (Ref: DEC-003, DEC-004)

Priority: Must | SCR:

AC:

  • [ ] Function search_employee_daily_tour_income ở DB project
  • [ ] Input: _from date, _to date, _branch_ids uuid[], _job_positions text[]
  • [ ] Output: user_id, employee_code, display_name, report_date, total_amount (cùng shape FR-005)
  • [ ] Data source: project_task_assignee JOIN project_task (dùng done_at với timezone Asia/Ho_Chi_Minh)
  • [ ] Filter: pt.is_done = true, pta.tour_money > 0, pta.supervisor = false, pta.assigner = false
  • [ ] Filter soft delete: account.deleted_at IS NULL, department_user.deleted_at IS NULL
  • [ ] Group by: user_id + date

A6) Assumptions

IDAssumptionOwner xác nhận
ASM-001HCNS đã có quyền xem report — không cần thêm permission mớiPO
ASM-002order_commission_user.invoice_id luôn NOT NULL cho data gần đây (migration 1742112936829 đã thêm cột)TL — verify
ASM-003Index trên invoice.paid_atorder_commission_user.invoice_id đã tồn tạiTL — verify

A7) Risks

IDRiskImpactProbabilityMitigation
RSK-001Query chậm khi nhiều NV + nhiều commission recordsTrung bìnhThấpSQL đã aggregate sẵn; index trên paid_at; worst case 500 NV × 31 ngày = 15.5K rows
RSK-002order_commission_user.invoice_id NULL cho data cũ → thiếu doanh sốTrung bìnhTrung bìnhINNER JOIN loại records cũ — chấp nhận chỉ báo cáo từ khi có invoice_id
RSK-003Cross-module import MonthPickerWithButton gây couplingThấpThấpNếu gây issue → copy component sang shared

A8) Metrics (Post-launch)

MetricCách đoTargetKhi nào đo
Adoption rateSố lần HCNS mở page / tháng> 10 lần/thángT+4 tuần
Export usageSố lần click "Tải xuống" / tháng> 5 lần/thángT+4 tuần
Query performanceEXPLAIN ANALYZE execution time< 2 giâyNgay sau deploy

A9) Glossary

Thuật ngữ (VI)Thuật ngữ (EN)Định nghĩaPhân biệt với
Hoa hồng tư vấnConsulting CommissionSố tiền hoa hồng NV thực nhận từ tư vấn đơn hàng (order_commission_user.amount), tính tỷ lệ theo invoice đã thanh toán. Loại trừ thanh toán bằng ví (wallet + wallet_promotion). Ref: DEC-012≠ Doanh thu đơn hàng (invoice.amount); ≠ Cấu hình hoa hồng (order_commission.amount)
Tiền tourTour IncomeTiền NV nhận từ thực hiện tour dịch vụ, ghi nhận khi task hoàn thành (project_task_assignee.tour_money)≠ Commission tư vấn
Truy thu commissionCommission ClawbackSố tiền commission bị thu hồi khi đơn hàng hoàn tiền, ghi nhận theo ngày duyệt hoàn (transaction với behavior_id = 'refund_commission')≠ Commission tư vấn (doanh số gốc)
Pivot tablePivot TableBảng ma trận: hàng = nhân viên, cột = ngày, giá trị = tổng tiền≠ Bảng danh sách (list table)

RACI

DeliverablePOTLFE DevBE DevQA
PRD (file này)ACIII
UI SpecCIRII
Dev SpecIACRI
QA Test PlanCIIIR
Migration SQLIARI
Hasura MetadataIAR

R = Responsible, A = Accountable, C = Consulted, I = Informed


C3) Formulas

FORMULA-001: Tổng hoa hồng tư vấn NV ngày (Ref: DEC-012)

total_amount = SUM(order_commission_user.amount)
WHERE invoice.paid_at IN [ngày đó, timezone Asia/Ho_Chi_Minh]
  AND user_id = [NV đó]
  AND invoice.payment_method_id NOT IN ('wallet', 'wallet_promotion')

Lưu ý:
- order_commission_user.amount = hoa hồng "Thực nhận" (tỷ lệ theo invoice)
- KHÔNG phải "Cấu hình" (order_commission.amount)
- Loại trừ thanh toán bằng ví (giống logic report_employee_result hiện có)

Đơn vị: VND (bigint)
Ví dụ: NV DV220169, ngày 09/03/2026
  - Đơn 1 (CK ngân hàng): hoa hồng thực nhận 2.000.000 ✓
  - Đơn 2 (tiền mặt): hoa hồng thực nhận 1.700.000 ✓
  - Đơn 3 (ví DIVA): hoa hồng thực nhận 500.000 ✗ (loại trừ)
  → total_amount = 3.700.000

Edge case:
- Không có commission ngày đó → không có row (FE hiển thị 0)
- invoice_id NULL (data cũ) → INNER JOIN loại bỏ, không tính
- Toàn bộ invoice ngày đó thanh toán bằng ví → không có row (FE hiển thị 0)

FORMULA-002: Tổng tour NV ngày

total_amount = SUM(project_task_assignee.tour_money)
WHERE project_task.done_at IN [ngày đó, timezone Asia/Ho_Chi_Minh]
  AND assignee_id = [NV đó]
  AND project_task.is_done = true
  AND tour_money > 0

Đơn vị: VND (bigint, cast từ numeric)
Ví dụ: NV DV220169, ngày 09/03/2026
  - Tour 1: 500.000
  - Tour 2: 300.000
  → total_amount = 800.000

Edge case:
- tour_money = NULL → không tính (WHERE > 0 loại)
- tour_money = 0 → không tính
- task chưa done → không tính (is_done = false)

FORMULA-004: Tổng truy thu NV ngày

total_amount = SUM(transaction.amount)
WHERE transaction_request.behavior_id = 'refund_commission'
  AND transaction_request.status = 'S'
  AND transaction.wallet_type_id = 'COMMISSION'
  AND transaction_request.updated_at IN [ngày đó, timezone Asia/Ho_Chi_Minh]
  AND transaction.user_id = [NV đó]

Đơn vị: VND (bigint)
Ví dụ: NV DV220169, ngày 20/03/2026
  - Truy thu đơn 1: 2.000.000
  - Truy thu đơn 2: 1.500.000
  → total_amount = 3.500.000

Lưu ý:
- Amount trong wallet DB luôn >= 0 (CHECK constraint)
- Pivot table hiển thị số dương (đây là "số tiền bị truy thu")
- Drill-down popup hiển thị số âm (FE negate: -amount, màu đỏ)

Edge case:
- Không có truy thu ngày đó → không có row (FE hiển thị 0)
- Approver không nhập truy thu khi duyệt hoàn → không tạo transaction → không ảnh hưởng

FORMULA-003: Tổng tháng NV (frontend)

monthly_total = SUM(total_amount) FOR ALL days in month
WHERE user_id = [NV đó]

Ví dụ: NV DV220169, tháng 03/2026
  - Ngày 01: 3.700.000
  - Ngày 02: 4.000.000
  - ... (các ngày khác)
  → monthly_total = SUM tất cả ngày

Edge case:
- NV không có data ngày nào → monthly_total = 0
- Tháng 2 nhuận (29 ngày) → cột động theo số ngày thực tế