diff --git a/.claude/WAL.md b/.claude/WAL.md index 782fa86..976eeb1 100644 --- a/.claude/WAL.md +++ b/.claude/WAL.md @@ -1,25 +1,19 @@ # WAL — auto-generated, không sửa tay updated: 2026-07-29 | session: S159 (phiên-LOGIC L8, window 1) | branch: main -goal: bookend @open 5 vòng + vá spec KHKK + thi hành (49)-(53) — TẤT CẢ XONG +goal: arc-2 — Tổng quan pipeline 4 GĐ + menu eOffice (anh chốt 4 quyết, run `2026-07-29-S159-tong-quan-pipeline-menu`) chain: -- [x] Sàn-3 ① kêu ×2 → retro-harvest 2 synthesis (H2 verify 12/12 byte-exact, 0 bịa) -- [x] ARC spec KHKK: H5 **2 site** · H6 **4 site** (site-4 lọt cả 3 vòng đếm trước) -- [x] Bookend 5 vòng: 10 vai · 9 spawn + 6 resume · 11 artifact ~231 KB -- [x] (49) 6 site 2 file LỆNH → cặp H24 VÔ-ĐIỀU-KIỆN mỗi bookend + nới bất-biến (6) + **phép (10) mới, fault-inject 4/4** -- [x] (50) seed 10/10 diary on-behalf · (51) `_INDEX` 78→80 + `Run #407` mồ-côi đã thêm (sweep 0) -- [x] (52) slot (45) RÚT (báo động giả) · (53) lọc detector `(sub|harness)-*` → INFORM 8→14 -- [x] STATUS: row RAG **TÁCH 2 trục** (service UP ⟂ SE index stale) + CURRENT S159 · HANDOFF cập nhật -- [x] dọn dư-lượng **2 LẦN** (tái phát sau `harness-audit`) → stray 0 · empty-dir 0 · agent-memory 23 vai nguyên -- [x] verify cuối: mind-check `đạt=10 TRUOT=0 co=1` · detector TOTAL 43 (không đội) · **test 562 PASS** +- [x] bookend @open 5 vòng + vá spec KHKK + (49)-(53) — XONG (run `2026-07-29-S159-bookend-open`, xem WAL cũ trong git) +- [x] anh giao arc-2 + đổi lead → Fable 5; em đọc lại đĩa (S156 21-bước + spec KHKK) → báo → anh chốt 4 câu (❶ ranh GĐ ❷ pipeline 4 GĐ ❸ ẩn 3 nhóm ❹ mở HĐ+DM không vá authz — verbatim trong run.md) +- [x] T2 prod LIVE: hide=3 upd=99 ins=376 · verify 38/38=13 canread · Pe_* 197/185 NGUYÊN · Hrm_Config* nguyên +- [x] T3 spec đổi nhãn "Đề xuất ký kết hợp đồng" + ranh GĐ + Q8-ĐÓNG (2 site) +- [!] T1 frontend-designer đang chạy background (redesign UserDashboardPage → pipeline 4 GĐ, fe-user only) +- [ ] T4: designer về → đọc đĩa sub-file → build fe-user → reviewer diff → commit/push → cicd verify -next: chờ anh xử (51-bis) chọn a/b/c cho `_INDEX` 97,45%; rồi `/session-end` đóng L8 (hoặc `/pause` nếu dừng giữa chừng) +next: chờ T1 return → verify RUỘT (chống skeleton) → build → reviewer (diff >50 LOC) → commit [CLAUDE] FE-User → push → cicd-monitor verify: -- 🔴 **`git status` MÙ với thư-mục rỗng** ⇒ close-gate PHẢI chạy RIÊNG: `find . -mindepth 2 -type d -name .claude` + `find .claude -type d -empty` -- 🔴 **Dư-lượng TÁI PHÁT theo MỖI đợt spawn** (dọn @S159 lần 1, `harness-audit` sinh lại lần 2) ⇒ đây là hiện-tượng HỆ-THỐNG, phải dọn ở **cuối mỗi wave**, không phải 1 lần/phiên -- 🔴 CẤM chép `gap-carry-dropped=3` vào tally (D-4: đơn-vị trong-phiên ⟂ consecutive-audit đang =7 ⇒ XOÁ LỊCH-SỬ) -- 🔴 grep `END` khớp cả dòng TRÍCH DẪN ⇒ kiểm **VỊ TRÍ** khớp (citation-trap, lead mắc 1 lần @S159) -- 🔴 C11 diary-0-delta hiện **9** vì detector đo *"diary không có COMMIT mới hơn"* mà 12 file vừa seed **chưa commit** — đo lại SAU khi hook commit, đừng giả định -- 🔴 **"5 FLAG" của `lead-stale` là SÀN không phải TỔNG** (chết #53 ở 5/8 bề-mặt) ⇒ coverage vòng soi-lead S159 KHÔNG xác định được +- prod menu đảo được: `UPDATE MenuItems SET IsVisible=1 WHERE [Key] IN ('Hrm','Off','Personal')` + revoke = xoá row INSERT (backup pre-apply có số liệu gốc) +- 🔴 ❹ = rủi-ro CÓ Ý THỨC owner (mở HĐ khi authz trần) — ghi run.md, PHẢI chép vào HANDOFF lúc closeout +- dọn dư-lượng spawn sau wave: `find . -mindepth 2 -type d -name .claude` + `find .claude -type d -empty` diff --git a/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md b/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md index 43209cb..96c3abd 100644 --- a/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md +++ b/.claude/workflows/runs/2026-07-28-S157-ke-hoach-ky-ket-hd/spec-ke-hoach-ky-ket-hd-28-07-2026.md @@ -24,6 +24,7 @@ Giữa **Duyệt NCC** (module PE, kết thúc `PE.DaDuyet`) và **Hợp đồng ### 1.2 Tên + mục đích (owner đặt) > **anh:** *"Bước này là bước **'đề xuất giá trị ký kết hợp đồng'**"* · sau đó gọi là **"Kế hoạch ký kết Hợp đồng"**. +> 🔄 **OWNER CHỐT NHÃN @S159 (2026-07-29, AskUser ❶):** tên phiếu/menu = **"Đề xuất ký kết hợp đồng"** — mọi label VN trong spec đọc theo tên này (tên bảng/class English `ContractSigningPlan*` GIỮ NGUYÊN). Cùng lượt anh chốt: **ranh GĐ2 = b.7→12** (b.12 chốt con số ∈ GĐ2) · GĐ3 = b.13→18 tận dụng Contract V2 · GĐ4 = b.19→21 + ký cứng **upload-only** · **b.7 email BỎ** khỏi scope, thay bằng tạo-phiếu-từ-PE-`DaDuyet` (= chính thiết kế W2 — **Q8 ĐÓNG: out-of-scope**). Đây là phát biểu **MỤC ĐÍCH**: đầu ra của khúc này = **MỘT CON SỐ CÓ PHÊ DUYỆT** (giá trị ký kết đề xuất). b.8-9 (duyệt mẫu / shopdrawing với TVGS) = **CĂN CỨ**; b.10-12 = **chuỗi duyệt chính con số đó**. @@ -219,7 +220,7 @@ attachments · `DELETE {id}` (allow-list `DangSoanThao|TuChoi`, mirror PE `:1404 `OffDeXuat`+3 con **đều trong `All`**; còn `Pe_*` sinh bằng **factory** `:150`/`:154` **NẰM NGOÀI `All`**. **Theo khuôn `Off_DeXuat`, KHÔNG theo `Pe_*`.** ⇒ 2 row canonical `docs/STATUS.md` (Menu keys · Policies = |All|×4) phải bump cùng lúc. -- **Seed `DbInitializer.cs`:** 4 dòng menu (khuôn Off_DeXuat `:1817-1820`, label VN *"Kế hoạch ký kết HĐ"* / +- **Seed `DbInitializer.cs`:** 4 dòng menu (khuôn Off_DeXuat `:1817-1820`, label VN *"Đề xuất ký kết hợp đồng"* — 🔄 nhãn owner chốt @S159, thay "Kế hoạch ký kết HĐ" / *"Danh sách"* / *"Tạo mới"* / *"Inbox duyệt"*) + admin-permission (`:2383` — **grep MỌI site seed-permission**, S155 ghi nhận có 2 site cho vài nhóm). 🔴 **GIẤU MENU Ở W1 = KHÔNG CẤP `CanRead` cho role thường** — **KHÔNG** phải `IsVisible=0`. diff --git a/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-backup-pre-apply.txt b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-backup-pre-apply.txt new file mode 100644 index 0000000..d20d2dc --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-backup-pre-apply.txt @@ -0,0 +1,30 @@ +# backup-pre-apply — prod 2026-07-29T11:14:11+07:00 + +## Permission columns +Id RoleId MenuKey CanRead CanCreate CanUpdate CanDelete +-- ------ ------- ------- --------- --------- --------- +BA21E047-5950-4599-A439-00882DB38AF7 F54055F6-B5C5-41E5-C418-08DE9F7D9CC2 Workflows 1 1 1 1 + +## Per-key CanRead counts TRUOC (38 key grant-set) +MenuKey rows_ canread +------- ----- ------- +CatalogMaterials 13 2 +Catalogs 13 2 +CatalogServices 13 2 +CatalogUnits 13 2 +CatalogWorkItems 13 2 +Contracts 1 1 +Departments 13 2 +Master 13 2 +Projects 13 2 +Suppliers 13 2 + +## Negative control TRUOC: Pe_* + IsVisible roots +pe_rows pe_canread +------- ---------- +197 185 +Key IsVisible +--- --------- +Hrm 1 +Off 1 +Personal 1 diff --git a/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-verify-post-apply.txt b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-verify-post-apply.txt new file mode 100644 index 0000000..72d1519 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/prod-verify-post-apply.txt @@ -0,0 +1,19 @@ +# verify-post-apply — prod 2026-07-29T11:15:21+07:00 · apply: hide=3 upd=99 ins=376 + +## Ky vong: moi key 13/13 canread +min_canread max_canread so_key +----------- ----------- ------ +13 13 38 + +## IsVisible 3 roots + control am Pe_* (ky vong 197/185 giu nguyen) +Key IsVisible +--- --------- +Hrm 0 +Off 0 +Personal 0 +pe_rows pe_canread +------- ---------- +197 185 +hrmcfg_canread +-------------- +7 diff --git a/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/run.md b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/run.md new file mode 100644 index 0000000..8c211f1 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/run.md @@ -0,0 +1,38 @@ +# run — S159 arc-2 · Tổng quan pipeline 4 GĐ + điều chỉnh menu eOffice + +- **run-id:** `2026-07-29-S159-tong-quan-pipeline-menu` · phiên-LOGIC **L8**, window 1 (arc thứ 2 sau bookend) +- **mode:** RUN-TRACE (≥3 task) · lead = **Fable 5** (anh `/model` — chủ dự án đổi, 0 caveat) +- **Nguồn lệnh:** ảnh chụp eoffice + text anh 2026-07-29 (đánh dấu 3 vùng đỏ) + 4 câu AskUser đã chốt + +## 🔴 OWNER CHỐT — NGUYÊN VĂN (2026-07-29, gõ vào sổ ngay để không bốc hơi — bài ring2 M-1 @S158) + +**Lệnh khung (text kèm ảnh):** +> *"Hiện điều chỉnh cái giao diện này lại theo thế này. Đổi vai main sang Fable nhé. Hiện là tổng quy trình mình đã đi (…): GĐ1: Duyệt NCC (1→6) CEO chốt đã xong · GĐ2: Đề xuất Ký kết hợp đồng (7→12) — Có 1 bước email thì bỏ đi cũng đc, thay bằng bước bắt các phiếu từ Giai đoạn 1 → Để nối tiếp làm tiếp. Đây là 1 đầu phiếu mới hoàn toàn · GĐ3: 12→18 (Duyệt Hợp đồng — Cái này mình đã có làm trước rồi, tận dụng lại) · GĐ4 (Bước còn lại): Ký cứng hợp đồng các thứ → có thể nằm ngoài do vướng chữ ký cứng, ký nháy các thứ, có thể ko cần đủ các bước chỉ cần cho upload các file đã ký cứng lên là đc. Đọc lại nắm đủ nội dung. Xong báo lại tao trước khi làm."* + +**4 câu chốt (AskUser cùng ngày):** +- **❶ Ranh GĐ:** *"Đúng như em map"* ⇒ GĐ2 = b.7→**12** (b.12 = chốt con số) · GĐ3 = b.**13**→18 · GĐ4 = b.19→21 + ký cứng upload · **tên phiếu/menu GĐ2 = "Đề xuất ký kết hợp đồng"** (spec đổi nhãn theo). +- **❷ Tổng quan:** *"Pipeline 4 GĐ đầy đủ"* — GĐ1+GĐ3 data thật · GĐ2/GĐ4 khung "sắp triển khai". +- **❸ Ẩn 3 nhóm (verbatim):** *"Chỉ nên hiển thị ở trang admin là đc → Để tao khỏi nhầm giữa cái đã public và cái đang làm"* ⇒ `IsVisible=0` cho 3 root `Hrm` `Off` `Personal` (fe-admin thấy đủ, eOffice ẩn — đúng comment entity `MenuItem.cs`). +- **❹ Mở HĐ + DANH MỤC (verbatim):** *"Hiện đang phát triển → Nên cứ cho hiển thị hết để mọi người góp ý, còn duyệt NCC → thì cứ để như cũ nhé. Phần đó đã tốt rồi."* + 🔴 **Ghi sổ rủi ro CÓ Ý THỨC:** lead đã nêu lỗ hổng authz HĐ (`ContractsController` `[Authorize]` trần 22 endpoint ghi + Reject-trước-guard, @S156) và rằng mở menu = phơi lỗ cho toàn bộ NV. **Anh tái khẳng định hiển thị hết, KHÔNG vá đợt này** (giữ "chỗ hợp đồng cứ từ từ"). Đây là quyết định chủ dự án, đã cảnh báo, chấp nhận trong giai đoạn phát triển. **PE (Duyệt NCC) KHÔNG chạm.** + +## Ánh xạ 4 GĐ ↔ đĩa (đã báo, anh xác nhận) + +| GĐ | Bước | Đĩa | +|---|---|---| +| 1 Duyệt NCC | 1→6 | ✅ module PE (`DaDuyet=7`) | +| 2 Đề xuất ký kết HĐ | 7→12 | 📋 spec KHKK 5-wave (nay đổi nhãn); b.7 email **BỎ** — thay bằng tạo-phiếu-từ-PE-`DaDuyet` = **spec W2 đã đúng sẵn** ⇒ Q8 (email external) **ĐÓNG: out-of-scope** | +| 3 Duyệt HĐ | 13→18 | ✅ tận dụng Contract V2 (verdict `LAI` @S156) | +| 4 Ký cứng | 19→21 | 🔸 ngoài hệ thống — upload file đã ký (Contract attachments sẵn nền); thiết kế sau | + +## Stages + +- [!] **T1 — FE Tổng quan pipeline** (`frontend-designer`, fe-user ONLY) — ĐANG CHẠY background +- [x] **T2 — Prod menu data** ✅ **LIVE + VERIFY TRỌN** (`prod-backup-pre-apply.txt` → apply `hide=3 upd=99 ins=376` → `prod-verify-post-apply.txt`): + · Inspect lộ thực-trạng ≠ giả định: Master subtree có matrix 13-row/key (canread 2/13) nhưng **`Ct_*` 28 key = 0 ROW** (chưa từng seed), `Contracts` root 1 row ⇒ INSERT 376 + UPDATE 99, số khớp toán từng đồng (9×11=99 · 28×13+12=376) + · Verify: **38/38 key min=max=13 canread** · 3 root `IsVisible=0` · control âm **Pe_* 197/185 NGUYÊN** (❹ "để như cũ") · `Hrm_Config*` canread=7 nguyên (vùng đang-làm loại khỏi grant, đúng ý ❸) + · Thuần cộng: chỉ thêm `CanRead=1, CanCreate/Update/Delete=0` — không rút quyền ai +- [x] **T3 — Spec đổi nhãn** ✅ 2 site (`:26` owner-block + `:222` seed label) — "Đề xuất ký kết hợp đồng"; tên English `ContractSigningPlan*` giữ; ranh GĐ + Q8-ĐÓNG ghi vào spec +- [ ] **T4 — build fe-user + reviewer diff → commit/push → cicd verify** + +🔴 Luật run: CẤM điền kết quả trước khi đo. Prod = backup-trước-apply, verify-LIVE-sau (S88). diff --git a/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-tongquan.md b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-tongquan.md new file mode 100644 index 0000000..34bfdc0 --- /dev/null +++ b/.claude/workflows/runs/2026-07-29-S159-tong-quan-pipeline-menu/sub-frontend-designer-tongquan.md @@ -0,0 +1,133 @@ +# sub-frontend-designer-tongquan — T1 FE "Tổng quan tất cả luồng duyệt" (fe-user ONLY) + +> Run: `2026-07-29-S159-tong-quan-pipeline-menu` · vai: frontend-designer · file đích: `fe-user/src/pages/UserDashboardPage.tsx` +> 🔴 GHI ĐĨA TRONG LÚC LÀM (chống #53). Mỗi mục = 1 bước đã XONG, ghi ngay khi xong. +> 🔴 **NO-SCREENSHOT** — môi trường spawn này KHÔNG có dev-server/Playwright (FD2 visual loop KHÔNG chạy được). +> Thay bằng: (i) `npm run build` fe-user PASS + tail output · (ii) tự-review tĩnh theo rubric FD4. KHÔNG bịa screenshot. + +--- + +## 1 — Khảo sát file đích (ĐÃ ĐỌC, không suy đoán) + +`fe-user/src/pages/UserDashboardPage.tsx` — 309 dòng, named export `UserDashboardPage`. Trạng thái TRƯỚC khi sửa: + +- Header: `` +- 3 query TanStack (đúng như lệnh mô tả): + - `['my-dashboard']` → `GET /reports/my-dashboard` → `MyDashboard {draftsInProgress, pendingMyApproval, dueSoon, overdue, draftsTotalValue}` (type khai LOCAL trong file, dòng 25-31). + - `['my-contracts-recent']` → `GET /contracts` params `{page:1,pageSize:5}` → `Paged`. + - `['my-pe-recent']` → `GET /purchase-evaluations` params `{page:1,pageSize:5}` → `Paged`. +- Section hiện có: (a) "Của tôi" = grid 5 `StatCard` (local component, `card-accent` + `icon-chip` + `stat-value`, tone map `STAT_TONE` 6 tone dùng CSS var `--color-*`); (b) "HĐ gần đây" list 5; (c) "Phiếu Duyệt NCC gần đây" list 5 (có `PeFinalizeChip` + PE display-status pill). +- Helper local: `fmtMoney` (tỷ/tr/locale). Import sẵn: `PhaseBadge`, `SlaTimer`, `EmptyState`, `Button`, `cn`, `ContractTypeLabel`, PE display-status helpers. + +⇒ Kế hoạch: GIỮ toàn bộ 3 query + 5 KPI + 2 list (owner: "đừng vứt"), THÊM hero pipeline 4 GĐ ở trên cùng, đổi PageHeader title. + +## 2 — Data thật cho pipeline (đọc trước khi dùng) + +- `PeListItem` (`fe-user/src/types/purchaseEvaluation.ts`) có `phase`; `PurchaseEvaluationPhase.DaDuyet = 7`, `ChoDuyet = 10`, `TraLai = 98`, `TuChoi = 99` — khớp lệnh "phase=7 = Đã duyệt". +### 🔴 2 ĐÍNH CHÍNH so với brief của lead (đã đọc đĩa, KHÔNG tin liệt kê) + +1. **`Paged` KHÔNG có `totalCount`** — field tên là **`total`**. + `fe-user/src/types/master.ts:1-9` → `{items, total, page, pageSize, totalPages, hasNext, hasPrev}`; khớp BE `PagedResult.cs:3-7` (`Total`). +2. **`pendingMe` KHÔNG phải query-param của `GET /purchase-evaluations`** — nó là **URL-param của trang list**, và trang list map nó sang **endpoint KHÁC**: `GET /purchase-evaluations/inbox` trả **mảng phẳng** `PeListItem[]` (không paging) — `PurchaseEvaluationsListPage.tsx:81-89` + `PurchaseEvaluationsController.cs:30-35`. + ⇒ Đếm "chờ tôi duyệt" = `inbox.length`, KHÔNG dùng được `pageSize=1`. + ✅ `phase` THÌ ĐÚNG là query-param BE (`PurchaseEvaluationsController.cs:23` `PurchaseEvaluationPhase? phase`), `phase=7` = Đã duyệt. + +### Nguồn số cho 4 GĐ (chốt) — chỉ **+2 request**, dưới trần ~3 + +| Ô số | Nguồn | Request thêm? | +|---|---|---| +| GĐ1 · Tổng phiếu | `total` của query **đã có** `['my-pe-recent']` (`/purchase-evaluations?page=1&pageSize=5`) | ❌ 0 (tái dùng) | +| GĐ1 · Đã duyệt | `GET /purchase-evaluations?page=1&pageSize=1&phase=7` → `.total` | ✅ +1 | +| GĐ1 · Chờ tôi duyệt | `GET /purchase-evaluations/inbox` → `.length` | ✅ +1 | +| GĐ2 (sắp) | không có số riêng — mượn ô "Đã duyệt" GĐ1 làm câu "N phiếu sẵn sàng nối tiếp" | ❌ 0 | +| GĐ3 · Tổng HĐ | `total` của query **đã có** `['my-contracts-recent']` | ❌ 0 | +| GĐ3 · Chờ tôi duyệt / Quá hạn | `['my-dashboard']` `pendingMyApproval` / `overdue` — **đã xác minh handler CHỈ đếm bảng Contracts** (`GetMyDashboardQuery.cs:51-75`, `db.Contracts`) ⇒ gán vào GĐ3 là ĐÚNG NGHĨA, không phải mượn số | ❌ 0 | +| GĐ4 (sắp) | KHÔNG gán số (owner tuyên "sắp triển khai" — không bịa data từ `ContractPhase.DaPhatHanh`) | ❌ 0 | + +### Token/DS đã nạp (FD1 — dùng, không reinvent) +`fe-user/src/index.css` `@theme`: brand-50..900 (`brand-600 = #1f7dc1`), accent-500/600 đỏ, **teal / amberx / violet / greenx chỉ có stop 50/100/500/600/700** (KHÔNG -800 — gotcha cũ). Utility sẵn: `.card-accent` (rail `--accent`, hover lift, có `prefers-reduced-motion`), `.icon-chip` (`--chip-bg`/`--chip-fg`), `.stat-value`, `.label-eyebrow`, `.app-gradient-brand`. +Icon: đã `node -e` verify 18 export lucide dùng tới (`Workflow`, `ClipboardCheck`, `FileSignature`, `Stamp`, `Hourglass`, `ChevronRight/Down`, `RefreshCw`, `ShieldCheck`…) — tránh bẫy alias-dodge. +Route đã verify tồn tại trong `App.tsx`: `/purchase-evaluations`, `/my-contracts`, `/inbox`, `/contracts/new`, `/purchase-evaluations/:id` — mọi link đều trỏ route THẬT (không đâm `*` fallback). + +## 3 — Quyết định design (trước khi code) + +- **Header:** title → **"Tổng quan tất cả luồng duyệt"**; lời chào `Xin chào, {tên}` KHÔNG vứt — dời xuống `description` cùng câu định vị 4 GĐ. +- **Hero = dải pipeline 4 GĐ:** grid `lg:grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr]` — 3 ô `auto` là mũi tên nối (`ChevronRight` ở lg, `ChevronDown` khi dọc `, phần bấm là `