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ú

+
+
+ +