Files
solution-erp/fe-admin/src/pages/khkk/KhkkCreatePage.tsx
pqhuy1987 a1b9cbb175
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m41s
[CLAUDE] Contract: UAT S174 dot-A — 3 LOI THAT + O2 bo chu SAP + bo nut Tu choi (FE-only, 0-mig)
Nguon: /fable-clone frontend-designer ensemble 5 lane (wf_016b1bed-b85, 5/5 xong, 0 chet,
755K tok). Spec day du: runs/2026-08-05-S174-khkk-style-audit/spec-khkk-style-dong-bo-05-08-2026.md

=== DOT A — LOI THAT (khong phai style) ===

A1 🔴 Nut "Tai xuong" file dinh kem KHKK BI 401 — lane L1 bat, lead verify.
  Cu: <a href="/api/contract-signing-plans/.../download"> TRAN.
  JWT chi gan boi interceptor axios (lib/api.ts:18); the <a> KHONG di qua axios => request
  toi [Authorize(Policy="KeHoachKyKet.Read")] (ContractSigningPlansController.cs:222-223)
  di TAY KHONG => 401, nut khong tai duoc.
  Moi: ham downloadAttachment() dung khuon PE (PeWorkflowPanel.tsx:327-342) —
  api.get(responseType:'blob') -> createObjectURL -> click ngam -> revoke.

A2 🔴 REGRESSION DO CHINH LEAD GAY RA TRONG PHIEN NAY (lane L3 bat).
  Man Thao tac dung <main className="hidden ... lg:block"> => duoi lg chi con panel-1,
  KHONG co form => MOBILE KHONG TAO DUOC PHIEU (bam phieu xong khong co cho bam "Tao").
  Va 2 ve: (1) <main> bo hidden, hien moi kho; (2) mobile xep DOC voi panel-1 chot tran
  14rem — khong chot thi danh sach dai day form roi khoi man, dung cai ngo cut vua va.

A3 Tra no da khai voi owner: trang chi tiet KHKK tren mobile mat nut Gui duyet / Xoa phieu.
  Giu KhkkWorkflowPanel o DUNG lg:hidden — desktop van sach (panel-3 lo), mobile thao tac duoc.

=== OWNER CHOT VONG NAY ===

O2 "Dung bo chu sap di" — go SoonChip().
  Lane L4 do: KHONG giai doan nao con dung nghia "sap" (ca 4 deu fetch API that);
  SoonChip KHONG export, 8 hit noi bo => 0 host ngoai phu thuoc.
  🔴 Go != "thay het bang 0". BA ca, BA cach xu dung nghia:
    · 2 ca dem-duoc      -> luon in SO (ke ca 0)
    · ca KHOA-QUYEN      -> BO TRANG (in 0 la NOI DOI: co the day phieu, chi la khong duoc
                            xem, va cung khong duoc phep goi API de biet). O khoa + title du.
    · ca FAILED (dau —)  -> GIU NGUYEN ("khong tai duoc" khac "co 0 phieu", dung gop)

Cau-4 "vay thi bo luon tu choi ~ tuong tu PE" — go nut TU CHOI khoi KhkkWorkflowPanel.
  PE co code tu-choi nhung THUC TE khong hien (BE go khoi nextPhases + FE loc, UAT S60).
  🔴 Chi go DUONG BAM o UI. May BE (KhkkTransitionAction.Reject, phase TuChoi=99) GIU
  NGUYEN: phieu cu da tu-choi van hien dung, mo lai sau chi la them nut.

B4 Grid man Thao tac 300/340 -> 260/320 dung so cua PE (PurchaseEvaluationWorkspacePage:80).

=== HAI GIA-DINH CUA LEAD BI LANE BAC (lead da tu verify, lane DUNG) ===
- O3 hang-muc N-dong KHONG can migration. ContractSigningPlanLineConfiguration.cs:37-38
  unique index 3 COT (PlanId, SupplierId, CatalogEntryId) => 1 NCC DA duoc phep N dong x
  N hang muc tu Mig 71. May rong san, chi UI hep. Lead tung bao owner "nang nhat, can
  migration" — SAI, da dinh chinh.
- O4 shopdrawing dinh kem = FE-only 0-BE (rail dossierItemId da land du 6 tang tu Mig 69).

