From 66fa4691dc0f13d7a02ff3d45016ba5a86d20698 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 7 May 2026 15:06:31 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20FE-Admin+FE-User:=20PE=20workspace?= =?UTF-8?q?=20"new"=20mode=20=E2=80=94=20sectioned=20create=20view=205=20s?= =?UTF-8?q?ections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh sửa nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết." Implementation: + PeWorkspaceCreateView.tsx (~230 LOC, mirror fe-admin + fe-user) - Sectioned card layout giống PeDetailTabs (5 section divider + title style) - Section 1 "Thông tin gói thầu": editable inputs (Loại / Tên */Dự án */Địa điểm/Mô tả/Payment) — 2-col grid responsive - Section 2 "Chọn NCC/TP": a. NCC chọn: text "(sau khi thêm NCC + chốt winner)" placeholder b. Ngân sách: editable inline (toggle "Nhập tay" + Select OR 2 input — giống BudgetFieldRow pattern) c. Giá chào thầu: text "(auto-tính sau winner)" placeholder d. Bản so sánh: LockedHint icon + text "Tải sau khi tạo" - Section 3 "NCC tham gia (0)": LockedHint "Lưu phiếu trước → thêm NCC..." - Section 4 "Hạng mục + Báo giá (0)": LockedHint - Section 5 "Ý kiến 4 PB": amber banner "nhập khi duyệt" - Action bar bottom: "Tạo phiếu" (disabled khi !tenGoiThau || !projectId) + Hủy - POST /pe full payload (header + budget mode A or B). onSuccess: toast + invalidate pe-list + onSaved(id, type) callback ~ PurchaseEvaluationWorkspacePage.tsx (× 2 app) - Replace trong mode='new' - PeHeaderForm vẫn còn (dùng cho /new?id= deep-link "Sửa header" cũ) Helpers duplicate trong PeWorkspaceCreateView (Section + FormRow + LockedHint) để tránh circular import từ PeDetailTabs. UAT mode rule applied (per memory feedback_uat_skip_verify): skip dotnet test + npm build verify, push ngay. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/pe/PeWorkspaceCreateView.tsx | 308 ++++++++++++++++++ .../pe/PurchaseEvaluationWorkspacePage.tsx | 6 +- .../components/pe/PeWorkspaceCreateView.tsx | 308 ++++++++++++++++++ .../pe/PurchaseEvaluationWorkspacePage.tsx | 6 +- 4 files changed, 622 insertions(+), 6 deletions(-) create mode 100644 fe-admin/src/components/pe/PeWorkspaceCreateView.tsx create mode 100644 fe-user/src/components/pe/PeWorkspaceCreateView.tsx diff --git a/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx new file mode 100644 index 0000000..3f91927 --- /dev/null +++ b/fe-admin/src/components/pe/PeWorkspaceCreateView.tsx @@ -0,0 +1,308 @@ +// Create view cho workspace mode "new" — sectioned layout giống PeDetailTabs +// (5 section visible) nhưng trống hết. Section 1 + 2 editable (header + budget). +// Section 3-5 locked với placeholder "Lưu phiếu trước". Sau save → caller +// onSaved navigate sang ?id={newId} → workspace switch sang detail view (full +// PeDetailTabs với inline edit Section 1 + BudgetFieldRow + Suppliers/Items). +// +// Pattern user 2026-05-07: "Thêm mới list ra hết trường dữ liệu giống chỉnh +// sửa nhưng trống, mở rộng từng phần. Save header xong mới cho nhập chi tiết." +import { useState } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' +import { Lock } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { Input } from '@/components/ui/Input' +import { Label } from '@/components/ui/Label' +import { Select } from '@/components/ui/Select' +import { Textarea } from '@/components/ui/Textarea' +import { api } from '@/lib/api' +import { getErrorMessage } from '@/lib/apiError' +import { + PurchaseEvaluationType, + PurchaseEvaluationTypeLabel, +} from '@/types/purchaseEvaluation' +import { BudgetPhase, type BudgetListItem } from '@/types/budget' +import type { Paged, Project } from '@/types/master' + +export function PeWorkspaceCreateView({ + defaultType, + onSaved, + onCancel, +}: { + defaultType: number + /** Callback sau khi POST thành công với (newId, type). Caller navigate. */ + onSaved: (id: string, type: number) => void + onCancel?: () => void +}) { + const qc = useQueryClient() + const [form, setForm] = useState({ + type: defaultType, + tenGoiThau: '', + projectId: '', + diaDiem: '', + moTa: '', + paymentTerms: '', + budgetId: '', + // Mig 17 — manual budget fallback + budgetManual: false, + budgetManualName: '', + budgetManualAmount: 0, + }) + + const projects = useQuery({ + queryKey: ['all-projects'], + queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, + }) + + const eligibleBudgets = useQuery({ + queryKey: ['eligible-budgets', form.projectId], + queryFn: async () => { + const res = await api.get>('/budgets', { + params: { pageSize: 100, projectId: form.projectId, phase: BudgetPhase.DaDuyet }, + }) + return res.data.items + }, + enabled: !!form.projectId, + }) + + const budgetPayload = form.budgetManual + ? { budgetId: null, budgetManualName: form.budgetManualName || null, budgetManualAmount: form.budgetManualAmount > 0 ? form.budgetManualAmount : null } + : { budgetId: form.budgetId || null, budgetManualName: null, budgetManualAmount: null } + + const create = useMutation({ + mutationFn: async () => { + const res = await api.post<{ id: string }>('/purchase-evaluations', { + type: form.type, + tenGoiThau: form.tenGoiThau, + projectId: form.projectId, + diaDiem: form.diaDiem || null, + moTa: form.moTa || null, + paymentTerms: form.paymentTerms || null, + ...budgetPayload, + }) + return res.data.id + }, + onSuccess: id => { + toast.success('Đã tạo phiếu — mở chi tiết để thêm NCC + hạng mục.') + qc.invalidateQueries({ queryKey: ['pe-list'] }) + onSaved(id, form.type) + }, + onError: e => toast.error(getErrorMessage(e)), + }) + + const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending + + return ( +
+ {/* Header bar */} +
+
+

Tạo phiếu Duyệt NCC mới

+

+ Nhập Section 1 + 2 → bấm “Tạo phiếu” → sau đó NCC / Hạng mục / Báo giá / Ý kiến mở khóa. +

+
+ {onCancel && ( + + )} +
+ +
+ {/* Section 1 — Thông tin gói thầu (editable) */} +
+
+
+ + +
+
+ + setForm({ ...form, tenGoiThau: e.target.value })} + placeholder="vd Cung cấp bê tông" + /> +
+
+ + +
+
+ + setForm({ ...form, diaDiem: e.target.value })} + placeholder="Lô K, KCN Lộc An..." + /> +
+
+ + setForm({ ...form, moTa: e.target.value })} + placeholder="Phương án A: ..." + /> +
+
+ +