From 6cbc6ad937ebccb443be207549801c89dee575f0 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Jul 2026 21:19:41 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20App+Api+FE-User+FE-Admin+Tests:=20W2?= =?UTF-8?q?=20KHKK=20=E2=80=94=20CRUD=20phieu=20nhap=20+=20can=20cu=20b.8-?= =?UTF-8?q?9=20+=20FE=203=20page=20x2=20app=20+=2012=20fix=20reviewer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BE mega-file ContractSigningPlanFeatures (Create 4-rao + auto-Lines snapshot per-winner + MaKeHoach @CREATE + UpdateDraft null-safe + Get/List/inbox-precompute-V2/deleted + Dossier + Attachments + Delete allow-list M6 + picker) + Controller 15 action policy per-action 15/15 (KeHoachKyKet.*) - FE pages/khkk 3 page + types x2 app SHA-pair + 4-place (6 leaf Khkk_* het coming-soon; WfView: user=matrix?type=10 [whitelist +10] / admin=Designer deep-link); root KeHoachKyKet -> /khkk/list - Reviewer adversarial: 12 finding (3 CRITICAL dut-2-bo FE-BE: route picker + peId body + dossier planId; 2 MAJOR security: detail+download 0 rao -> EnsureCanViewAsync mirror PE S89 nhap-rieng-tu; PUT-vs-POST de-ban-sao; peTenGoiThau; winnerSupplierNames; Content-Disposition RFC6266; LEFT-join Projects) -> 11 FIXED + 1 GIU (F-10 ke thua khuon PE, bit dong bo sau) - tests: +8 (574 total 0 fail) — authz reflection dong + Create/Delete/guard/khong-dot-ma-khi-409 + fixture-co-interceptor Co-Authored-By: Claude Fable 5 --- fe-admin/src/App.tsx | 9 + fe-admin/src/components/Layout.tsx | 17 +- fe-admin/src/pages/khkk/KhkkCreatePage.tsx | 254 ++++ fe-admin/src/pages/khkk/KhkkDetailPage.tsx | 680 +++++++++ fe-admin/src/pages/khkk/KhkkListPage.tsx | 265 ++++ fe-admin/src/types/khkk.ts | 229 +++ fe-user/src/App.tsx | 12 + fe-user/src/components/Layout.tsx | 20 +- fe-user/src/pages/khkk/KhkkCreatePage.tsx | 254 ++++ fe-user/src/pages/khkk/KhkkDetailPage.tsx | 680 +++++++++ fe-user/src/pages/khkk/KhkkListPage.tsx | 265 ++++ .../src/pages/pe/WorkflowMatrixViewPage.tsx | 3 +- fe-user/src/types/khkk.ts | 229 +++ .../ContractSigningPlansController.cs | 200 +++ .../ContractSigningPlanFeatures.cs | 1228 +++++++++++++++++ .../IContractSigningPlanCodeGenerator.cs | 4 +- .../ContractSigningPlan.cs | 7 +- .../ContractSigningPlanConfiguration.cs | 6 +- .../Api/AuthorizePolicyRegressionTests.cs | 118 ++ .../ContractSigningPlanCrudTests.cs | 563 ++++++++ 20 files changed, 5020 insertions(+), 23 deletions(-) create mode 100644 fe-admin/src/pages/khkk/KhkkCreatePage.tsx create mode 100644 fe-admin/src/pages/khkk/KhkkDetailPage.tsx create mode 100644 fe-admin/src/pages/khkk/KhkkListPage.tsx create mode 100644 fe-admin/src/types/khkk.ts create mode 100644 fe-user/src/pages/khkk/KhkkCreatePage.tsx create mode 100644 fe-user/src/pages/khkk/KhkkDetailPage.tsx create mode 100644 fe-user/src/pages/khkk/KhkkListPage.tsx create mode 100644 fe-user/src/types/khkk.ts create mode 100644 src/Backend/SolutionErp.Api/Controllers/ContractSigningPlansController.cs create mode 100644 src/Backend/SolutionErp.Application/ContractSigningPlans/ContractSigningPlanFeatures.cs create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/ContractSigningPlanCrudTests.cs diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 4f7fac9..5f2d629 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -31,6 +31,9 @@ import { HrmConfigsPage } from '@/pages/hrm/HrmConfigsPage' import { InternalDirectoryPage } from '@/pages/office/InternalDirectoryPage' import { MeetingCalendarPage } from '@/pages/office/MeetingCalendarPage' import { MeetingRoomsPage } from '@/pages/office/MeetingRoomsPage' +import { KhkkListPage } from '@/pages/khkk/KhkkListPage' +import { KhkkCreatePage } from '@/pages/khkk/KhkkCreatePage' +import { KhkkDetailPage } from '@/pages/khkk/KhkkDetailPage' import { ProposalCreatePage } from '@/pages/office/ProposalCreatePage' import { ProposalDetailPage } from '@/pages/office/ProposalDetailPage' import { ProposalsListPage } from '@/pages/office/ProposalsListPage' @@ -95,6 +98,12 @@ function App() { {/* Văn phòng số — Phòng họp Booking + Catalog (Phase 10.2 G-O2 — Mig 36 S36) */} } /> } /> + {/* [W2 KHKK S161] Kế hoạch ký kết HĐ (GĐ2). KHÔNG route matrix bên admin — + Khkk_WfView admin trỏ Designer type-10 (Layout.tsx staticMap). */} + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index dc4d3c7..39e9294 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -29,14 +29,15 @@ const TYPE_CODE_TO_INT: Record = { // Ct__ sub-menu entries. 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) + // [W2 KHKK S161] GĐ2 hết skeleton — re-point trang thật (gotcha #50: thiếu route = + // sidebar drop SILENT). WfView phía admin trỏ Designer type-10 (fe-admin CỐ Ý không có + // matrix page — chốt 2026-07-27; admin "Luồng duyệt" = dựng/sửa quy trình). + Khkk_WfView: '/system/approval-workflows-v2/ContractSigningPlan', + Khkk_List: '/khkk/list', + Khkk_Create: '/khkk/create', + Khkk_Pending: '/khkk/list?filter=ChoDuyet', + Khkk_Approved: '/khkk/list?filter=DaDuyet', + Khkk_Deleted: '/khkk/list?view=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-admin/src/pages/khkk/KhkkCreatePage.tsx b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx new file mode 100644 index 0000000..313b60d --- /dev/null +++ b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx @@ -0,0 +1,254 @@ +// [W2 KHKK — S161 2026-07-29] Lập "Kế hoạch ký kết HĐ" từ phiếu Duyệt NCC đã duyệt. +// Khuôn `pages/office/ProposalCreatePage.tsx` (PURO: ui/PageHeader + section +// .card-accent + .icon-chip header). File MIRROR SHA256 identical với fe-admin. +// +// 3 section: (1) chọn phiếu PE nguồn — chỉ phiếu DaDuyet CHƯA có kế hoạch sống +// (BE lọc, FE không tự đoán); (2) quy trình duyệt V2 ApplicableType=10 admin đã +// ghim; (3) ghi chú. Dòng giá per-NCC do BE tự sinh từ winner của phiếu PE +// (snapshot `PeReferenceAmount`) — FE KHÔNG gửi lines. +import { useState, type FormEvent } from 'react' +import { useMutation, useQuery } from '@tanstack/react-query' +import { useNavigate } from 'react-router-dom' +import { CheckCircle2, FileCheck, FileText, Save, Settings2, X } from 'lucide-react' +import { toast } from 'sonner' +import { PageHeader } from '@/components/ui/PageHeader' +import { Button } from '@/components/ui/Button' +import { Label } from '@/components/ui/Label' +import { Textarea } from '@/components/ui/Textarea' +import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' +import { cn } from '@/lib/cn' +import type { CreateKhkkInput, KhkkPeCandidateDto } from '@/types/khkk' + +// ApplicableType của workflow V2 dành cho Kế hoạch ký kết (mirror BE +// `ApplicableType.ContractSigningPlan = 10`, Designer fe-admin đã mở từ W1). +const APPLICABLE_TYPE_KHKK = 10 + +interface WorkflowDto { + id: string + code: string + name: string + isUserSelectable: boolean +} + +function formatDate(iso: string): string { + const d = new Date(iso) + return d.toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + +export function KhkkCreatePage() { + const navigate = useNavigate() + const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('') + const [approvalWorkflowId, setApprovalWorkflowId] = useState('') + const [ghiChu, setGhiChu] = useState('') + + // Phiếu PE đủ điều kiện: Phase=DaDuyet VÀ chưa có kế hoạch sống (BE lọc, + // mirror `ListApprovedPurchaseEvaluationsQuery` + vị ngữ chưa-có-plan). + const candidates = useQuery({ + queryKey: ['khkk-pe-candidates'], + queryFn: async () => + (await api.get('/contract-signing-plans/approved-pe-awaiting-plan')).data, + }) + + // Endpoint trả AwAdminOverviewDto { types: [{ applicableType, history: [...] }] } — + // KHÔNG phải flat array (khuôn ProposalCreatePage:59-69). Lọc isUserSelectable = + // quy trình admin đã ghim cho user pick. + const workflows = useQuery({ + queryKey: ['approval-workflows-v2', { applicableType: APPLICABLE_TYPE_KHKK, isUserSelectable: true }], + queryFn: async () => { + const res = await api.get<{ types: { applicableType: number; history: WorkflowDto[] }[] }>( + '/approval-workflows-v2', + { params: { applicableType: APPLICABLE_TYPE_KHKK } }, + ) + const bucket = res.data.types.find((t) => t.applicableType === APPLICABLE_TYPE_KHKK) + return (bucket?.history ?? []).filter((w) => w.isUserSelectable) + }, + }) + + const create = useMutation({ + mutationFn: async () => { + if (!purchaseEvaluationId) throw new Error('Vui lòng chọn phiếu Duyệt NCC nguồn') + if (!approvalWorkflowId) throw new Error('Vui lòng chọn Quy trình duyệt') + const body: CreateKhkkInput = { + peId: purchaseEvaluationId, // [F-2] BE Command field = PeId + approvalWorkflowId, + ghiChu: ghiChu.trim() || null, + } + const res = await api.post<{ id: string; maKeHoach: string | null }>('/contract-signing-plans', body) + return res.data + }, + onSuccess: (data) => { + toast.success(data.maKeHoach ? `Đã lập kế hoạch ${data.maKeHoach}` : 'Đã lập kế hoạch ký kết') + navigate(`/khkk/${data.id}`) + }, + onError: (e) => toast.error(getErrorMessage(e)), + }) + + const onSubmit = (e: FormEvent) => { + e.preventDefault() + create.mutate() + } + + const list = candidates.data ?? [] + + return ( +
+ } + accent="brand" + actions={ + + } + /> + +
+ {/* Section 1: Phiếu Duyệt NCC nguồn */} +
+
+ + + +

+ Phiếu Duyệt NCC nguồn * +

+
+
+ {candidates.isLoading &&
Đang tải danh sách phiếu...
} + {!candidates.isLoading && list.length === 0 && ( +
+ Không có phiếu Duyệt NCC nào đã duyệt mà chưa lập kế hoạch ký kết. +
+ )} +
+ {list.map((pe) => { + const selected = pe.id === purchaseEvaluationId + return ( + + ) + })} +
+
+
+ + {/* Section 2: Quy trình duyệt */} +
+
+ + + +

Quy trình duyệt

+
+
+ + +
+ Chỉ hiển thị quy trình loại "Kế hoạch ký kết HĐ" admin đã ghim cho user chọn +
+
+
+ + {/* Section 3: Ghi chú */} +
+
+ + + +

Ghi chú

+
+
+ +