Verify: build 2 app PASS; mirror SHA256 4/4 cap KHOP; control-am: <SoonChip=0 · "> Tu choi"=0
· a-href=0; control-duong: responseType=2 + downloadAttachment=2 CA HAI app.
🔴 CHUA co screenshot — ca 5 lane deu khong chup duoc (dev-rig chan: chi :8080 LISTENING,
API :5443 khong co, trang sau ProtectedRoute — gotcha #3). Phai soi that 375/1366/1440 tren
prod sau deploy truoc khi tuyen PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:52:12 +07:00

403 lines
22 KiB
TypeScript

// [W2 KHKK — S161] Màn "Kế hoạch ký kết HĐ — Thao tác" (route `/khkk/create`, 8 leaf nhóm).
// File MIRROR SHA256 identical với fe-user counterpart.
//
// [S174 owner] ĐỔI TỪ TRANG-GIẤY-CUỘN SANG HOST 2-PANEL — anh chốt: **"làm giống giao diện
// thao tác của Duyệt NCC"** (`pages/pe/PurchaseEvaluationWorkspacePage.tsx`). Khuôn bê về:
// · header 1 dòng + grid `lg:grid-cols-[300px_1fr]`;
// · Panel 1 = danh sách (`KhkkWorkListPanel`) + nút hành-động dính đáy;
// · Panel 2 = EmptyState | form tạo | phiếu đang XEM.
// 🔴 Panel 1 chia 2 TẦNG theo đúng ảnh anh khoanh: trên = "Cột danh sách Duyệt NCC"
// (bấm ⇒ *tạo luôn KHKK*), dưới = "Cột KHKK đã tạo" (bấm ⇒ *chỉ xem*).
// 🔴 Bấm gì cũng Ở LẠI MÀN — anh chốt "click vào phiếu nào thì cho hiện phiếu đó luôn, chỉ
// view là đc, ko re-direct về phiếu gốc". Vì vậy panel nhận CALLBACK, không tự `navigate`.
// 🧊 Bản S174 đầu gắn 2 tầng này thành cột 3 của màn *Duyệt NCC* — anh bác ("chỗ duyệt NCC
// đã golive và đã tốt rồi"); `PurchaseEvaluationWorkspacePage` đã revert về byte `1b3bcaa`.
//
// [K2 S164 — OG-7] Rào là "1 PE = 1 kế hoạch MỖI NHÓM" ⇒ PE đã dùng vài nhóm VẪN hiện, kèm
// nhóm đã dùng + nút "Tạo thêm cho nhóm khác". Ẩn đi thì người dùng không phân biệt được
// "PE không đủ điều kiện" với "PE đã dùng hết nhóm".
//
// [K4b S166 — R-5] 8 nhóm dùng CHUNG một route ⇒ mọi thứ lấy từ URL phải ĐỌC LẠI MỖI RENDER.
// Khối reset dưới là SET-STATE-TRONG-RENDER CÓ CHỦ ĐÍCH; refactor thành `useEffect` sẽ
// tái-hiện vá-5 (đi từ leaf nhóm 2 sang leaf nhóm 5 phải F5 mới đúng). ĐỪNG ĐỘNG.
import { useState, type FormEvent, type ReactNode } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { FileCheck, X } from 'lucide-react'
import { toast } from 'sonner'
import { EmptyState } from '@/components/EmptyState'
import { Button } from '@/components/ui/Button'
import { Label } from '@/components/ui/Label'
import { Textarea } from '@/components/ui/Textarea'
import { KhkkDetailContent } from '@/components/khkk/KhkkDetailContent'
import { KhkkWorkListPanel } from '@/components/khkk/KhkkWorkListPanel'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import {
KHKK_APPROVAL_GROUP_DEFAULT,
KHKK_APPROVAL_GROUP_LABELS,
khkkGroupMenuLabel,
type CreateKhkkInput,
type KhkkDetailDto,
type KhkkPeCandidateDto,
} from '@/types/khkk'
// ApplicableType của workflow V2 dành cho Kế hoạch ký kết (mirror BE
// `ApplicableType.ContractSigningPlan = 10`).
const APPLICABLE_TYPE_KHKK = 10
interface WorkflowDto {
id: string
code: string
name: string
isUserSelectable: boolean
}
// 🧊 [S174] `firstFreeGroup()` ĐÃ GỠ cùng lúc bỏ ô chọn nhóm — nó chỉ tồn tại để tự nhảy
// sang nhóm trống, đúng hành-vi anh vừa cấm ("gán cứng theo từng nhóm, ko cho User điều
// chỉnh"). Giữ lại thành hàm chết thì lượt sau có người gọi lại.
export function KhkkCreatePage() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const groupParam = searchParams.get('group')
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
const [ghiChu, setGhiChu] = useState('')
// [S174 owner] "gán cứng luôn theo từng nhóm, ko cho User điều chỉnh chỗ này luôn"
// ⇒ nhóm duyệt KHÔNG còn là state: DẪN XUẤT thẳng từ leaf menu (`?group=n`). Không còn
// state thì không thể lệch, nên khối đồng-bộ-preset cũ (`lastPresetGroup`) cũng bỏ luôn —
// đó là thứ chỉ tồn tại để vá đúng cái lệch này.
const approvalGroup = presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT
// [S174] Phiếu KHKK đang XEM ở panel 2. Có giá trị ⇒ panel 2 hiện phiếu (read-only) thay
// cho form tạo. `null` ⇒ về form tạo. Đây là thứ thay cho việc `navigate('/khkk/:id')`.
const [viewPlanId, setViewPlanId] = useState<string | null>(null)
// Cùng query key với `KhkkWorkListPanel` ⇒ dùng CHUNG cache, không gọi mạng 2 lần.
const candidates = useQuery({
queryKey: ['khkk-pe-candidates'],
queryFn: async () =>
(await api.get<KhkkPeCandidateDto[]>('/contract-signing-plans/approved-pe-awaiting-plan')).data,
})
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)
},
})
// [S174 owner] Quy trình duyệt cũng GÁN CỨNG theo nhóm — mỗi nhóm đúng MỘT quy trình.
// 🔴 Chuỗi mã lấy từ NGUỒN BE, không suy từ màn hình: `DbInitializer.cs:609`
// `var code = $"KHKK-N{n}"` (vòng `for n = 1..8`, seed idempotent per-Code).
// Không khớp ⇒ KHÔNG tự rơi về quy trình khác: chặn tạo + nói rõ thiếu mã nào, vì gán
// nhầm quy trình là phiếu đi sai đường duyệt — hỏng nặng hơn là không tạo được.
const wantWorkflowCode = `KHKK-N${approvalGroup}`
const autoWorkflow = workflows.data?.find(
(w) => (w.code ?? '').trim().toUpperCase() === wantWorkflowCode,
) ?? null
// Phiếu đang XEM — chỉ tải khi thật sự có cái để xem.
const viewPlan = useQuery({
queryKey: ['khkk-detail', viewPlanId],
queryFn: async () => (await api.get<KhkkDetailDto>(`/contract-signing-plans/${viewPlanId}`)).data,
enabled: !!viewPlanId,
})
const create = useMutation({
mutationFn: async () => {
if (!purchaseEvaluationId) throw new Error('Vui lòng chọn phiếu Duyệt NCC nguồn')
if (!autoWorkflow) throw new Error(`Chưa có quy trình duyệt ${wantWorkflowCode}`)
const body: CreateKhkkInput = {
peId: purchaseEvaluationId, // [F-2] BE Command field = PeId
approvalWorkflowId: autoWorkflow.id,
ghiChu: ghiChu.trim() || null,
approvalGroup, // [K2] gửi TƯỜNG MINH, không dựa default ngầm của BE
}
return (await api.post<{ id: string; maKeHoach: string | null }>('/contract-signing-plans', body)).data
},
// [S174] "Header tạo xong thì cho sang chi tiết" — nay KHÔNG rời màn nữa: mở luôn phiếu
// vừa tạo ở panel 2 (chỉ xem) và trả form về trắng. Cùng tinh thần "không re-direct".
onSuccess: (data) => {
toast.success(data.maKeHoach ? `Đã lập kế hoạch ${data.maKeHoach}` : 'Đã lập kế hoạch ký kết')
setPurchaseEvaluationId('')
setGhiChu('')
setViewPlanId(data.id)
},
onError: (e) => toast.error(getErrorMessage(e)),
})
const list = candidates.data ?? []
const selectedPe = list.find((p) => p.id === purchaseEvaluationId)
const takenGroups = selectedPe?.existingGroups ?? []
const groupTaken = takenGroups.includes(approvalGroup)
const missingCreate = [
!purchaseEvaluationId && 'Phiếu Duyệt NCC nguồn',
!workflows.isLoading && !autoWorkflow && `Quy trình ${wantWorkflowCode} (chưa cấu hình)`,
groupTaken && `Phiếu này đã có kế hoạch nhóm N${approvalGroup}`,
].filter(Boolean) as string[]
const canSubmit = missingCreate.length === 0 && !!autoWorkflow && !create.isPending
// [S174] Bấm PE ở panel 1 = nạp vào form + rời chế-độ xem. 🧊 Khối "nhảy sang nhóm trống
// đầu tiên" ĐÃ BỎ: nhóm nay gán cứng theo leaf menu, tự nhảy nhóm = đúng thứ anh vừa cấm.
// PE đã có kế hoạch ở nhóm này ⇒ `groupTaken` chặn + nói rõ, người dùng tự sang leaf khác.
const pickPe = (peId: string) => {
setPurchaseEvaluationId(peId)
setViewPlanId(null)
}
const onSubmit = (e: FormEvent) => {
e.preventDefault()
create.mutate()
}
const headerTitle = presetGroup !== null
? `Kế hoạch ký kết HĐ — Thao tác · ${khkkGroupMenuLabel(presetGroup)}`
: 'Kế hoạch ký kết HĐ — Thao tác'
return (
<div className="flex h-[calc(100vh-4rem)] flex-col">
<header className="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white px-6 py-3">
<div className="flex min-w-0 items-center gap-2">
<FileCheck className="h-5 w-5 text-slate-500" />
<h1 className="truncate text-base font-semibold tracking-tight text-slate-900">{headerTitle}</h1>
</div>
<div className="text-[12px] text-slate-500">
Bấm phiếu cột trái tầng trên lập kế hoạch, tầng dưới chỉ xem.
</div>
</header>
{/* [S174 · B4] Bề ngang cột về ĐÚNG số của Duyệt NCC (`PurchaseEvaluationWorkspacePage.tsx:80`
`260px` / `xl:320px`) — trước đó lead tự đặt 300/340, lệch chuẩn không lý do.
[S174 · A2 — VÁ REGRESSION DO CHÍNH LEAD GÂY RA TRONG PHIÊN NÀY] `<main>` từng mang
`hidden … lg:block` ⇒ dưới `lg` chỉ còn panel-1, KHÔNG có form ⇒ **mobile không tạo
được phiếu**: bấm phiếu ở cột trái xong thì không có chỗ nào để bấm "Tạo kế hoạch".
🔴 Vá bằng 2 vế, thiếu vế nào cũng hỏng: (1) `<main>` bỏ `hidden`, hiện mọi khổ;
(2) mobile xếp DỌC với panel-1 chốt trần `14rem` — không chốt thì panel-1 (danh sách
dài) đẩy form rơi khỏi màn, đúng cái ngõ cụt vừa vá. Từ `lg` trở lên về 1 hàng như cũ. */}
<div className="grid flex-1 grid-cols-1 grid-rows-[minmax(0,14rem)_minmax(0,1fr)] overflow-hidden lg:grid-cols-[260px_1fr] lg:grid-rows-1 xl:grid-cols-[320px_1fr]">
<KhkkWorkListPanel
onPickPe={pickPe}
onPickPlan={setViewPlanId}
selectedPeId={purchaseEvaluationId || null}
selectedPlanId={viewPlanId}
/>
<main className="min-h-0 overflow-y-auto bg-slate-50 p-4 lg:p-6">
{/* (a) Đang XEM một kế hoạch — read-only, có đường quay về form tạo. */}
{viewPlanId && (
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-violet-200 bg-violet-50 px-4 py-2">
<span className="text-[12px] font-medium text-violet-800">
Đang xem kế hoạch chỉ xem, không sửa màn Thao tác.
</span>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => navigate(`/khkk/${viewPlanId}`)}>
Mở trang đy đ
</Button>
<Button variant="ghost" size="sm" onClick={() => setViewPlanId(null)}>
<X className="mr-1.5 h-3.5 w-3.5" />
Về form tạo
</Button>
</div>
</div>
{viewPlan.isLoading && <div className="text-sm text-slate-500">Đang tải</div>}
{!viewPlan.isLoading && !viewPlan.data && (
<div className="text-sm text-red-600">Không tìm thấy phiếu.</div>
)}
{viewPlan.data && (
<KhkkDetailContent plan={viewPlan.data} readOnly onChanged={() => viewPlan.refetch()} />
)}
</div>
)}
{/* (b) Chưa chọn gì — nói rõ phải bấm ở đâu, thay vì để trống một mảng trắng. */}
{!viewPlanId && !purchaseEvaluationId && (
<EmptyState
icon={FileCheck}
title="Chọn phiếu Duyệt NCC để lập kế hoạch"
description="Bấm một phiếu ở tầng trên cột trái để lập kế hoạch ký kết, hoặc bấm một kế hoạch ở tầng dưới để xem."
/>
)}
{/* (c) Đã chọn PE — form tạo. */}
{!viewPlanId && purchaseEvaluationId && (
<form onSubmit={onSubmit} className="rounded-lg border border-slate-200 bg-white shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-200 px-5 py-3">
<div>
<h2 className="text-base font-semibold text-slate-900">Lập kế hoạch kết mới</h2>
<p className="mt-0.5 text-[12px] text-slate-500">
Kiểm Section 1 + 2 bấm <strong>&ldquo;Tạo kế hoạch&rdquo;</strong> sau đó dòng giá NCC / căn cứ hồ / ý kiến mở khóa.
</p>
</div>
</div>
<div className="divide-y divide-slate-200">
<Section title="1. Phiếu Duyệt NCC nguồn *">
{/* Danh sách chọn nằm ở PANEL 1 — ở đây chỉ XÁC NHẬN cái đang chọn, để
không có hai chỗ chọn cùng một thứ (2 nguồn sự thật trên cùng màn). */}
<div className="rounded-lg border border-brand-300 bg-brand-50/60 p-3">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono text-xs text-brand-700">{selectedPe?.maPhieu ?? '—'}</span>
<span className="font-medium text-brand-800">{selectedPe?.tenGoiThau ?? '(đang tải…)'}</span>
</div>
<div className="mt-0.5 text-xs text-slate-500">
{selectedPe?.projectName ?? '—'}
{selectedPe?.workItemName ? ` · ${selectedPe.workItemName}` : ''}
</div>
{(selectedPe?.winnerSupplierNames?.length ?? 0) > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{selectedPe!.winnerSupplierNames.map((n) => (
<span
key={n}
className="inline-flex items-center rounded-md border border-emerald-300 bg-emerald-100 px-2 py-0.5 text-[11px] font-medium text-emerald-800"
>
{n}
</span>
))}
</div>
)}
{takenGroups.length > 0 && (
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs">
<span className="text-slate-500">Đã kế hoạch nhóm:</span>
{takenGroups.map((g) => (
<span
key={g}
className="inline-flex items-center rounded-md border border-amber-300 bg-amber-100 px-1.5 py-0.5 text-[11px] font-semibold text-amber-800"
title={KHKK_APPROVAL_GROUP_LABELS[g] ?? `Nhóm ${g}`}
>
N{g}
</span>
))}
</div>
)}
</div>
</Section>
{/* [S174 owner] "gán cứng luôn theo từng nhóm, ko cho User điều chỉnh chỗ này
luôn" ⇒ CẢ HAI ô thành GIÁ-TRỊ CỐ ĐỊNH, không còn `Select`. Cố ý KHÔNG
dùng `<Select disabled>`: ô xám vẫn trông như "bấm được nhưng đang khoá"
và vẫn nằm trong tab-order — hiển thị thẳng giá trị mới nói đúng rằng
đây là dữ-kiện, không phải lựa-chọn. Muốn nhóm khác thì đổi leaf ở menu. */}
<Section title="2. Nhóm duyệt & quy trình (theo menu, không đổi được)">
<div className="grid gap-3 md:grid-cols-2">
<div>
<Label className="text-[11px]">Nhóm duyệt</Label>
<div className="flex h-9 items-center gap-2 rounded-md border border-slate-200 bg-slate-50 px-3 text-sm text-slate-700">
<span className="rounded border border-violet-200 bg-violet-50 px-1.5 py-0.5 text-[10px] font-semibold text-violet-700">
N{approvalGroup}
</span>
<span className="truncate">
{KHKK_APPROVAL_GROUP_LABELS[approvalGroup] ?? `Nhóm ${approvalGroup}`}
</span>
</div>
{groupTaken ? (
<p className="mt-1 text-[11px] font-medium text-red-600">
Phiếu này đã kế hoạch cho nhóm N{approvalGroup} chọn phiếu khác,
hoặc sang leaf nhóm khác menu bên trái.
</p>
) : (
<p className="mt-1 text-[11px] text-slate-500">
Lấy theo mục đang mở menu đi nhóm bằng cách sang mục khác
</p>
)}
</div>
<div>
<Label className="text-[11px]">Quy trình duyệt</Label>
<div className="flex h-9 items-center rounded-md border border-slate-200 bg-slate-50 px-3 text-sm text-slate-700">
{workflows.isLoading ? (
<span className="text-slate-400">Đang tải</span>
) : autoWorkflow ? (
<span className="truncate">
<span className="font-mono text-[12px] text-brand-700">{autoWorkflow.code}</span>
<span className="text-slate-400"> · </span>
{autoWorkflow.name}
</span>
) : (
<span className="text-red-600">Không tìm thấy {wantWorkflowCode}</span>
)}
</div>
{!workflows.isLoading && !autoWorkflow ? (
<p className="mt-1 text-[11px] text-amber-700">
Chưa quy trình <strong>{wantWorkflowCode}</strong> (hoặc chưa đưc ghim
cho user chọn). Nhờ admin mở trong màn Quy trình duyệt rồi quay lại hệ thống
KHÔNG tự dùng quy trình của nhóm khác.
</p>
) : (
<p className="mt-1 text-[11px] text-slate-500">
Mỗi nhóm duyệt gắn đúng một quy trình hệ thống tự chọn theo nhóm
</p>
)}
</div>
</div>
</Section>
<Section title="3. Ghi chú">
<Label htmlFor="ghiChu" className="text-[11px]">Ghi chú kế hoạch</Label>
<Textarea
id="ghiChu"
value={ghiChu}
onChange={(e) => setGhiChu(e.target.value)}
maxLength={2000}
rows={4}
placeholder="Lưu ý về tiến độ ký kết, điều kiện thương thảo..."
/>
<div className="mt-1 text-[11px] text-slate-500">{ghiChu.length}/2000</div>
</Section>
</div>
{/* Hint TRÁI kể rõ còn thiếu gì, nút PHẢI. `groupTaken` chặn ở FE lượt bấm chắc
chắn ăn 409 của BE (`Features.cs:395-402`) — BE vẫn là rào thật. */}
<div className="flex items-center justify-between gap-3 border-t border-slate-200 bg-slate-50 px-5 py-3">
<div className="min-w-0 text-[11px]">
{missingCreate.length > 0 ? (
<span className="text-red-600">
Chưa đ đ tạo kế hoạch còn thiếu: <strong>{missingCreate.join(' · ')}</strong>
</span>
) : (
<span className="font-medium text-emerald-600"> Đ thông tin bắt buộc thể tạo kế hoạch</span>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
type="button"
variant="ghost"
className="text-xs"
onClick={() => { setPurchaseEvaluationId(''); setGhiChu('') }}
>
Hủy
</Button>
<Button type="submit" disabled={!canSubmit}>
{create.isPending ? 'Đang tạo…' : 'Tạo kế hoạch'}
</Button>
</div>
</div>
</form>
)}
</main>
</div>
</div>
)
}
// Helper — duplicate từ `PeWorkspaceCreateView` để tránh circular import (cùng lý do vì sao
// bên đó cũng tự dựng lại thay vì import từ `PeDetailTabs`).
function Section({ title, children }: { title: string; children: ReactNode }) {
return (
<section className="px-5 py-4">
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-slate-500">{title}</h3>
{children}
</section>
)
}