Appearance
v1.4 — 27/03/2026
| Thay đổi | Section | Ả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 Formulas | BE, 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
| Version | Date | Author | Thay đổi |
|---|---|---|---|
| 1.0 | 2026-03-18 | PO/BA | Initial — chuyển từ design doc Mode A |
| 1.2 | 2026-03-23 | PO/BA | Bổ sung truy thu commission (wallet DB) + drill-down popup chi tiết ngày |
| 1.3 | 2026-03-23 | PO/BA | Gom thành hub "Báo cáo doanh số cá nhân" — 3 tabs, ẩn 2 cards cũ, redirect route |
| 1.4 | 2026-03-27 | PO/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 sections | Trách nhiệm |
|---|---|---|
| PO/BA | Z, A, B | Approve requirements, UX flows |
| Tech Lead | Z, A, C | Approve architecture, review dev spec |
| FE Dev | B, C (C1-C5) | Implement UI |
| BE Dev | C (C1-C11) | Implement SQL functions, Hasura metadata |
| QA | A (A5), D | Write + 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:
- Doanh số theo ngày (default, build mới): Pivot table NV × ngày cho commission, tour, truy thu — filter, drill-down, export Excel
- Doanh thu theo đơn hàng: Import
EmployeeRevenueReportcomponent hiện có - Tiền tour: Import
TourIncomeReportcomponent 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:
- 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ũ
- Pivot Table: 3 SQL functions (commission + tour + truy thu) trả cùng shape + pivot table động
- 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)
- Export: Xuất Excel giống y hệt view trên màn hình
Milestones
| Milestone | Target | Owner | Điều kiện |
|---|---|---|---|
| BE — SQL functions + Hasura (ecommerce + project) | T+3 ngày | BE Dev | Deploy 2 migrations + metadata |
| BE — SQL function clawback + Hasura (wallet) | T+4 ngày | BE Dev | Deploy wallet migration + metadata |
| FE — Tab container + import 2 báo cáo cũ + redirect route | T+5 ngày | FE Dev | Song song với BE |
| FE — Pivot table (tab "Doanh số theo ngày") + filter | T+7 ngày | FE Dev | Sau khi BE deploy xong |
| FE — Drill-down popup | T+9 ngày | FE Dev | Sau khi table hoạt động |
| FE — Export Excel | T+10 ngày | FE Dev | Sau khi popup hoạt động |
| QA + Go-Live | T+13 ngày | QA | QA pass |
Trạng thái Sign-off
| Domain | Người | Status |
|---|---|---|
| Business | PO | ✅ Approved (via design doc review) |
| Tech | Tech Lead | Pending |
| QA | QA Lead | Pending |
Pending Decisions
| ID | Nội dung | Owner | Deadline | Status |
|---|---|---|---|---|
| PD-001 | Xác nhận transaction_request.code có auto-generate cho refund_commission không. Nếu không → dùng UUID fallback | BE Dev | Trước FE popup | Open |
Backlog Phase 2 (Out-of-scope)
| # | Tính năng | Lý do defer |
|---|---|---|
| 1 | So sánh giữa các tháng (month-over-month) | Scope riêng, cần thêm UI |
| 2 | Biểu đồ (chart) trực quan | Pivot table đủ cho yêu cầu hiện tại |
| 3 | Thêm loại doanh số khác (dịch vụ, mỹ phẩm) | Data structure khác, cần analysis riêng |
| 4 | KPI target vs actual | Cần tích hợp module KPI |
| 5 | Đã chuyển vào scope v1.1 (DEC-010) | |
| 6 | Dropdown "Doanh số thực nhận" (commission − truy thu gộp) | Cần cross-DB aggregation |
| 7 | Highlight ô có truy thu trong view commission | Nice-to-have |
| 8 | Filter chức vụ cho "Truy thu commission" | Wallet DB không có department data |
| 9 | Auto-generate transaction_request.code cho refund_commission | Hiện dùng UUID fallback |
Z) Decision Log
| ID | Category | Quyết định | Lý do | Ngày | Status |
|---|---|---|---|---|---|
| DEC-001 | Technical | Tạ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-18 | Locked |
| DEC-002 | Technical | 2 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 complexity | 2026-03-18 | Locked |
| DEC-003 | Technical | Tour function đọc trực tiếp project_task_assignee + project_task (DB project), KHÔNG wrap search_report_tour_income | Tránh cross-database function call (commission ở ecommerce, tour ở project) | 2026-03-18 | Locked |
| DEC-004 | Technical | Commission function ở DB ecommerce, tour function ở DB project | Mỗi function nằm cùng DB với data source — tránh cross-database | 2026-03-18 | Locked |
| DEC-005 | UX | Không phân trang — hiển thị tất cả NV, scroll vertical | HCNS cần nhìn toàn cảnh, export cũng cần tất cả data | 2026-03-18 | Locked |
| DEC-006 | Technical | MonthPickerWithButton import cross-module từ salary | Component đã mature, không cần duplicate | 2026-03-18 | Locked |
| DEC-007 | Technical | Hasura track 2 functions as custom function (root-level query) | Giống pattern search_dashboard_sales_revenue đã có | 2026-03-18 | Locked |
| DEC-008 | Business | Truy 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-23 | Locked |
| DEC-009 | UX | Truy 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ễu | 2026-03-23 | Locked |
| DEC-010 | UX | Drill-down popup khi click ô số tiền — chuyển từ Phase 2 vào Phase 1 | PO yêu cầu bổ sung; cần thiết để HCNS đối soát chi tiết | 2026-03-23 | Locked |
| DEC-011 | UX | Gom 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-tab | 2026-03-23 | Locked |
| DEC-012 | Business | Loạ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ố NV | 2026-03-27 | Locked |
A) PRD
A1) Blueprint
| Field | Value |
|---|---|
| Feature | Báo cáo doanh số cá nhân (Hub 3 tabs: Doanh số theo ngày + Doanh thu theo ĐH + Tiền tour) |
| Type | New Feature + Restructure |
| Platform | Web Admin (diva-admin) |
| Module ảnh hưởng | report (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àyEmployeeProfileCommission: xem commission từng NV riêng lẻ — phải mở từng profileEmployeeProfileTourIncome: 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
EmployeeRevenueReportcomponent (giữ nguyên logic) - Tab "Tiền tour": import
TourIncomeReportcomponent (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
| Goal | Metric | Target |
|---|---|---|
| HCNS xem tổng quan doanh số NV nhanh | Thờ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ông | Số lần HCNS export + pivot thủ công / tháng | 0 lần (hiện tại: 2-4 lần/tháng) |
| Export chính xác | Tỉ lệ data match giữa hệ thống vs Excel thủ công | 100% |
A4) Personas
| Persona | Vai trò | JTBD | Frequency |
|---|---|---|---|
| HCNS | Quản lý nhân sự, đánh giá hiệu suất | Xem tổng doanh số commission/tour NV theo ngày để đánh giá, báo cáo BOD | Hàng tháng |
| Quản lý chi nhánh | Theo dõi NV trong branch | Xem doanh số NV chi nhánh mình để coaching | Hà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_codeASC - [ ] 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
.xlsxformat 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:
transactionJOINtransaction_request(giống pattern vieworder_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 timezoneAsia/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
EmployeeRevenueReportcomponent hiện có - [ ] Tab "Tiền tour" → import + render
TourIncomeReportcomponent 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_userJOINinvoice(dùngpaid_atvới timezoneAsia/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_assigneeJOINproject_task(dùngdone_atvới timezoneAsia/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
| ID | Assumption | Owner xác nhận |
|---|---|---|
| ASM-001 | HCNS đã có quyền xem report — không cần thêm permission mới | PO |
| ASM-002 | order_commission_user.invoice_id luôn NOT NULL cho data gần đây (migration 1742112936829 đã thêm cột) | TL — verify |
| ASM-003 | Index trên invoice.paid_at và order_commission_user.invoice_id đã tồn tại | TL — verify |
A7) Risks
| ID | Risk | Impact | Probability | Mitigation |
|---|---|---|---|---|
| RSK-001 | Query chậm khi nhiều NV + nhiều commission records | Trung bình | Thấp | SQL đã aggregate sẵn; index trên paid_at; worst case 500 NV × 31 ngày = 15.5K rows |
| RSK-002 | order_commission_user.invoice_id NULL cho data cũ → thiếu doanh số | Trung bình | Trung bình | INNER JOIN loại records cũ — chấp nhận chỉ báo cáo từ khi có invoice_id |
| RSK-003 | Cross-module import MonthPickerWithButton gây coupling | Thấp | Thấp | Nếu gây issue → copy component sang shared |
A8) Metrics (Post-launch)
| Metric | Cách đo | Target | Khi nào đo |
|---|---|---|---|
| Adoption rate | Số lần HCNS mở page / tháng | > 10 lần/tháng | T+4 tuần |
| Export usage | Số lần click "Tải xuống" / tháng | > 5 lần/tháng | T+4 tuần |
| Query performance | EXPLAIN ANALYZE execution time | < 2 giây | Ngay sau deploy |
A9) Glossary
| Thuật ngữ (VI) | Thuật ngữ (EN) | Định nghĩa | Phân biệt với |
|---|---|---|---|
| Hoa hồng tư vấn | Consulting Commission | Số 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 tour | Tour Income | Tiề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 commission | Commission Clawback | Số 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 table | Pivot Table | Bả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
| Deliverable | PO | TL | FE Dev | BE Dev | QA |
|---|---|---|---|---|---|
| PRD (file này) | A | C | I | I | I |
| UI Spec | C | I | R | I | I |
| Dev Spec | I | A | C | R | I |
| QA Test Plan | C | I | I | I | R |
| Migration SQL | I | A | — | R | I |
| Hasura Metadata | I | A | — | R | — |
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ưởngFORMULA-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ế