Compare commits
2 Commits
b5799fc5d3
...
1a47a61c21
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a47a61c21 | |||
| b2a86feab6 |
@ -27,7 +27,16 @@
|
||||
|
||||
## Stages
|
||||
|
||||
- [!] **T1 — FE Tổng quan pipeline** (`frontend-designer`, fe-user ONLY) — ĐANG CHẠY background
|
||||
- [x] **T1 — FE Tổng quan pipeline** ✅ — `frontend-designer` land `UserDashboardPage.tsx` **585+/143−** (1 file, 0 dep mới, khai `NO-SCREENSHOT` trung thực); return dính #53 nhưng **file chính + sub-file 15.803B đã trên đĩa TRƯỚC khi chết** — mất 0 B. ⭐ Designer **BÁC 2 dữ kiện lead đưa** (đọc BE thật): `pendingMe` KHÔNG phải query-param (là endpoint `/inbox` mảng phẳng) · `Paged<T>` field là `total` không phải `totalCount`.
|
||||
- [x] **T4a — reviewer diff** ✅ `PASS_WITH_FLAGS — 7 FLAG (0H/2M/5L)` (`sub-reviewer-diff-tongquan.md` 16.761B; §A 12-claim verify độc lập + 7 giả-thuyết-tự-bác; chết #53 ×1 giữa chừng, resume sạch). **Disposition TỪNG DÒNG:**
|
||||
· FLAG-1 [M] aria trùng tên → **FIX**: prop `group` BẮT BUỘC + `aria-label` (TS tự bắt đủ 6 call-site)
|
||||
· FLAG-2 [M] 2 chuỗi Anh → **FIX**: "Toàn trình 4 giai đoạn" · "Tải lên bản hợp đồng đã ký cứng"
|
||||
· FLAG-3 [L] câu GĐ2 nói quá → **FIX**: bỏ mệnh đề "sẵn sàng nối tiếp"
|
||||
· FLAG-5 [L] queryKey đôi → **FIX SÂU HƠN đề nghị**: 🔴 đề nghị gốc (dùng chung key, giữ queryFn trả `.length`) sẽ **VỠ CACHE** InboxPage (cache mảng ⟂ number cùng key) — fix đúng = chung key + chung queryFn (mảng) + `select: d => d.length`; request +2→+1
|
||||
· FLAG-4 [L] đích `/inbox` số lệch nguồn → **PARK** (lệch có sẵn pre-diff, fix cần param/endpoint mới — ngoài scope, ghi nợ)
|
||||
· FLAG-6 [L] 2 nút Thử-lại trùng → **PARK** (reviewer tự đính chính giữa chừng; cosmetic, chấp nhận)
|
||||
· FLAG-7 [L] nhãn GĐ4 → **NO_CHANGE** (đúng spec owner ❷/❹ — GĐ4 cố ý ngoài hệ thống)
|
||||
- [x] **T4b — build độc lập SAU mọi edit** ✅ PASS 454ms (`tsc -b && vite build`) · 6/6 `group` land · diagnostics IDE giữa chừng = snapshot dở (#68), bác bằng compiler thật
|
||||
- [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 ý ❸)
|
||||
|
||||
@ -46,4 +46,78 @@
|
||||
|
||||
## B — FLAG
|
||||
|
||||
<!-- IN PROGRESS — file này hợp lệ giữa chừng; dòng END ở cuối mới là chốt -->
|
||||
### FLAG-1 [M] — a11y AA: hai nút khác đích nhưng TRÙNG tên đọc được
|
||||
|
||||
`fe-user/src/pages/UserDashboardPage.tsx:449` (GĐ1 → `/purchase-evaluations?pendingMe=1`) và `:495` (GĐ3 → `/inbox`) đều là `<button>` có tên khả truy cập **y hệt nhau**: "Chờ tôi duyệt". Thêm nút thứ ba cùng nhãn ở KPI (`:538`). Ngữ cảnh phân biệt (tiêu đề "Duyệt NCC" / "Duyệt Hợp đồng") chỉ tồn tại **bằng thị giác** — `<h3>` của thẻ giai đoạn (`:153`) không được liên kết chương trình với nhóm nút, nên người dùng trình đọc màn hình duyệt theo danh sách nút sẽ thấy 2-3 mục trùng tên dẫn tới 3 nơi khác nhau. Spec yêu cầu a11y AA nên đây là khoản nợ thật, không phải chuyện thẩm mỹ.
|
||||
**Fix đề nghị:** thêm `aria-label` đầy đủ cho `StageStatRow` (ví dụ `aria-label={`${title} — ${label}`}`), hoặc bọc cụm số liệu bằng `role="group"` + `aria-labelledby` trỏ vào `id` của `<h3>` giai đoạn.
|
||||
|
||||
### FLAG-2 [M] — lọt chữ tiếng Anh ra giao diện (ràng buộc "100% tiếng Việt")
|
||||
|
||||
Tôi rút toàn bộ chuỗi hiển thị MỚI từ diff (`git diff | grep '^+' | grep -oE '>...<|label="..."|title="..."|description="..."'`) rồi soi từng chuỗi. Có đúng **2 chuỗi lọt tiếng Anh**:
|
||||
- `:405` — `<h2>Pipeline 4 giai đoạn · 21 bước</h2>`: **"Pipeline"**. Nó lại nằm ngay dưới dòng eyebrow "Quy trình mua sắm → ký kết hợp đồng" (`:402-404`) nên vừa là tiếng Anh vừa lặp ý.
|
||||
- `:520` — `description="Upload bản hợp đồng đã ký cứng."`: **"Upload"**.
|
||||
|
||||
Mọi chuỗi còn lại (14 nhãn/tiêu đề) đều thuần Việt, kể cả "Giai đoạn {n}", "Sắp triển khai", các thông báo lỗi. Lưu ý bối cảnh: "upload các file đã ký cứng" là chữ owner dùng trong lời thoại gốc (`run.md:10`), nên đây là câu hỏi biên tập cho owner chứ không phải implementer làm sai ý; còn "Pipeline" thì owner chỉ nói trong lời trao đổi nội bộ (`run.md:14` — "Pipeline 4 GĐ đầy đủ"), không phải chốt nhãn hiển thị.
|
||||
**Fix đề nghị:** `:405` → "Quy trình 4 giai đoạn · 21 bước" (bỏ luôn trùng ý với eyebrow); `:520` → "Tải lên bản hợp đồng đã ký cứng." — hoặc owner xác nhận giữ nguyên chữ "Upload".
|
||||
|
||||
### FLAG-3 [L] — câu "sẵn sàng nối tiếp" ở GĐ2 nói QUÁ điều dữ liệu chứng minh được
|
||||
|
||||
`:467-471` — dòng gợi ý của GĐ2 in `${peApproved} phiếu Duyệt NCC đã duyệt sẵn sàng nối tiếp`, trong đó `peApproved` là **tổng mọi phiếu `phase=DaDuyet`**. Nhưng phiếu đã duyệt có thể ĐÃ sinh hợp đồng rồi: entity có sẵn cột `PurchaseEvaluation.ContractId` (`src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs:31` — *"FK Contracts — set khi user gen HĐ từ phiếu"*), và endpoint `POST /purchase-evaluations/{id}/contracts-from-evaluation` (`PurchaseEvaluationsController.cs:340`) chính là đường sinh ra nó. Vậy con số đang gộp cả phiếu "đã nối tiếp xong" vào nhóm "sẵn sàng nối tiếp" — càng dùng lâu số càng phồng và càng sai nghĩa.
|
||||
Không thể lọc bằng endpoint hiện có (spec cấm thêm endpoint), nên cách rẻ nhất là sửa câu chữ.
|
||||
**Fix đề nghị:** đổi thành "{n} phiếu Duyệt NCC đã duyệt" (bỏ mệnh đề "sẵn sàng nối tiếp"), hoặc chờ GĐ2 có thật rồi thêm tham số lọc `hasContract=false` ở BE.
|
||||
|
||||
### FLAG-4 [L] — bấm số ở GĐ3 sẽ ra màn hình có số KHÁC (lệch có sẵn từ trước, nay bị nhân đôi độ nổi bật)
|
||||
|
||||
Hai ô của GĐ3 lấy số từ `/reports/my-dashboard` nhưng lại dẫn sang `/inbox` — hai nguồn đếm theo hai luật khác nhau:
|
||||
- `:495-501` "Chờ tôi duyệt" = `pendingMyApproval`, handler **loại trừ hợp đồng do chính tôi soạn** (`GetMyDashboardQuery.cs` — `eligiblePhases.Contains(c.Phase) && c.DrafterUserId != userId`). Còn `/contracts/inbox` **không** loại trừ (`ContractFeatures.cs:391-401` chỉ lọc `eligiblePhases.Contains(c.Phase)`), lại còn `.Take(100)`.
|
||||
- `:503-510` "Đã quá hạn" = `overdue` tính trên hợp nhất *hợp đồng tôi soạn* ∪ *hợp đồng chờ tôi duyệt*; phần "tôi soạn" **không bao giờ xuất hiện** trong hộp thư đến.
|
||||
|
||||
Nghĩa là số 3 trên trang chủ có thể mở ra hộp thư 7 dòng, hoặc 0 dòng quá hạn. Đây là lệch **có sẵn trước diff** (thẻ KPI cũ `:542`, `:557` cũng trỏ `/inbox`), nên tôi không tính là hồi quy — nhưng hero vừa nâng nó lên vị trí nổi nhất trang và lặp lại đúng con số đó hai lần trên cùng một màn.
|
||||
**Fix đề nghị:** hoặc đổi đích sang `/my-contracts` cho ô "Đã quá hạn", hoặc để hộp thư nhận tham số lọc (`/inbox?overdue=1`) — nếu chưa làm ngay thì ghi vào backlog, đừng để trôi.
|
||||
|
||||
### FLAG-5 [L] — cùng một request nhưng hai khoá cache, thay vì dùng lại khoá đã có
|
||||
|
||||
`:367-370` tạo `queryKey: ['pipeline-pe-inbox']` cho `GET /purchase-evaluations/inbox`. Nhưng `fe-user/src/pages/InboxPage.tsx:77-78` **đã** có `queryKey: ['pe-inbox']` gọi đúng endpoint đó, đúng bộ tham số đó (không tham số). Hai khoá khác nhau ⇒ hai bản cache độc lập cho cùng một tài nguyên: mỗi lần đi Trang chủ ↔ Hộp thư đều bắn thêm một request, và hai màn có thể hiển thị hai ảnh chụp khác thời điểm.
|
||||
Yêu cầu "queryKey mới không đè cache trang list" vẫn **thoả** (`['pe-list', …]` không bị đụng) — đây là chuyện tiết kiệm và nhất quán, không phải vi phạm spec.
|
||||
**Fix đề nghị:** dùng lại `['pe-inbox']` cho ô "Chờ tôi duyệt" của GĐ1 ⇒ số request thêm rơi từ +2 xuống +1 và hai màn luôn khớp nhau.
|
||||
|
||||
### FLAG-6 [L] — báo lỗi hiện hai lần cho cùng một sự cố
|
||||
|
||||
`:409-420` băng cảnh báo `hasError` gộp cả 5 query, trong đó có `recent` và `recentPe`. Khi đúng một danh sách hỏng, người dùng thấy đồng thời băng vàng ở hero ("Không tải được một vài số liệu") **và** khối lỗi riêng của danh sách đó (`:559-573` cho hợp đồng, phần tương ứng cho phiếu), mỗi chỗ một nút "Thử lại". Không sai chức năng — `retryFailed` chỉ gọi lại đúng query hỏng — nhưng là nhiễu.
|
||||
**Tự đính chính trong lúc viết flag này:** tôi định đề nghị "bỏ `recent`/`recentPe` khỏi `hasError`", nhưng đọc lại thì **sai** — `recent.data.total` nuôi ô "Tổng hợp đồng" (`:488`) và `recentPe.data.total` nuôi ô "Tổng phiếu" (`:434`), tức hai query đó thực sự thuộc hero. Băng cảnh báo phủ chúng là ĐÚNG.
|
||||
**Fix đề nghị (đã sửa theo đúng sự thật):** giữ nguyên `hasError`, chỉ bỏ bớt một trong hai nút "Thử lại" trùng nhau — hoặc chấp nhận trùng vì nó vô hại. Mức L, không chặn commit.
|
||||
|
||||
### FLAG-7 [L] — GĐ4 gắn nhãn "Sắp triển khai" trong khi phần khung của nó đã chạy thật (việc của owner quyết, không phải lỗi người làm)
|
||||
|
||||
`:519-521` đánh GĐ4 "Ký cứng & phát hành (Bước 19 → 21)" là chưa triển khai. Trên thực tế module Hợp đồng đã có sẵn hai phase cuối `DangDongDau(8) → DaPhatHanh(9)` cùng chức năng đính kèm bản scan đã ký (`POST /contracts/{id}/attachments`). Người dùng đang dùng hai phase này mỗi ngày có thể ngơ ngác khi trang chủ bảo phần đó "sắp triển khai".
|
||||
Diff **khớp đúng spec** ở điểm này — owner tự chốt GĐ4 nằm ngoài hệ thống (`run.md:10` và bảng `run.md:26`: *"ngoài hệ thống — upload file đã ký (Contract attachments sẵn nền); thiết kế sau"*) — nên tôi ghi nhận là điểm cần owner biết, **không** tính là implementer làm sai.
|
||||
**Fix đề nghị:** nếu owner muốn tránh hiểu nhầm, đổi mô tả GĐ4 thành "Hiện làm ngoài hệ thống — đính kèm bản đã ký vào hồ sơ hợp đồng" thay vì chỉ badge "Sắp triển khai".
|
||||
|
||||
---
|
||||
|
||||
## C — Quan sát KHÔNG tính FLAG (ghi để khỏi bị đào lại)
|
||||
|
||||
- **Mũi tên nối `text-slate-400` (~2,6:1)** ở `:165-166`: KHÔNG vi phạm AA vì cả hai icon đều `aria-hidden` và thứ tự giai đoạn đã được mã hoá bằng badge chữ "Giai đoạn 1..4"; đây là đồ hoạ trang trí thuần tuý.
|
||||
- **Nợ FD tự khai** (chiều cao dải pipeline ở tablet 768–1023px; số đỏ "quá hạn" nằm trong thẻ teal): tôi không phản bác được bằng cách soi tĩnh — cần mắt người trên prod. Ghi nhận là nợ đã khai minh bạch, không phải giấu.
|
||||
- **Trùng số liệu hero ⟂ KPI**: "Chờ tôi duyệt" và "Đã quá hạn" nay xuất hiện hai lần trên một màn. Owner đã yêu cầu giữ KPI cũ nên đây là hệ quả cố ý, không phải sơ suất.
|
||||
|
||||
---
|
||||
|
||||
## D — Kết luận
|
||||
|
||||
**Verdict: PASS_WITH_FLAGS — 7 FLAG (0 H / 2 M / 5 L). Không có gì chặn commit.**
|
||||
|
||||
Bốn trục soi được lệnh giao, kết quả từng trục:
|
||||
|
||||
| Trục | Kết quả | Căn cứ |
|
||||
|---|---|---|
|
||||
| Đúng-spec (4 nhãn + ranh bước) | **ĐẠT** | GĐ2 đúng nguyên văn "Đề xuất ký kết hợp đồng" (`:464`, khớp `run.md:13`); ranh 1→6 / 7→12 / 13→18 / 19→21 đúng từng chữ; tổng 21 bước cộng lại khớp. GĐ2/GĐ4 dùng badge xám nét đứt + biểu tượng đồng hồ cát, KHÔNG dùng đỏ/vàng ⇒ đọc ra "kế hoạch", không đọc nhầm thành lỗi. |
|
||||
| Data đúng | **ĐẠT** | `phase=7` đúng là Đã-duyệt; `pendingMe` được xử lý đúng bản chất (endpoint riêng trả mảng, không phải query-param); đọc `total` đúng tên field của `Paged<>`; không có phép chia nào nên không sinh NaN; skeleton chỉ nằm trong từng dòng số, khung 4 giai đoạn luôn hiện. |
|
||||
| Không hồi quy KPI cũ | **ĐẠT** | Đủ 5 trường `MyDashboardDto` (`:532`, `:539`, `:547`, `:555`, `:562`) + đủ 2 danh sách gần đây, lại còn thêm nhánh báo lỗi mà bản cũ thiếu. |
|
||||
| Vệ sinh query / TS / React | **ĐẠT** | Khoá cache mới không đụng khoá cũ; +2 request (trần 3); `refetchOnWindowFocus:false` nên không bão request; named export `UserDashboardPage` giữ nguyên; 0 import từ `fe-admin`; 0 import thừa (tôi đối chiếu từng icon với chỗ dùng). |
|
||||
|
||||
**Kiểm chứng độc lập tôi tự chạy (không tin lời khai của FD):** `npx tsc --noEmit -p tsconfig.app.json` trong `fe-user` → **exit 0, output 0 byte**. Đây là bản chạy riêng của tôi, không dùng lại kết quả build 506 ms mà lead đã chạy.
|
||||
|
||||
**Giới hạn phải khai:** không live-verify được (route `/dashboard` đòi đăng nhập, FE chưa deploy). Mọi kết luận về bố cục, tương phản thực tế và hành vi runtime là **suy luận từ mã nguồn**, chưa có một khung hình nào được nhìn bằng mắt.
|
||||
|
||||
<!-- END sub-reviewer-diff-tongquan · TOTAL=7 FLAG -->
|
||||
|
||||
@ -18,6 +18,7 @@ import { getErrorMessage } from '@/lib/apiError'
|
||||
import { cn } from '@/lib/cn'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { AttachmentPreviewDialog, isPreviewable } from './AttachmentPreviewDialog'
|
||||
import { PePipelineStrip } from './PePipelineStrip'
|
||||
import {
|
||||
PeAttachmentPurpose,
|
||||
PeAttachmentPurposeLabel,
|
||||
@ -272,6 +273,8 @@ export function PeDetailTabs({
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
{/* [S159] dải 4 giai đoạn toàn trình — owner vẽ tab lên ảnh phiếu (phiếu PE ⇒ GĐ1 active) */}
|
||||
<PePipelineStrip current={1} />
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
65
fe-admin/src/components/pe/PePipelineStrip.tsx
Normal file
65
fe-admin/src/components/pe/PePipelineStrip.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
// PePipelineStrip — dải 4 giai đoạn toàn trình, đặt trên đầu detail phiếu Duyệt NCC.
|
||||
// [S159 2026-07-29] Owner vẽ tab strip trực tiếp lên ảnh phiếu + chốt tên qua chat:
|
||||
// "Duyệt NCC | Kế hoạch ký kết HĐ | Duyệt hợp đồng | Hợp đồng cứng"
|
||||
// (2 chỉnh verbatim: "Trình ký Hợp đồng đổi thành Duyệt hợp đồng, ký cứng -> Hợp đồng cứng").
|
||||
// MIRROR ×2 app BYTE-IDENTICAL (cùng họ PeDetailTabs md5-parity) — sửa file này
|
||||
// thì sửa CẢ HAI, verify md5 trước commit.
|
||||
// v1: phiếu PE ⇒ giai đoạn 1 active. GĐ2/GĐ4 chưa có module (badge "Sắp");
|
||||
// GĐ3 có module HĐ nhưng CHƯA nối per-phiếu (cầu PE→KHKK→HĐ ship ở wave GĐ2)
|
||||
// ⇒ mọi stage không-active đều KHÔNG click được — hiển thị lộ trình, không giả link.
|
||||
import { Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
const STAGES = [
|
||||
{ n: 1, label: 'Duyệt NCC', soon: false },
|
||||
{ n: 2, label: 'Kế hoạch ký kết HĐ', soon: true },
|
||||
{ n: 3, label: 'Duyệt hợp đồng', soon: false },
|
||||
{ n: 4, label: 'Hợp đồng cứng', soon: true },
|
||||
] as const
|
||||
|
||||
export function PePipelineStrip({ current = 1 }: { current?: number }) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
aria-label="Toàn trình 4 giai đoạn"
|
||||
className="flex items-stretch gap-1 overflow-x-auto rounded-t-lg border-b border-slate-200 bg-slate-50/70 px-2 py-1.5"
|
||||
>
|
||||
{STAGES.map((s, i) => {
|
||||
const active = s.n === current
|
||||
return (
|
||||
<div key={s.n} role="listitem" className="flex min-w-0 flex-1 items-center gap-1">
|
||||
<div
|
||||
aria-current={active ? 'step' : undefined}
|
||||
title={
|
||||
active
|
||||
? 'Giai đoạn hiện tại'
|
||||
: s.soon
|
||||
? 'Sắp triển khai'
|
||||
: 'Sẽ nối khi giai đoạn trước hoàn tất'
|
||||
}
|
||||
className={cn(
|
||||
'flex w-full min-w-0 items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-center text-[11px] font-medium leading-tight',
|
||||
active
|
||||
? 'bg-brand-600 text-white shadow-sm'
|
||||
: 'border border-dashed border-slate-300 bg-white text-slate-500',
|
||||
)}
|
||||
>
|
||||
{active && <Check aria-hidden className="h-3 w-3 shrink-0" />}
|
||||
<span className="truncate">{s.label}</span>
|
||||
{s.soon && !active && (
|
||||
<span className="shrink-0 rounded bg-slate-100 px-1 text-[9px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
Sắp
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{i < STAGES.length - 1 && (
|
||||
<span aria-hidden className="shrink-0 px-0.5 text-slate-300">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -57,6 +57,11 @@ function resolvePath(key: string): string | null {
|
||||
Dashboard: '/dashboard',
|
||||
Contracts: '/my-contracts',
|
||||
PurchaseEvaluations: '/purchase-evaluations',
|
||||
// [S159 2026-07-29] 2 root placeholder giai đoạn 2/4 (owner: menu hiển thị đúng 4 GĐ
|
||||
// như tab strip). Module chưa build → trỏ về Tổng quan pipeline (khung "Sắp triển khai").
|
||||
// LESSON gotcha #50: key DB mới PHẢI có route ở đây, không thì sidebar drop SILENT.
|
||||
KeHoachKyKet: '/dashboard',
|
||||
HopDongCung: '/dashboard',
|
||||
// [Plan CA Hotfix 1 S29 2026-05-22] 4 master + 4 catalog leaf moved từ
|
||||
// fe-admin → fe-user. resolvePath PHẢI có route mapping nếu không
|
||||
// MenuLeaf line 238 `if (!path) return null` → sidebar drop silent.
|
||||
|
||||
@ -18,6 +18,7 @@ import { getErrorMessage } from '@/lib/apiError'
|
||||
import { cn } from '@/lib/cn'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { AttachmentPreviewDialog, isPreviewable } from './AttachmentPreviewDialog'
|
||||
import { PePipelineStrip } from './PePipelineStrip'
|
||||
import {
|
||||
PeAttachmentPurpose,
|
||||
PeAttachmentPurposeLabel,
|
||||
@ -272,6 +273,8 @@ export function PeDetailTabs({
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
{/* [S159] dải 4 giai đoạn toàn trình — owner vẽ tab lên ảnh phiếu (phiếu PE ⇒ GĐ1 active) */}
|
||||
<PePipelineStrip current={1} />
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
65
fe-user/src/components/pe/PePipelineStrip.tsx
Normal file
65
fe-user/src/components/pe/PePipelineStrip.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
// PePipelineStrip — dải 4 giai đoạn toàn trình, đặt trên đầu detail phiếu Duyệt NCC.
|
||||
// [S159 2026-07-29] Owner vẽ tab strip trực tiếp lên ảnh phiếu + chốt tên qua chat:
|
||||
// "Duyệt NCC | Kế hoạch ký kết HĐ | Duyệt hợp đồng | Hợp đồng cứng"
|
||||
// (2 chỉnh verbatim: "Trình ký Hợp đồng đổi thành Duyệt hợp đồng, ký cứng -> Hợp đồng cứng").
|
||||
// MIRROR ×2 app BYTE-IDENTICAL (cùng họ PeDetailTabs md5-parity) — sửa file này
|
||||
// thì sửa CẢ HAI, verify md5 trước commit.
|
||||
// v1: phiếu PE ⇒ giai đoạn 1 active. GĐ2/GĐ4 chưa có module (badge "Sắp");
|
||||
// GĐ3 có module HĐ nhưng CHƯA nối per-phiếu (cầu PE→KHKK→HĐ ship ở wave GĐ2)
|
||||
// ⇒ mọi stage không-active đều KHÔNG click được — hiển thị lộ trình, không giả link.
|
||||
import { Check } from 'lucide-react'
|
||||
import { cn } from '@/lib/cn'
|
||||
|
||||
const STAGES = [
|
||||
{ n: 1, label: 'Duyệt NCC', soon: false },
|
||||
{ n: 2, label: 'Kế hoạch ký kết HĐ', soon: true },
|
||||
{ n: 3, label: 'Duyệt hợp đồng', soon: false },
|
||||
{ n: 4, label: 'Hợp đồng cứng', soon: true },
|
||||
] as const
|
||||
|
||||
export function PePipelineStrip({ current = 1 }: { current?: number }) {
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
aria-label="Toàn trình 4 giai đoạn"
|
||||
className="flex items-stretch gap-1 overflow-x-auto rounded-t-lg border-b border-slate-200 bg-slate-50/70 px-2 py-1.5"
|
||||
>
|
||||
{STAGES.map((s, i) => {
|
||||
const active = s.n === current
|
||||
return (
|
||||
<div key={s.n} role="listitem" className="flex min-w-0 flex-1 items-center gap-1">
|
||||
<div
|
||||
aria-current={active ? 'step' : undefined}
|
||||
title={
|
||||
active
|
||||
? 'Giai đoạn hiện tại'
|
||||
: s.soon
|
||||
? 'Sắp triển khai'
|
||||
: 'Sẽ nối khi giai đoạn trước hoàn tất'
|
||||
}
|
||||
className={cn(
|
||||
'flex w-full min-w-0 items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-center text-[11px] font-medium leading-tight',
|
||||
active
|
||||
? 'bg-brand-600 text-white shadow-sm'
|
||||
: 'border border-dashed border-slate-300 bg-white text-slate-500',
|
||||
)}
|
||||
>
|
||||
{active && <Check aria-hidden className="h-3 w-3 shrink-0" />}
|
||||
<span className="truncate">{s.label}</span>
|
||||
{s.soon && !active && (
|
||||
<span className="shrink-0 rounded bg-slate-100 px-1 text-[9px] font-semibold uppercase tracking-wide text-slate-400">
|
||||
Sắp
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{i < STAGES.length - 1 && (
|
||||
<span aria-hidden className="shrink-0 px-0.5 text-slate-300">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -2,9 +2,9 @@
|
||||
// [S159 2026-07-29 — owner chốt qua AskUser] Hero = dải pipeline 4 GIAI ĐOẠN xuyên
|
||||
// suốt quy trình mua sắm → ký kết hợp đồng:
|
||||
// GĐ1 Duyệt NCC (bước 1 → 6, CEO chốt) 🟢 LIVE — module PE
|
||||
// GĐ2 Đề xuất ký kết hợp đồng (bước 7 → 12) ⏳ sắp triển khai
|
||||
// GĐ3 Duyệt Hợp đồng (bước 13 → 18) 🟢 LIVE — module Contract
|
||||
// GĐ4 Ký cứng & phát hành (bước 19 → 21) ⏳ sắp triển khai
|
||||
// GĐ2 Kế hoạch ký kết HĐ (bước 7 → 12) ⏳ sắp triển khai
|
||||
// GĐ3 Duyệt hợp đồng (bước 13 → 18) 🟢 LIVE — module Contract
|
||||
// GĐ4 Hợp đồng cứng (bước 19 → 21) ⏳ sắp triển khai
|
||||
// Dưới hero GIỮ nguyên KPI cá nhân + 2 danh sách gần đây (chỉ redesign cho hài hoà).
|
||||
// fe-user ONLY — KHÔNG mirror sang fe-admin (trang này là UX riêng của user).
|
||||
//
|
||||
@ -473,7 +473,7 @@ export function UserDashboardPage() {
|
||||
<StageArrow />
|
||||
|
||||
{/* GĐ2 — sắp triển khai */}
|
||||
<StageCard index={2} title="Đề xuất ký kết hợp đồng" stepRange="Bước 7 → 12" icon={FileSignature}>
|
||||
<StageCard index={2} title="Kế hoạch ký kết HĐ" stepRange="Bước 7 → 12" icon={FileSignature}>
|
||||
<StageSoon
|
||||
description="Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — chốt giá trị ký kết."
|
||||
hint={
|
||||
@ -489,14 +489,14 @@ export function UserDashboardPage() {
|
||||
{/* GĐ3 — LIVE */}
|
||||
<StageCard
|
||||
index={3}
|
||||
title="Duyệt Hợp đồng"
|
||||
title="Duyệt hợp đồng"
|
||||
stepRange="Bước 13 → 18"
|
||||
icon={FileText}
|
||||
tone="teal"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<StageStatRow
|
||||
group="Duyệt Hợp đồng"
|
||||
group="Duyệt hợp đồng"
|
||||
label="Tổng hợp đồng"
|
||||
value={recent.data?.total}
|
||||
tone="teal"
|
||||
@ -505,7 +505,7 @@ export function UserDashboardPage() {
|
||||
onClick={() => navigate('/my-contracts')}
|
||||
/>
|
||||
<StageStatRow
|
||||
group="Duyệt Hợp đồng"
|
||||
group="Duyệt hợp đồng"
|
||||
label="Chờ tôi duyệt"
|
||||
value={s?.pendingMyApproval}
|
||||
tone="teal"
|
||||
@ -515,7 +515,7 @@ export function UserDashboardPage() {
|
||||
onClick={() => navigate('/inbox')}
|
||||
/>
|
||||
<StageStatRow
|
||||
group="Duyệt Hợp đồng"
|
||||
group="Duyệt hợp đồng"
|
||||
label="Đã quá hạn"
|
||||
value={s?.overdue}
|
||||
tone="teal"
|
||||
@ -531,7 +531,7 @@ export function UserDashboardPage() {
|
||||
<StageArrow />
|
||||
|
||||
{/* GĐ4 — sắp triển khai */}
|
||||
<StageCard index={4} title="Ký cứng & phát hành" stepRange="Bước 19 → 21" icon={Stamp}>
|
||||
<StageCard index={4} title="Hợp đồng cứng" stepRange="Bước 19 → 21" icon={Stamp}>
|
||||
<StageSoon description="Tải lên bản hợp đồng đã ký cứng." />
|
||||
</StageCard>
|
||||
</div>
|
||||
|
||||
@ -34,6 +34,13 @@ public static class MenuKeys
|
||||
public static readonly string[] ContractTypeCodes =
|
||||
["ThauPhu", "GiaoKhoan", "NhaCungCap", "DichVu", "MuaBan", "NguyenTacNcc", "NguyenTacDv"];
|
||||
|
||||
// [S159 2026-07-29] 2 root PLACEHOLDER giai đoạn 2/4 (owner: menu hiển thị đúng
|
||||
// 4 giai đoạn như tab strip). CỐ Ý NGOÀI `All` — chưa có module ⇒ không sinh
|
||||
// policy per-action (giữ canonical Menu keys/Policies không drift); FE trỏ về
|
||||
// /dashboard. Wave-1 GĐ2 (spec KHKK) phải TÁI DÙNG/thay root `KeHoachKyKet` này.
|
||||
public const string KeHoachKyKet = "KeHoachKyKet";
|
||||
public const string HopDongCung = "HopDongCung";
|
||||
|
||||
public static string ContractTypeGroup(string typeCode) => $"Ct_{typeCode}";
|
||||
public static string ContractTypeList(string typeCode) => $"Ct_{typeCode}_List";
|
||||
public static string ContractTypeCreate(string typeCode) => $"Ct_{typeCode}_Create";
|
||||
|
||||
@ -1736,15 +1736,19 @@ public static class DbInitializer
|
||||
|
||||
private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)
|
||||
{
|
||||
// [S159 2026-07-29] UAT anh Kiệt: "cho đánh số thứ tự phía trước giúp anh — 01. HĐ thầu phụ"
|
||||
// ⇒ prefix 01.-07. theo THỨ TỰ hiển thị (= thứ tự ContractTypeCodes). Prod đã UPDATE
|
||||
// trực tiếp cùng ngày (prepend-only, 0 ký tự VN qua wire); labelBackfill dưới giữ
|
||||
// các env khác hội tụ (gotcha #11 — sửa tree.Add một mình là vô tác dụng trên DB đang chạy).
|
||||
var typeLabels = new Dictionary<string, string>
|
||||
{
|
||||
["ThauPhu"] = "HĐ Thầu phụ",
|
||||
["GiaoKhoan"] = "HĐ Giao khoán",
|
||||
["NhaCungCap"] = "HĐ Nhà cung cấp",
|
||||
["DichVu"] = "HĐ Dịch vụ",
|
||||
["MuaBan"] = "HĐ Mua bán",
|
||||
["NguyenTacNcc"] = "HĐ Nguyên tắc NCC",
|
||||
["NguyenTacDv"] = "HĐ Nguyên tắc Dịch vụ",
|
||||
["ThauPhu"] = "01. HĐ Thầu phụ",
|
||||
["GiaoKhoan"] = "02. HĐ Giao khoán",
|
||||
["NhaCungCap"] = "03. HĐ Nhà cung cấp",
|
||||
["DichVu"] = "04. HĐ Dịch vụ",
|
||||
["MuaBan"] = "05. HĐ Mua bán",
|
||||
["NguyenTacNcc"] = "06. HĐ Nguyên tắc NCC",
|
||||
["NguyenTacDv"] = "07. HĐ Nguyên tắc Dịch vụ",
|
||||
};
|
||||
|
||||
var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)>
|
||||
@ -1764,7 +1768,11 @@ public static class DbInitializer
|
||||
(MenuKeys.CatalogServices, "Dịch vụ", MenuKeys.Catalogs, 3, "Wrench"),
|
||||
(MenuKeys.CatalogWorkItems,"Hạng mục công việc", MenuKeys.Catalogs, 4, "ListChecks"),
|
||||
// [S57] Order 30→31: nhường slot 30 cho nhóm "Cá nhân" (đứng ngay sau Văn phòng số = 29, mirror Puro).
|
||||
(MenuKeys.Contracts, "Hợp đồng", null, 31, "FileText"),
|
||||
(MenuKeys.Contracts, "Duyệt hợp đồng", null, 31, "FileText"), // [S159] rename theo 4-GĐ (owner tab strip)
|
||||
// [S159] 2 root PLACEHOLDER GĐ2/GĐ4 — menu = 4 giai đoạn (owner). Ngoài MenuKeys.All,
|
||||
// FE staticMap trỏ /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây).
|
||||
(MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, "FileSignature"),
|
||||
(MenuKeys.HopDongCung, "Hợp đồng cứng", null, 32, "Stamp"),
|
||||
(MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"),
|
||||
(MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"),
|
||||
(MenuKeys.System, "Hệ thống", null, 90, "Settings"),
|
||||
@ -1774,7 +1782,7 @@ public static class DbInitializer
|
||||
(MenuKeys.MenuVisibility, "Menu eOffice", MenuKeys.System, 94, "Eye"),
|
||||
(MenuKeys.Workflows, "Quy trình HĐ", MenuKeys.System, 95, "GitBranch"),
|
||||
// Module Duyệt NCC (tiền-HĐ)
|
||||
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
|
||||
(MenuKeys.PurchaseEvaluations, "Duyệt NCC", null, 25, "ClipboardCheck"), // [S159] rename theo 4-GĐ
|
||||
(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"),
|
||||
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08): schema riêng
|
||||
// UAT trước khi migrate hoàn toàn. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
||||
@ -1932,6 +1940,19 @@ public static class DbInitializer
|
||||
// chạy vì upsert `:1893-1905` chỉ update `Order` cho key đã tồn tại.
|
||||
[MenuKeys.PurchaseEvaluationPending("DuyetNcc")] = "Đang duyệt",
|
||||
[MenuKeys.PurchaseEvaluationPending("DuyetNccPhuongAn")] = "Đang duyệt",
|
||||
// [S159] 🔴 UAT anh Kiệt: đánh số 01.-07. nhóm HĐ (verbatim "01 . HĐ thầu phụ").
|
||||
// Prod đã prepend trực tiếp cùng ngày ⇒ trên prod các entry này NO-OP (equal);
|
||||
// dev/fresh-DB hội tụ qua đây. Nguồn số = thứ tự ContractTypeCodes (= [Order]).
|
||||
// [S159] 2 root rename theo 4-GĐ (prod đã SQL cùng ngày ⇒ NO-OP trên prod).
|
||||
[MenuKeys.PurchaseEvaluations] = "Duyệt NCC",
|
||||
[MenuKeys.Contracts] = "Duyệt hợp đồng",
|
||||
[MenuKeys.ContractTypeGroup("ThauPhu")] = "01. HĐ Thầu phụ",
|
||||
[MenuKeys.ContractTypeGroup("GiaoKhoan")] = "02. HĐ Giao khoán",
|
||||
[MenuKeys.ContractTypeGroup("NhaCungCap")] = "03. HĐ Nhà cung cấp",
|
||||
[MenuKeys.ContractTypeGroup("DichVu")] = "04. HĐ Dịch vụ",
|
||||
[MenuKeys.ContractTypeGroup("MuaBan")] = "05. HĐ Mua bán",
|
||||
[MenuKeys.ContractTypeGroup("NguyenTacNcc")] = "06. HĐ Nguyên tắc NCC",
|
||||
[MenuKeys.ContractTypeGroup("NguyenTacDv")] = "07. HĐ Nguyên tắc Dịch vụ",
|
||||
};
|
||||
var updatedLabels = 0;
|
||||
foreach (var (key, expectedLabel) in labelBackfill)
|
||||
@ -2113,6 +2134,9 @@ public static class DbInitializer
|
||||
|
||||
var reviewKeys = MenuKeys.All.Where(InReviewScope)
|
||||
.Concat(peKeys)
|
||||
// [S159] 2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false
|
||||
// ⇒ nhánh read-only). Prod đã INSERT trực tiếp; đây giữ fresh-DB hội tụ.
|
||||
.Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung })
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
var roles = await roleManager.Roles.ToListAsync();
|
||||
|
||||
Reference in New Issue
Block a user