diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index a5e6043..4b56187 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -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 (
+ {/* [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) */} +
diff --git a/fe-admin/src/components/pe/PePipelineStrip.tsx b/fe-admin/src/components/pe/PePipelineStrip.tsx new file mode 100644 index 0000000..b115479 --- /dev/null +++ b/fe-admin/src/components/pe/PePipelineStrip.tsx @@ -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 ( +
+ {STAGES.map((s, i) => { + const active = s.n === current + return ( +
+
+ {active && } + {s.label} + {s.soon && !active && ( + + Sắp + + )} +
+ {i < STAGES.length - 1 && ( + + → + + )} +
+ ) + })} +
+ ) +} diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 43669e6..c0bfd11 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -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. diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index a5e6043..4b56187 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -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 (
+ {/* [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) */} +
diff --git a/fe-user/src/components/pe/PePipelineStrip.tsx b/fe-user/src/components/pe/PePipelineStrip.tsx new file mode 100644 index 0000000..b115479 --- /dev/null +++ b/fe-user/src/components/pe/PePipelineStrip.tsx @@ -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 ( +
+ {STAGES.map((s, i) => { + const active = s.n === current + return ( +
+
+ {active && } + {s.label} + {s.soon && !active && ( + + Sắp + + )} +
+ {i < STAGES.length - 1 && ( + + → + + )} +
+ ) + })} +
+ ) +} diff --git a/fe-user/src/pages/UserDashboardPage.tsx b/fe-user/src/pages/UserDashboardPage.tsx index 50f38e5..4394178 100644 --- a/fe-user/src/pages/UserDashboardPage.tsx +++ b/fe-user/src/pages/UserDashboardPage.tsx @@ -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() { {/* GĐ2 — sắp triển khai */} - +
navigate('/my-contracts')} /> navigate('/inbox')} /> {/* GĐ4 — sắp triển khai */} - +
diff --git a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs index 612d7fa..a1af172 100644 --- a/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs +++ b/src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs @@ -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"; diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index ff0fe76..4df9355 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -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 { - ["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();