[CLAUDE] Contract: UAT S174 tinh-gon UI KHKK + cot 2 tang KHKK o workspace PE (FE-only x2 app, 0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m51s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m51s
Anh chot qua 6 anh khoanh do + 4 cau tra loi. 7 chinh, mirror SHA256 identical 8/8 cap. Man DANH SACH KHKK (KhkkListPage / KhkkListPanel) - Giu DUY NHAT header trang "Ke hoach ky ket HD / Giai doan 2". - Bo o loc "Tat ca nhom": may loc GIU NGUYEN (query + stage2Content), nhom nay den DUY NHAT tu URL ?group=n (8 leaf menu trai). Go luon prop onGroupChange (noUnusedLocals). - Bo thanh "Cay toan trinh - N goi" qua prop MOI `hideHeader` opt-in tren PipelineTreePanel. 6 host con lai (PE / GD3 / GD4 / Inbox) KHONG truyen => hinh cu nguyen ven. Nut "Hien/An cay" (mobile) song TRONG thanh vua an => hideHeader ep than cay `flex`, neu khong `<lg` mat trang cai cay. Man CHI TIET KHKK (KhkkDetailPage / KhkkDetailContent) - Go khoi "Quy trinh duyet" (so-do Buoc/Cap + HANH DONG + 2 lich su) — day la BAN SAO THU HAI, ban chinh o Panel-3 man danh sach. Anh: "bo het, no se hien thi o Menu duyet". KHAI HE QUA: `<lg` dieu huong thang vao trang nay => mobile hien khong con Gui duyet / Xoa phieu. Mo lai = boc `lg:hidden`. - Section 5 "Y kien cap duyet" doi sang DUNG style Duyet NCC (PeDetailTabs.tsx:705-774): card `rounded-lg p-3` + h4 ten cap + chip trang thai + y kien text-sm + chan ngay border-t; thay o phang 1 dong text-[11px]. GIU o "Chua ky" (PE khong co) — vang o thi nguoi doc khong biet con thieu chu ky cua ai. Man TAO KHKK (KhkkCreatePage) - Doi sang khuon "Tao phieu Duyet NCC moi" (PeWorkspaceCreateView): 1 card + divide-y, thanh dau ke LUONG, thanh cuoi hint do liet ke DICH DANH truong con thieu thay vi chi lam xam nut. Nut "Luu nhap" -> "Tao ke hoach". - Ve "tao xong sang chi tiet" VON DA DUNG tu S161 (onSuccess -> navigate /khkk/:id); dot nay KHONG dung luong do, chi doi hinh. Man DUYET NCC — THAO TAC (PurchaseEvaluationWorkspacePage + PeKhkkSidePanel MOI) - Them cot 3: 1 cot 2 TANG 50/50 — tren = PE da duyet CHUA lap KHKK gom theo DU AN, duoi = KHKK da tao. Ti le 50/50 bang `min-h-0 flex-1` ca hai tang. - 0 ENDPOINT MOI: dung lai approved-pe-awaiting-plan + contract-signing-plans. "Chua co KHKK" = existingGroups RONG — tu K2/S164 BE KHONG con loc ho, loc o FE. - GATE #85: ca 2 cua gac bang policy `KeHoachKyKet.Read` trong khi day la man PE => gate bang DUNG key policy cua endpoint (KHONG OR key menu Khkk_*), thieu quyen thi NOI RA thay vi de cot trang (cot trang doc thanh "chua co gi"). - Cot chi moc tu `xl`; o `lg` bo cuc giu DUNG 2 panel nhu cu. Con lai (chua lam, da khai voi anh): bam PE o tang tren moi mo /khkk/create chu chua chon san dung phieu do — can them ?peId= vao man tao. Verify: build 2 app PASS; mirror SHA256 8/8 cap KHOP; byte-level grep bundle xac nhan chuoi moi co mat ca 2 app (khong tin dong "built" — bai gotcha #77). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,25 +1,32 @@
|
||||
// [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.
|
||||
// File MIRROR SHA256 identical với fe-user counterpart.
|
||||
//
|
||||
// 3 section: (1) chọn phiếu PE nguồn — phiếu DaDuyet có NCC trúng thầu (BE lọc, FE
|
||||
// không tự đoán); (2) NHÓM DUYỆT N1..N8 + 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.
|
||||
//
|
||||
// [K2 S164 — OG-7 UI-hướng-1] ĐỔI HÌNH DẠNG màn này: trước K2, PE đã có kế hoạch bị
|
||||
// BE loại khỏi picker (biến mất) và tạo trùng thì ăn 409 chặn hẳn. Từ K2, rào là
|
||||
// "1 PE = 1 kế hoạch MỖI NHÓM" ⇒ PE vẫn hiện, kèm danh sách nhóm đã dùng + nút phụ
|
||||
// "Tạo thêm cho nhóm khác" (preset sang nhóm còn trống). Ẩn PE đ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".
|
||||
import { useState, type FormEvent } from 'react'
|
||||
// [S174 owner] ĐỔI KHUÔN: bỏ `PageHeader` + 3 khối `.card-accent` rời, chuyển sang ĐÚNG
|
||||
// hình "Tạo phiếu Duyệt NCC mới" (`components/pe/PeWorkspaceCreateView.tsx:152-352`) —
|
||||
// anh chốt "Tạo Header → tương tự NCC → sau khi Header tạo xong thì cho sang chi tiết":
|
||||
// · MỘT card `rounded-lg border bg-white shadow-sm`, các section ngăn bằng `divide-y`;
|
||||
// · thanh đầu = tiêu-đề + 1 dòng kể LUỒNG (nhập gì → bấm gì → cái gì mở khoá) + "← Hủy";
|
||||
// · thanh cuối = hint TRÁI liệt kê ĐÍCH DANH trường còn thiếu, nút PHẢI.
|
||||
// 🔸 Vế "sang chi tiết" VỐN ĐÃ ĐÚNG từ S161 (`onSuccess` → `navigate('/khkk/:id')`) — đợt
|
||||
// này KHÔNG đụng luồng đó, chỉ đổi hình. Ghi ra để người sau khỏi tưởng vừa thêm mới.
|
||||
//
|
||||
// [K2 S164 — OG-7 UI-hướng-1] Rào là "1 PE = 1 kế hoạch MỖI NHÓM" ⇒ PE vẫn hiện, kèm
|
||||
// danh sách nhóm đã dùng + nút phụ "Tạo thêm cho nhóm khác" (preset sang nhóm còn trống).
|
||||
// Ẩn PE đ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".
|
||||
import { useState, type FormEvent, type ReactNode } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { CheckCircle2, FileCheck, FileText, Layers, Plus, Save, Settings2, X } from 'lucide-react'
|
||||
import { CheckCircle2, Plus } 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 { Select } from '@/components/ui/Select'
|
||||
import { Textarea } from '@/components/ui/Textarea'
|
||||
import { api } from '@/lib/api'
|
||||
import { getErrorMessage } from '@/lib/apiError'
|
||||
@ -92,8 +99,7 @@ export function KhkkCreatePage() {
|
||||
})
|
||||
|
||||
// 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.
|
||||
// KHÔNG phải flat array. 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 () => {
|
||||
@ -121,6 +127,7 @@ export function KhkkCreatePage() {
|
||||
const res = await api.post<{ id: string; maKeHoach: string | null }>('/contract-signing-plans', body)
|
||||
return res.data
|
||||
},
|
||||
// 🔴 Vế "Header tạo xong thì sang chi tiết" nằm ĐÚNG ở đây — giữ nguyên từ S161.
|
||||
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}`)
|
||||
@ -142,6 +149,15 @@ export function KhkkCreatePage() {
|
||||
const takenGroups = selectedPe?.existingGroups ?? []
|
||||
const groupTaken = takenGroups.includes(approvalGroup)
|
||||
|
||||
// [S174] Khuôn `PeWorkspaceCreateView.tsx:145-150`: liệt kê ĐÍCH DANH cái còn thiếu thay
|
||||
// vì chỉ khoá nút — nút xám không nói được vì sao nó xám.
|
||||
const missingCreate = [
|
||||
!purchaseEvaluationId && 'Phiếu Duyệt NCC nguồn',
|
||||
!approvalWorkflowId && 'Quy trình duyệt',
|
||||
groupTaken && `Nhóm duyệt còn trống (N${approvalGroup} đã có kế hoạch)`,
|
||||
].filter(Boolean) as string[]
|
||||
const canSubmit = missingCreate.length === 0 && !create.isPending
|
||||
|
||||
// Bấm chọn PE (cả ở dòng lẫn ở nút "Tạo thêm cho nhóm khác"): nếu nhóm đang chọn đã
|
||||
// bị PE đó dùng thì nhảy sang nhóm trống đầu tiên ⇒ người dùng không phải tự dò.
|
||||
const selectPe = (pe: KhkkPeCandidateDto) => {
|
||||
@ -154,52 +170,37 @@ export function KhkkCreatePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
eyebrow="Giai đoạn 2"
|
||||
title="Lập kế hoạch ký kết HĐ"
|
||||
subtitle="Chọn phiếu Duyệt NCC đã duyệt → hệ thống chép dòng giá của từng NCC trúng thầu"
|
||||
icon={<FileCheck className="h-5 w-5" />}
|
||||
accent="brand"
|
||||
actions={
|
||||
<Button variant="outline" onClick={() => navigate('/khkk/list')}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Huỷ
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<form onSubmit={onSubmit} className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
{/* Thanh đầu — mirror `PeWorkspaceCreateView.tsx:155-165` */}
|
||||
<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ý kết HĐ mới</h2>
|
||||
<p className="mt-0.5 text-[12px] text-slate-500">
|
||||
Nhập Section 1 + 2 → bấm <strong>“Tạo kế hoạch”</strong> → sau đó dòng giá NCC / căn cứ hồ sơ / ý kiến mở khóa.
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" onClick={() => navigate('/khkk/list')} className="text-xs">
|
||||
← Hủy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-5">
|
||||
{/* Section 1: Phiếu Duyệt NCC nguồn */}
|
||||
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-brand-500)' }}>
|
||||
<header className="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
||||
<span
|
||||
className="icon-chip"
|
||||
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-brand-50)', ['--chip-fg' as string]: 'var(--color-brand-600)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold tracking-tight text-brand-800">
|
||||
Phiếu Duyệt NCC nguồn <span className="text-red-500">*</span>
|
||||
</h3>
|
||||
</header>
|
||||
<div className="space-y-2 p-5">
|
||||
{candidates.isLoading && <div className="text-sm text-muted-foreground">Đang tải danh sách phiếu...</div>}
|
||||
{!candidates.isLoading && list.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-6 text-center text-sm text-muted-foreground">
|
||||
Không có phiếu Duyệt NCC nào đã duyệt và có NCC trúng thầu.
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
||||
{list.map((pe) => {
|
||||
const selected = pe.id === purchaseEvaluationId
|
||||
// [K2 S164 — OG-7] `?? []` = phòng payload cũ trong cache (trước K2 DTO
|
||||
// không có field này) — thiếu guard thì `.length` nổ trắng trang.
|
||||
const taken = pe.existingGroups ?? []
|
||||
const free = firstFreeGroup(taken)
|
||||
return (
|
||||
<div key={pe.id} className="space-y-1">
|
||||
<div className="divide-y divide-slate-200">
|
||||
<Section title="1. Phiếu Duyệt NCC nguồn *">
|
||||
{candidates.isLoading && <div className="text-sm text-muted-foreground">Đang tải danh sách phiếu...</div>}
|
||||
{!candidates.isLoading && list.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-6 text-center text-sm text-muted-foreground">
|
||||
Không có phiếu Duyệt NCC nào đã duyệt và có NCC trúng thầu.
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
||||
{list.map((pe) => {
|
||||
const selected = pe.id === purchaseEvaluationId
|
||||
// [K2 S164 — OG-7] `?? []` = phòng payload cũ trong cache (trước K2 DTO
|
||||
// không có field này) — thiếu guard thì `.length` nổ trắng trang.
|
||||
const taken = pe.existingGroups ?? []
|
||||
const free = firstFreeGroup(taken)
|
||||
return (
|
||||
<div key={pe.id} className="space-y-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectPe(pe)}
|
||||
@ -264,39 +265,23 @@ export function KhkkCreatePage() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
{/* Section 2: Quy trình duyệt */}
|
||||
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-violet-500)' }}>
|
||||
<header className="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
||||
<span
|
||||
className="icon-chip"
|
||||
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-violet-50)', ['--chip-fg' as string]: 'var(--color-violet-700)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</span>
|
||||
<h3 className="text-sm font-semibold tracking-tight text-violet-700">Nhóm duyệt & quy trình</h3>
|
||||
</header>
|
||||
<div className="grid grid-cols-1 gap-4 p-5 md:grid-cols-2">
|
||||
<Section title="2. Nhóm duyệt & quy trình">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{/* [K2 S164 — vá-4] Nhóm duyệt N1..N8. Mặc định nhóm 1 (UI-hướng-1: ca HẸP,
|
||||
user cần nhóm khác thì đổi ngay tại đây). Nhóm PE đã dùng bị disable —
|
||||
rào cứng 409 vẫn ở BE, ô này chỉ để không mời bấm nhầm. */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="approvalGroup" className="flex items-center gap-1.5">
|
||||
<Layers className="h-3.5 w-3.5 text-violet-600" />
|
||||
Nhóm duyệt (N1–N8) <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<select
|
||||
<div>
|
||||
<Label htmlFor="approvalGroup" className="text-[11px]">Nhóm duyệt (N1–N8) *</Label>
|
||||
<Select
|
||||
id="approvalGroup"
|
||||
value={approvalGroup}
|
||||
onChange={(e) => setApprovalGroup(Number(e.target.value))}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
{KHKK_APPROVAL_GROUPS.map((g) => {
|
||||
const used = takenGroups.includes(g)
|
||||
@ -307,83 +292,92 @@ export function KhkkCreatePage() {
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</Select>
|
||||
{groupTaken ? (
|
||||
<div className="mt-1 text-xs font-medium text-red-600">
|
||||
<p className="mt-1 text-[11px] font-medium text-red-600">
|
||||
Phiếu Duyệt NCC này đã có kế hoạch ký kết cho nhóm N{approvalGroup} — chọn nhóm khác.
|
||||
</div>
|
||||
</p>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
Mỗi phiếu Duyệt NCC lập được 1 kế hoạch cho MỖI nhóm duyệt
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="workflow">
|
||||
Quy trình duyệt <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<select
|
||||
<div>
|
||||
<Label htmlFor="workflow" className="text-[11px]">Quy trình duyệt *</Label>
|
||||
<Select
|
||||
id="workflow"
|
||||
value={approvalWorkflowId}
|
||||
onChange={(e) => setApprovalWorkflowId(e.target.value)}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm"
|
||||
>
|
||||
<option value="">— Chọn quy trình —</option>
|
||||
<option value="">— Chọn quy trình duyệt —</option>
|
||||
{workflows.data?.map((w) => (
|
||||
<option key={w.id} value={w.id}>
|
||||
{w.code} - {w.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Chỉ hiển thị quy trình loại "Kế hoạch ký kết HĐ" admin đã ghim cho user chọn
|
||||
</div>
|
||||
</Select>
|
||||
{workflows.data && workflows.data.length === 0 ? (
|
||||
<p className="mt-1 text-[11px] text-amber-700">
|
||||
⚠ Chưa có quy trình duyệt "Kế hoạch ký kết HĐ" nào được ghim cho user chọn. Liên hệ admin tạo trước.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-1 text-[11px] text-slate-500">
|
||||
Chỉ hiển thị quy trình loại "Kế hoạch ký kết HĐ" admin đã ghim cho user chọn
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Section>
|
||||
|
||||
{/* Section 3: Ghi chú */}
|
||||
<section className="card-accent" style={{ ['--accent' as string]: 'var(--color-teal-500)' }}>
|
||||
<header className="flex items-center gap-2 border-b border-slate-100 px-5 py-3">
|
||||
<span
|
||||
className="icon-chip"
|
||||
style={{ height: '1.75rem', width: '1.75rem', ['--chip-bg' as string]: 'var(--color-teal-50)', ['--chip-fg' as string]: 'var(--color-teal-700)' }}
|
||||
aria-hidden
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<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>
|
||||
|
||||
{/* Thanh cuối — hint TRÁI kể rõ còn thiếu gì, nút PHẢI (mirror `:318-337`).
|
||||
`groupTaken` chặn ngay ở FE lượt bấm chắc chắn ăn 409 của BE
|
||||
(`Features.cs:395-402`) — vẫn để BE là rào thật, đây chỉ đỡ 1 vòng mạng. */}
|
||||
<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>
|
||||
<h3 className="text-sm font-semibold tracking-tight text-teal-700">Ghi chú</h3>
|
||||
</header>
|
||||
<div className="space-y-2 p-5">
|
||||
<Label htmlFor="ghiChu">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="text-xs text-muted-foreground mt-1">{ghiChu.length}/2000</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => navigate('/khkk/list')}>
|
||||
Huỷ
|
||||
) : (
|
||||
<span className="font-medium text-emerald-600">✓ Đủ thông tin bắt buộc — có thể tạo kế hoạch</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button type="button" variant="ghost" onClick={() => navigate('/khkk/list')} className="text-xs">
|
||||
Hủy
|
||||
</Button>
|
||||
{/* [K2 S164] `groupTaken` chặn ngay ở FE lượt bấm chắc chắn ăn 409 của BE
|
||||
(`Features.cs:395-402`) — vẫn để BE là rào thật, đây chỉ là đỡ 1 vòng mạng. */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={create.isPending || !purchaseEvaluationId || !approvalWorkflowId || groupTaken}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{create.isPending ? 'Đang lưu...' : 'Lưu nháp'}
|
||||
<Button type="submit" disabled={!canSubmit}>
|
||||
{create.isPending ? 'Đang tạo…' : 'Tạo kế hoạch'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user