From ea962b1e59432bd6b003836cf0c93bed73732abf Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Jul 2026 14:40:10 +0700 Subject: [PATCH] [CLAUDE] Contract+FE-User: dot-5 menu 6-leaf y-chang-Duyet-NCC + GET /contracts/deleted + bo hero + "Tong quan quy trinh" - BE: ListDeletedContractsQuery (IgnoreQueryFilters + Where(IsDeleted) + IDOR mirror; LUAT thay so-dem theo reviewer FLAG-10) + GET /contracts/deleted [Contracts.Read] - Seeder: Ct x7 +3 leaf (WfView/Approved/Deleted) + labelBackfill "Dang duyet" x7 + Khkk 6 leaf + Dashboard -> "Tong quan quy trinh" + reviewKeys +23 key - FE user: matrix +type=3 (HD chung 1 bo V2) + route /contracts/workflow-matrix + Ct regex +3 action + MyContractsPage (phase/deleted + banner + an nut Tao + FLAG-2 guard selectContract khi deleted) + MenuLeaf depth-0 UPPERCASE + UserDashboardPage BO HERO (owner: "bo cai tren di, cai duoi OK") + title "Tong quan quy trinh" - fe-admin: staticMap +2 Khkk (FLAG-1); leaf Ct_ moi van an theo isAdminHidden (da kiem CLEAN) - reviewer PASS_WITH_FLAGS 10 FLAG (0H/4M/6L): fix 4M, park 6L co ghi (FLAG-4 dinh nghia "Da duyet" HD -> cho owner) - build fe x2 + BE PASS; test 562 PASS Co-Authored-By: Claude Opus 5 --- fe-admin/src/components/Layout.tsx | 2 + fe-user/src/App.tsx | 2 + fe-user/src/components/Layout.tsx | 20 +- fe-user/src/pages/UserDashboardPage.tsx | 357 +----------------- .../src/pages/contracts/MyContractsPage.tsx | 43 ++- .../src/pages/pe/WorkflowMatrixViewPage.tsx | 5 +- .../Controllers/ContractsController.cs | 12 + .../Contracts/ContractFeatures.cs | 58 +++ .../PeSoftDeleteFeatures.cs | 9 +- .../Persistence/DbInitializer.cs | 36 +- 10 files changed, 173 insertions(+), 371 deletions(-) diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 9cd9465..dc4d3c7 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -31,10 +31,12 @@ function resolvePath(key: string): string | null { const staticMap: Record = { // [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ // active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT). + Khkk_WfView: '/coming-soon?stage=2&m=wfview', Khkk_List: '/coming-soon?stage=2&m=list', Khkk_Create: '/coming-soon?stage=2&m=create', Khkk_Pending: '/coming-soon?stage=2&m=pending', Khkk_Approved: '/coming-soon?stage=2&m=approved', + Khkk_Deleted: '/coming-soon?stage=2&m=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50) Hdc_ThauPhu: '/coming-soon?stage=4&t=01', Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02', Hdc_NhaCungCap: '/coming-soon?stage=4&t=03', diff --git a/fe-user/src/App.tsx b/fe-user/src/App.tsx index ec8c6af..2e6506c 100644 --- a/fe-user/src/App.tsx +++ b/fe-user/src/App.tsx @@ -61,6 +61,8 @@ function App() { } /> } /> } /> + {/* [S159-đợt5] alias cho leaf "Luồng duyệt" của 7 nhóm HĐ (?type=3) */} + } /> } /> } /> } /> diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 7346172..8a8f84a 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -75,6 +75,9 @@ function resolvePath(key: string): string | null { // LESSON gotcha #50: key DB mới PHẢI có route ở đây, không thì sidebar drop SILENT. KeHoachKyKet: '/dashboard', HopDongCung: '/dashboard', + // [S159-đợt5] 2 leaf mới của skeleton GĐ2 (đủ 6 y chang Duyệt NCC) + Khkk_WfView: '/coming-soon?stage=2&m=wfview', + Khkk_Deleted: '/coming-soon?stage=2&m=deleted', // [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. @@ -124,7 +127,9 @@ function resolvePath(key: string): string | null { } if (staticMap[key]) return staticMap[key] - const match = key.match(/^Ct_([^_]+)_(List|Create|Pending)$/) + // [S159-đợt5 owner "y chang Duyệt NCC"] +WfView/Approved/Deleted. WfView cả 7 loại + // đều ?type=3 (HĐ chung 1 bộ workflow V2) — thêm &ct=N để active-state riêng từng leaf. + const match = key.match(/^Ct_([^_]+)_(List|Create|Pending|WfView|Approved|Deleted)$/) if (match) { const [, code, action] = match const typeInt = TYPE_CODE_TO_INT[code] @@ -132,6 +137,9 @@ function resolvePath(key: string): string | null { if (action === 'List') return `/my-contracts?type=${typeInt}` if (action === 'Create') return `/contracts/new?type=${typeInt}` if (action === 'Pending') return `/inbox?type=${typeInt}` + if (action === 'WfView') return `/contracts/workflow-matrix?type=3&ct=${typeInt}` + if (action === 'Approved') return `/my-contracts?type=${typeInt}&phase=9` + if (action === 'Deleted') return `/my-contracts?type=${typeInt}&deleted=1` } // Pe__ cho module Duyệt NCC (user side) @@ -343,7 +351,15 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) { // Density-first: active leaf gets a brand left-rail + tint (crisp // selected affordance, NAMGROUP). Inactive stays quiet slate. 'relative block rounded-md leading-snug transition-colors', - isDeep ? 'px-3 py-1 text-[11px]' : 'px-3 py-1.5 text-[12px] font-medium', + // [S159-đợt5 owner] leaf cấp-0 render qua MenuLeaf ("Tổng quan quy trình") viết + // HOA đồng bộ group header cấp-0 (khuôn MenuGroup :260). 🔴 KHAI ĐÚNG PHẠM VI + // (reviewer FLAG-3): "Hộp thư" KHÔNG đi qua đây — nó là USER_FIXED_TOP render + // bằng StaticLeaf (lớp CSS riêng, vẫn chữ thường). Đồng bộ nốt = quyết owner. + depth === 0 + ? 'px-3 py-2 text-[11px] font-semibold uppercase tracking-wide' + : isDeep + ? 'px-3 py-1 text-[11px]' + : 'px-3 py-1.5 text-[12px] font-medium', isActive ? 'bg-brand-50 font-semibold text-brand-700 before:absolute before:inset-y-1 before:left-0 before:w-0.5 before:rounded-full before:bg-brand-600' : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', diff --git a/fe-user/src/pages/UserDashboardPage.tsx b/fe-user/src/pages/UserDashboardPage.tsx index ff64a73..7b220bd 100644 --- a/fe-user/src/pages/UserDashboardPage.tsx +++ b/fe-user/src/pages/UserDashboardPage.tsx @@ -52,7 +52,6 @@ import { useNavigate } from 'react-router-dom' import { AlertTriangle, ArrowRight, - ChevronDown, ChevronLeft, ChevronRight, ClipboardCheck, @@ -65,7 +64,6 @@ import { Search, SearchX, Stamp, - Workflow, X, } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' @@ -82,7 +80,6 @@ import { ContractTypeLabel } from '@/types/forms' import { PeFinalizeChip } from '@/components/pe/PeFinalizeChip' import { PeUrgentChips } from '@/components/pe/PeUrgentChips' import { - PurchaseEvaluationPhase, getPeDisplayStatus, PeDisplayStatus, PeDisplayStatusColor, @@ -90,14 +87,6 @@ import { type PeListItem, } from '@/types/purchaseEvaluation' -type MyDashboard = { - draftsInProgress: number - pendingMyApproval: number - dueSoon: number - overdue: number - draftsTotalValue: number -} - const PAGE_SIZE = 10 const fmtMoney = (v: number) => { @@ -153,177 +142,6 @@ const stageOf = (n: number) => STAGES.find(s => s.n === n) ?? STAGES[0] HERO — dải pipeline 4 giai đoạn (GIỮ NGUYÊN, owner đã duyệt) ───────────────────────────────────────────────────────────────────────────── */ -// Giai đoạn LIVE mang accent (brand / teal); giai đoạn "sắp triển khai" = khung -// nét đứt xám trung tính — MỜ nhưng KHÔNG đỏ/vàng, để không đọc nhầm thành lỗi. -const STAGE_TONE: Record = { - brand: { - rail: 'var(--color-brand-500)', - chipBg: 'var(--color-brand-50)', - chipFg: 'var(--color-brand-600)', - badge: 'bg-brand-50 text-brand-700 ring-brand-200', - head: 'text-brand-800!', - row: 'hover:bg-brand-50 focus-visible:ring-brand-500', - num: 'text-brand-700!', - link: 'text-brand-700 hover:text-brand-800 focus-visible:ring-brand-500', - }, - teal: { - rail: 'var(--color-teal-500)', - chipBg: 'var(--color-teal-50)', - chipFg: 'var(--color-teal-700)', - badge: 'bg-teal-50 text-teal-700 ring-teal-100', - head: 'text-teal-700!', - row: 'hover:bg-teal-50 focus-visible:ring-teal-500', - num: 'text-teal-700!', - link: 'text-teal-700 hover:text-teal-600 focus-visible:ring-teal-500', - }, -} - -// Khung 1 giai đoạn — nhãn/icon/tone đọc TỪ `STAGES`, không truyền chuỗi rời. -function StageCard({ stage, children }: { stage: StageMeta; children: React.ReactNode }) { - const { n, title, stepRange, icon: Icon, tone } = stage - const t = tone ? STAGE_TONE[tone] : null - return ( -
-
- - Giai đoạn {n} - - - - -
- -

{title}

-
{stepRange}
- -
{children}
-
- ) -} - -// Mũi tên nối giữa 2 giai đoạn — ngang trên desktop, xuống dòng trên mobile. -function StageArrow() { - return ( -
- - -
- ) -} - -// 1 dòng số liệu trong giai đoạn LIVE — bấm được, mở đúng màn hình đã lọc sẵn. -// Là - ) -} - -// Link mở module của giai đoạn LIVE. -function StageLink({ label, tone, onClick }: { label: string; tone: StageTone; onClick: () => void }) { - const t = STAGE_TONE[tone] - return ( - - ) -} - -// Thân của giai đoạn chưa triển khai — badge trung tính + 1 dòng mô tả. -function StageSoon({ description, hint }: { description?: string; hint?: string }) { - return ( -
- - - Sắp triển khai - -

{description}

- {hint &&

{hint}

} -
- ) -} - /* ───────────────────────────────────────────────────────────────────────────── TẦNG LIÊN KẾT 2 — "Hành trình": mini-strip 4 chấm trên TỪNG DÒNG. Chấm của chính giai đoạn dòng đó mang trạng thái phiếu; 3 chấm còn lại mờ + @@ -1084,11 +902,6 @@ export function UserDashboardPage() { /* ── Số liệu hero (5 query nền, y như trước — chỉ 2 query list rút pageSize 5 → 1 vì 2 danh sách "gần đây" đã bị gỡ, hero chỉ cần `.total`). ───────────── */ - const stats = useQuery({ - queryKey: ['my-dashboard'], - queryFn: async () => (await api.get('/reports/my-dashboard')).data, - }) - const peTotalQuery = useQuery({ queryKey: ['pipeline-pe-total'], queryFn: async () => @@ -1102,46 +915,6 @@ export function UserDashboardPage() { (await api.get>('/contracts', { params: { page: 1, pageSize: 1 } })).data.total, }) - // [S159] số phiếu Duyệt NCC ĐÃ DUYỆT (phase=7). pageSize=1 vì chỉ cần `total`. - const peApprovedQuery = useQuery({ - queryKey: ['pipeline-pe-approved'], - queryFn: async () => - ( - await api.get>('/purchase-evaluations', { - params: { page: 1, pageSize: 1, phase: PurchaseEvaluationPhase.DaDuyet }, - }) - ).data.total, - }) - - // [S159] phiếu Duyệt NCC đang chờ CHÍNH TÔI duyệt. Endpoint /inbox trả mảng phẳng. - // 🔴 DÙNG CHUNG queryKey + queryFn với InboxPage:77-78 (reviewer FLAG-5) — cache chia sẻ, - // 2 màn luôn khớp số. CẤM đổi queryFn trả `.length` dưới key này: InboxPage cache MẢNG, - // trùng key khác shape = vỡ cache lẫn nhau ⇒ đếm bằng `select` (transform per-observer, - // không chạm cache). - const peInboxQuery = useQuery({ - queryKey: ['pe-inbox'], - queryFn: async () => (await api.get('/purchase-evaluations/inbox')).data, - select: d => d.length, - }) - - const s = stats.data - const peApproved = peApprovedQuery.data - - const hasError = - stats.isError || - contractTotalQuery.isError || - peTotalQuery.isError || - peApprovedQuery.isError || - peInboxQuery.isError - - const retryFailed = () => { - if (stats.isError) void stats.refetch() - if (contractTotalQuery.isError) void contractTotalQuery.refetch() - if (peTotalQuery.isError) void peTotalQuery.refetch() - if (peApprovedQuery.isError) void peApprovedQuery.refetch() - if (peInboxQuery.isError) void peInboxQuery.refetch() - } - /* ── Section "Danh sách phiếu toàn trình" ──────────────────────────────── */ const [stage, setStage] = useState(1) @@ -1219,135 +992,13 @@ export function UserDashboardPage() { return (
- {/* ── HERO: pipeline 4 giai đoạn ───────────────────────────────────── */} -
-
- - - -
- {/* Không dùng .label-eyebrow (brand-600 = 4.42:1, hụt AA 4.5) — brand-700 - giữ đúng nhận diện mà đạt 5.6:1. */} -
- Quy trình mua sắm → ký kết hợp đồng -
-

Toàn trình 4 giai đoạn · 21 bước

-
-
- - {hasError && ( -
- - - Không tải được một vài số liệu. Các phần còn lại vẫn hiển thị bình thường. - - -
- )} - -
- {/* GĐ1 — LIVE */} - -
- navigate('/purchase-evaluations')} - /> - navigate(`/purchase-evaluations?phase=${PurchaseEvaluationPhase.DaDuyet}`)} - /> - navigate('/purchase-evaluations?pendingMe=1')} - /> -
- navigate('/purchase-evaluations')} /> -
- - - - {/* GĐ2 — sắp triển khai */} - - 0 - ? `${peApproved} phiếu Duyệt NCC đã duyệt` - : undefined - } - /> - - - - - {/* GĐ3 — LIVE */} - -
- navigate('/my-contracts')} - /> - navigate('/inbox')} - /> - navigate('/inbox')} - /> -
- navigate('/my-contracts')} /> -
- - - - {/* GĐ4 — sắp triển khai */} - - - -
-
+ {/* [S159-dot5 owner: "O dep day, vay bo cai tren di nhe, cai duoi OK day"] + HERO pipeline 4 card DA GO — tang tong-quan-lien-ket nay la thanh tab + 4 GD cua Danh sach chi tiet ben duoi (count + cot Hanh trinh per-row). */} {/* ── DANH SÁCH PHIẾU TOÀN TRÌNH ───────────────────────────────────── */}
diff --git a/fe-user/src/pages/contracts/MyContractsPage.tsx b/fe-user/src/pages/contracts/MyContractsPage.tsx index b3f7978..a5ca138 100644 --- a/fe-user/src/pages/contracts/MyContractsPage.tsx +++ b/fe-user/src/pages/contracts/MyContractsPage.tsx @@ -29,11 +29,24 @@ export function MyContractsPage() { const typeFilter = searchParams.get('type') ? Number(searchParams.get('type')) : null const selectedId = searchParams.get('id') const search = searchParams.get('q') ?? '' + // [S159-đợt5] leaf 'Đã duyệt' (?phase=9 — BE /contracts có sẵn param phase) và + // 'Đã xóa' (?deleted=1 — endpoint /contracts/deleted mới, chỉ-xem). + const phaseFilter = searchParams.get('phase') ? Number(searchParams.get('phase')) : null + const showDeleted = searchParams.get('deleted') === '1' const list = useQuery({ - queryKey: ['my-contracts', typeFilter], + queryKey: ['my-contracts', typeFilter, phaseFilter, showDeleted], queryFn: async () => - (await api.get>('/contracts', { params: { page: 1, pageSize: 100 } })).data, + ( + await api.get>(showDeleted ? '/contracts/deleted' : '/contracts', { + params: { + page: 1, + pageSize: 100, + ...(showDeleted && typeFilter != null ? { type: typeFilter } : {}), + ...(!showDeleted && phaseFilter != null ? { phase: phaseFilter } : {}), + }, + }) + ).data, }) const detail = useQuery({ @@ -57,6 +70,10 @@ export function MyContractsPage() { }, [list.data, typeFilter, search]) function selectContract(id: string) { + // [S159-đợt5 FLAG-2 reviewer] Màn "Đã xóa" = CHỈ XEM DANH SÁCH (khuôn PE S155 + // `disabled={deletedView}`): detail endpoint KHÔNG IgnoreQueryFilters ⇒ HĐ đã xóa + // 404 ⇒ desktop = 2 panel trắng, mobile = navigate sang trang 404. Chặn tại gốc. + if (showDeleted) return // Desktop ≥ lg: cập nhật URL để render Panel 2/3 cạnh List. // Mobile: Panel 2/3 hidden → navigate fullpage /contracts/:id (UX khả dụng). if (typeof window !== 'undefined' && window.matchMedia('(min-width: 1024px)').matches) { @@ -96,10 +113,22 @@ export function MyContractsPage() { {rows.length}
- + {showDeleted && ( + + Đã xóa — chỉ xem + + )} + {!showDeleted && phaseFilter === 9 && ( + + Đã phát hành + + )} + {!showDeleted && ( + + )} {/* 3-panel grid — flex-1 để fill phần còn lại của viewport */} @@ -130,7 +159,7 @@ export function MyContractsPage() { )} diff --git a/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx b/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx index 1a52025..2801a6d 100644 --- a/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx +++ b/fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx @@ -25,6 +25,7 @@ import type { AwAdminOverviewDto, AwDefinitionDto, AwLevelDto } from '@/types/ap const TYPE_LABEL: Record = { 1: 'Duyệt NCC', 2: 'Duyệt NCC + Giải pháp', + 3: 'Hợp đồng (chung mọi loại)', // [S159-đợt5] } // Color palette per Step (Phòng) — cycle 5 màu để phân biệt Bước. Tailwind JIT @@ -50,7 +51,9 @@ const LEVEL_PALETTE = [ export function WorkflowMatrixViewPage() { const [searchParams] = useSearchParams() const rawType = Number(searchParams.get('type')) - const typeInt = rawType === 1 || rawType === 2 ? rawType : 1 + // [S159-đợt5] +3 = Contract (ApplicableType.Contract — HĐ dùng CHUNG 1 bộ workflow V2 + // cho mọi ContractType ⇒ 7 leaf 'Luồng duyệt' của 7 nhóm HĐ đều trỏ về matrix này). + const typeInt = rawType === 1 || rawType === 2 || rawType === 3 ? rawType : 1 const { data, isLoading, isError } = useQuery({ queryKey: ['workflow-matrix', typeInt], diff --git a/src/Backend/SolutionErp.Api/Controllers/ContractsController.cs b/src/Backend/SolutionErp.Api/Controllers/ContractsController.cs index 2fd3fe8..249e41d 100644 --- a/src/Backend/SolutionErp.Api/Controllers/ContractsController.cs +++ b/src/Backend/SolutionErp.Api/Controllers/ContractsController.cs @@ -23,6 +23,18 @@ public class ContractsController(IMediator mediator) : ControllerBase CancellationToken ct = default) => Ok(await mediator.Send(new ListContractsQuery(phase, supplierId, projectId) { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct)); + // [S159-đợt5] Màn "Đã xóa" HĐ (chỉ-xem) — mirror PE /deleted (S155). Read-only, + // policy root Contracts.Read (chặt hơn [Authorize] trần của class — không phá gì). + [HttpGet("deleted")] + [Authorize(Policy = "Contracts.Read")] + public async Task>> ListDeleted( + [FromQuery] ContractType? type = null, + [FromQuery] int page = 1, [FromQuery] int pageSize = 20, + [FromQuery] string? search = null, [FromQuery] bool sortDesc = true, + CancellationToken ct = default) + => Ok(await mediator.Send(new ListDeletedContractsQuery(type) + { Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct)); + [HttpGet("inbox")] public async Task>> Inbox(CancellationToken ct) => Ok(await mediator.Send(new GetMyInboxQuery(), ct)); diff --git a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs index 195defc..024b6c7 100644 --- a/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs +++ b/src/Backend/SolutionErp.Application/Contracts/ContractFeatures.cs @@ -351,6 +351,64 @@ public class ListContractsQueryHandler(IApplicationDbContext db, ICurrentUser cu } } +// ========== LIST DELETED contracts — màn "Đã xóa" (chỉ-xem) ========== +// [S159-đợt5 — owner "y chang Duyệt NCC"] Cookie-cutter `ListDeletedPurchaseEvaluationsQuery` +// (PeSoftDeleteFeatures.cs). 🔴 LUẬT (không đếm số — reviewer FLAG-10: "chỗ THỨ HAI" là +// con số tự-lan, ApprovalWorkflowV2AdminFeatures cũng dùng Ignore ở lớp KHÁC): mọi nơi +// dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm `.Where(x => x.IsDeleted)` +// vì Ignore GỠ filter chứ KHÔNG ĐẢO (bài 8a S155). Tự kiểm: +// grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs +// IDOR scope MIRROR ListContractsQueryHandler (Drafter ∪ eligible-phase; phase ở đây = +// phase tại thời điểm xóa). + +public record ListDeletedContractsQuery(ContractType? Type = null) + : PagedRequest, IRequest>; + +public class ListDeletedContractsQueryHandler(IApplicationDbContext db, ICurrentUser currentUser) + : IRequestHandler> +{ + public async Task> Handle(ListDeletedContractsQuery request, CancellationToken ct) + { + var q = from c in db.Contracts.AsNoTracking().IgnoreQueryFilters() + .Where(x => x.IsDeleted) + join s in db.Suppliers.AsNoTracking() on c.SupplierId equals s.Id + join p in db.Projects.AsNoTracking() on c.ProjectId equals p.Id + select new { c, s, p }; + + if (!currentUser.Roles.Contains(AppRoles.Admin)) + { + var userId = currentUser.UserId; + var eligiblePhases = ListContractsQueryHandler.GetEligiblePhases(currentUser.Roles); + q = q.Where(x => x.c.DrafterUserId == userId || eligiblePhases.Contains(x.c.Phase)); + } + + if (request.Type is not null) q = q.Where(x => x.c.Type == request.Type); + + if (!string.IsNullOrWhiteSpace(request.Search)) + { + var kw = request.Search.Trim(); + q = q.Where(x => + (x.c.MaHopDong != null && x.c.MaHopDong.Contains(kw)) || + (x.c.TenHopDong != null && x.c.TenHopDong.Contains(kw)) || + x.s.Name.Contains(kw) || x.p.Name.Contains(kw)); + } + + q = request.SortDesc ? q.OrderByDescending(x => x.c.DeletedAt) : q.OrderBy(x => x.c.DeletedAt); + + var total = await q.CountAsync(ct); + var items = await q + .Skip((request.Page - 1) * request.PageSize).Take(request.PageSize) + .Select(x => new ContractListItemDto( + x.c.Id, x.c.MaHopDong, x.c.TenHopDong, x.c.Type, x.c.Phase, + x.c.SupplierId, x.s.Name, + x.c.ProjectId, x.p.Name, + x.c.GiaTri, x.c.SlaDeadline, x.c.CreatedAt)) + .ToListAsync(ct); + + return new PagedResult(items, total, request.Page, request.PageSize); + } +} + // ========== INBOX — HĐ chờ role/tôi xử lý ========== public record GetMyInboxQuery : IRequest>; diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs index e8db393..84de23a 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PeSoftDeleteFeatures.cs @@ -181,7 +181,14 @@ public class ListDeletedPurchaseEvaluationsQueryHandler( { // 🔴 8a — `IgnoreQueryFilters()` **GỠ** filter chứ KHÔNG ĐẢO nó. Thiếu // `.Where(x => x.IsDeleted)` ⇒ màn "Đã xóa" liệt kê TOÀN BỘ phiếu sống lẫn chết. - // 🔴 D3 — đây là chỗ DUY NHẤT trong `src/Backend` dùng `IgnoreQueryFilters`: + // 🔴 D3 (S159-đợt5, sửa lần 2 theo reviewer FLAG-10 — BỎ CON SỐ ĐẾM, nêu LUẬT): + // MỌI nơi dùng `IgnoreQueryFilters` để LIỆT KÊ bản ghi đã xóa BẮT BUỘC kèm + // `.Where(x => x.IsDeleted)`. Lớp list-deleted hiện: đây + `ListDeletedContractsQueryHandler` + // (ContractFeatures.cs). `ApprovalWorkflowV2AdminFeatures.cs` cũng dùng Ignore ở nhiều + // vị trí nhưng thuộc LỚP KHÁC (đọc *LevelOpinions + dò phiếu-cha-mồ-côi, không list-deleted). + // Tự kiểm: grep -rn "IgnoreQueryFilters" src/Backend --include=*.cs + // (Bài: "chỗ DUY NHẤT/THỨ HAI" là con số tự-lan — S155 ghi DUY NHẤT đã sai từ khi + // ApprovalWorkflowV2AdminFeatures dùng; đừng thay số sai cũ bằng số sai mới.) // CẤM đưa vào helper dùng chung trả `IQueryable`, CẤM sửa // `PurchaseEvaluationConfiguration.cs:84`, CẤM thêm cờ `includeDeleted` vào query list // hiện có (cờ sẽ lan tới `PeBudgetAccumulator` = phá đúng thứ owner muốn). diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 43b1fa2..aae85ee 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -1753,7 +1753,7 @@ public static class DbInitializer var tree = new List<(string Key, string Label, string? Parent, int Order, string Icon)> { - (MenuKeys.Dashboard, "Tổng quan", null, 10, "LayoutDashboard"), + (MenuKeys.Dashboard, "Tổng quan quy trình", null, 10, "LayoutDashboard"), // [S159-đợt5] owner: "đổi thành gì dễ hình dung? Tổng quan quy trình" // [S58 2026-06-11] "Danh mục" 20→80: anh yêu cầu đưa xuống CUỐI sidebar // (sau Reports 50, trước vùng System 90 admin). Main upsert tự re-set // Order trên DB cũ. Con (21-25) giữ nguyên — order chỉ so trong siblings. @@ -1777,10 +1777,13 @@ public static class DbInitializer // Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render // MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app). ("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"), - ("Khkk_List", "Danh sách", "Khkk_G1", 1, "List"), - ("Khkk_Create", "Thao tác", "Khkk_G1", 2, "Plus"), - ("Khkk_Pending", "Đang duyệt", "Khkk_G1", 3, "CheckCircle2"), - ("Khkk_Approved", "Đã duyệt", "Khkk_G1", 4, "CheckCheck"), + // [S159-đợt5 owner] đủ 6 leaf "y chang Duyệt NCC" (WfView đầu + Deleted cuối). + ("Khkk_WfView", "Luồng duyệt", "Khkk_G1", 1, "Network"), + ("Khkk_List", "Danh sách", "Khkk_G1", 2, "List"), + ("Khkk_Create", "Thao tác", "Khkk_G1", 3, "Plus"), + ("Khkk_Pending", "Đang duyệt", "Khkk_G1", 4, "CheckCircle2"), + ("Khkk_Approved", "Đã duyệt", "Khkk_G1", 5, "CheckCheck"), + ("Khkk_Deleted", "Đã xóa", "Khkk_G1", 6, "Trash2"), (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"), @@ -1863,9 +1866,14 @@ public static class DbInitializer // đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4). tree.Add(("Hdc_" + code, label + " - Bảng cứng", MenuKeys.HopDongCung, hdcOrder++, "FileText")); // [S159-đợt4] owner vẽ: "01. HĐ Thầu phụ - Bảng cứng" tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText")); + // [S159-đợt5 owner "y chang Duyệt NCC"] mỗi loại HĐ đủ 6 leaf. Label "Đang duyệt" + // cho key CŨ đi qua labelBackfill (gotcha #11 — sửa chữ ở đây vô tác dụng DB đang chạy). + tree.Add(("Ct_" + code + "_WfView", "Luồng duyệt", MenuKeys.ContractTypeGroup(code), order++, "Network")); tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List")); tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus")); - tree.Add((MenuKeys.ContractTypePending(code),"Duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2")); + tree.Add((MenuKeys.ContractTypePending(code),"Đang duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCircle2")); + tree.Add(("Ct_" + code + "_Approved","Đã duyệt", MenuKeys.ContractTypeGroup(code), order++, "CheckCheck")); + tree.Add(("Ct_" + code + "_Deleted", "Đã xóa", MenuKeys.ContractTypeGroup(code), order++, "Trash2")); } // Per-type workflow admin leaves dưới `Workflows` — mỗi loại là 1 @@ -1958,6 +1966,16 @@ public static class DbInitializer // [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", + // [S159-đợt5] Sidebar "Tổng quan" → "Tổng quan quy trình" (owner đề tên, lead lấy). + [MenuKeys.Dashboard] = "Tổng quan quy trình", + // [S159-đợt5] 7 leaf Ct_X_Pending "Duyệt" → "Đang duyệt" (đồng khuôn PE — gotcha #11). + [MenuKeys.ContractTypePending("ThauPhu")] = "Đang duyệt", + [MenuKeys.ContractTypePending("GiaoKhoan")] = "Đang duyệt", + [MenuKeys.ContractTypePending("NhaCungCap")] = "Đang duyệt", + [MenuKeys.ContractTypePending("DichVu")] = "Đang duyệt", + [MenuKeys.ContractTypePending("MuaBan")] = "Đang duyệt", + [MenuKeys.ContractTypePending("NguyenTacNcc")] = "Đang duyệt", + [MenuKeys.ContractTypePending("NguyenTacDv")] = "Đang duyệt", // [S159-đợt4] 7 leaf Hdc_ thêm hậu tố " - Bảng cứng" (owner vẽ; prod SQL cùng ngày). ["Hdc_ThauPhu"] = "01. HĐ Thầu phụ - Bảng cứng", ["Hdc_GiaoKhoan"] = "02. HĐ Giao khoán - Bảng cứng", @@ -2142,9 +2160,12 @@ public static class DbInitializer foreach (var c in MenuKeys.ContractTypeCodes) { yield return MenuKeys.ContractTypeGroup(c); + yield return "Ct_" + c + "_WfView"; // [S159-đợt5] yield return MenuKeys.ContractTypeList(c); yield return MenuKeys.ContractTypeCreate(c); yield return MenuKeys.ContractTypePending(c); + yield return "Ct_" + c + "_Approved"; // [S159-đợt5] + yield return "Ct_" + c + "_Deleted"; // [S159-đợt5] } } static IEnumerable MasterMenuKeys() => new[] @@ -2155,7 +2176,8 @@ public static class DbInitializer }; static IEnumerable KhkkKeys() => new[] { - "Khkk_G1", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved", + "Khkk_G1", "Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending", + "Khkk_Approved", "Khkk_Deleted", }; static IEnumerable HdcKeys() => MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c);