Files
solution-erp/fe-user/src/pages/khkk/KhkkCreatePage.tsx
pqhuy1987 a0ccac3982
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m24s
[CLAUDE] Contract: UAT S174 dot-D + C3 — O6/O4/O7/O8 + 2 tong toan phieu (FE-only, 0-mig)
Owner S175 chot cach doc "sum tong" (gate TIEN): (1) truc = CHI TONG TOAN PHIEU, KHONG co
dong tong per-NCC; (2) hien 2 TONG RIENG canh nhau — Sigma gia DE XUAT ⟂ Sigma gia DUYET,
dat PHIA TREN bang. Mirror 9 CAP file sha256 khop 9/9; build XANH ca 2 app, 0 error TS.

=== O6 — bam card PE tren cay GIU NGUYEN man KHKK ===
+callback `onOpenPe` (usePipelineStages) mirror DOI XUNG `onOpenKhkk` cua leaf GD2.
KHONG truyen => giu `navigate('/purchase-evaluations?id=..&expand=1')` => 5 host cay con lai
0 doi hanh vi (da verify: chi KhkkListPage truyen).
Host: `?pe=<id>` render `PeDetailTabs readOnly mode="workspace"` ngay panel 2 — DUNG LAI
renderer that cua man PE thay vi dung ban rut gon thu hai (2 ban = 2 su that).
🔴 `pe` va `id` LOAI TRU NHAU (2 setter cung xoa key kia): panel 2 chi ve duoc mot thu, de
ca hai thi lan chon sau khong xoa duoc lan truoc.
`<lg` roi ve duong cu (khong co panel 2 thi xem-tai-cho la bat kha).
Panel 3 doi chu khi dang xem PE — cau cu "se hien khi chon phieu" la SAI (phieu DA chon roi).

=== O4 — shopdrawing / can cu ho so cho dinh kem file (FE-only) ===
🔴 0 BE MOI — rail da du: controller `[FromForm] Guid? dossierItemId` (:181) + DTO
`KhkkAttachmentDto.dossierItemId` (:241) co tu Mig 69. FE truoc day don gian la KHONG BAO GIO
gui truong do len => moi file roi ve muc PHIEU. Nay upload gui kem dossierItemId.
Chia 2 ro: file gan can cu hien ngay DUOI dong can cu do; Section 4 chi con file muc phieu —
khong chia thi file hien o CA HAI cho, nguoi dung dem 2 lan.
Dong-con file hien CA KHI read-only (nguoi duyet can xem shopdrawing, chi khong them/xoa).

=== O7 — nut edit tren card KHKK (man Thao tac) ===
But chi khuon `PeListPanel:208-232`. LUON hien roi MO DI khi khong sua duoc: nut chi-hien-khi-
hover thi tren may cam-ung khong ai tim ra; an han khi phieu khoa thi nguoi dung tuong chuc
nang khong ton tai thay vi hieu la phieu da di duyet.
🔴 Nut NGOAI the <button> cua card — long button trong button la HTML khong hop le.
Host: but chi => mo che-do SUA; than card => chi xem nhu cu. Guard readOnly Y HET
`KhkkListPage:147-150` (cung mot cau hoi "ai duoc sua" phai cho cung mot cau tra loi o moi man).
Da verify khong phai co-hoc: `KhkkDetailContent:161` `isDraft = draftPhase && !readOnly`.

=== O8 — cay 2 cap Du an > Hang muc ===
🔴 0 ENDPOINT MOI: `workItemName` DA co san trong `KhkkPeCandidateDto:423` => cap 2 la gom lai
tren du-lieu dang co. Truoc day 1 cap theo du an nen mot du an nhieu goi thau do ra danh sach
phang dai, khong thay ranh gioi hang muc.
🔴 2 muc `z` khac nhau (z-20 cha / z-10 con): cung z thi header hang muc truot DE len header
du an khi cuon, mat cap cha.

=== C3 — Field: value ve slate-900, label giu brand-600 ===
Truoc day MOI value deu to brand => mau thuong-hieu mat het suc nang: ca trang deu la mau nhan
thi khong con gi la nhan, va khong chua duoc mau cho thu THAT SU can noi (NCC trung thau,
canh bao vuot ngan sach). PE giu slate cho value dung vi ly do nay.

=== O3 — MOI LAND VE (2). VE (1) CHUA LAM — xem CON NO ===
🔴 KHONG cong gop 2 cot thanh mot so "hien hanh": de-xuat va duyet la 2 so co y nghia phap-ly
khac nhau (mot cai chua ai chot, mot cai da qua choke-point `ApplyApprovedValuesOnFinalize`);
tron lai la bia ra mot so khong ton tai trong ho-so nao.
Tong duyet chi cong phan DA co, va noi ro con bao nhieu dong chua chot.

=== 🔴 CON NO — O3 ve (1) "hang muc cho chon NHIEU" KHONG PHAI FE-ONLY ===
Spec ghi O3 = "N-ROW 0-migration". "0-migration" DUNG (unique 3 cot PlanId+SupplierId+
CatalogEntryId co tu Mig 71) NHUNG KHONG co nghia la 0 BE. Do that:
  · KHONG co endpoint TAO line. Chi co `PUT /{id}/lines/{lineId}` = gan catalogEntryId cho
    dong DA CO (Controller:121). Danh sach route day du: 0 route POST/DELETE line.
  · Line duoc auto-sinh 1-dong-moi-NCC-trung-thau luc TAO PHIEU (`Features.cs:508-527`),
    tien moi = tong bao gia DUOC CHON cua NCC do.
=> Muon 1 NCC co N hang muc phai TACH tien cua NCC do ra N phan — va KHONG CO quy tac nao
   trong he thong noi chia the nao. Do la cau hoi TIEN, chua ai tra loi.
🔸 Tin tot: BE cau HD da nhan N dong san (`CreateContractFromSigningPlanFeatures:101-115`
   nhan `LineIds` list, chi rao "moi HD 1 NCC"). Cai hep la FE dialog dung radio single-select
   (`KhkkDetailPage:310`) — do la UI, doi duoc.

=== CON NO KHAC ===
🔴 CHUA soi mat tren prod 375/1366/1440 (5 lane NO-SCREENSHOT, gotcha #3) — moi ket luan la
DOC MA, khong phai da nhin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:59:04 +07:00

446 lines
25 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-[260px_1fr] xl:grid-cols-[320px_1fr]` — ĐÚNG BYTE
// khuôn PE (`PurchaseEvaluationWorkspacePage.tsx:80`). 🧊 Comment cũ ghi `300px` là số
// của bản nháp đầu, KHÔNG bao giờ khớp code sau khi đợt A land 260/320 (S175 vá).
// · Panel 1 = danh sách (`KhkkWorkListPanel`) — 2 tầng, MỖI tầng có header dính ĐỈNH
// (`sticky top-0`) gom theo dự án. 🧊 Comment cũ ghi "nút hành-động dính đáy" là MÔ TẢ
// CỦA PE, không phải của đây: `grep -c Button KhkkWorkListPanel` = **0** (control-dương
// `PeListPanel` = 8). Bê nguyên câu mô tả từ khuôn nguồn sang là tự khai một thứ chưa
// dựng — S175 vá theo L3 H-5.
// · 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, Pencil, 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 { cn } from '@/lib/cn'
import { useAuth } from '@/contexts/AuthContext'
import {
KHKK_APPROVAL_GROUP_DEFAULT,
KHKK_APPROVAL_GROUP_LABELS,
KhkkPhase,
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)
// [S175 · O7] Bấm bút chì = mở thẳng chế-độ SỬA; bấm thân card = chỉ xem như cũ.
const [editPlan, setEditPlan] = useState(false)
const { user } = useAuth()
// 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={(id) => { setViewPlanId(id); setEditPlan(false) }}
selectedPeId={purchaseEvaluationId || null}
selectedPlanId={viewPlanId}
onEditPlan={(id) => { setViewPlanId(id); setEditPlan(true) }}
/>
<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 && (() => {
// [S175 · O7] `readOnly` = KHÔNG bấm bút chì, HOẶC phiếu không đủ điều-kiện sửa.
// 🔴 Guard y hệt `KhkkListPage.tsx:147-150` — cùng một câu hỏi "ai được sửa phiếu
// này" thì phải cho cùng một câu trả lời ở mọi màn; lệch nhau là đẻ lỗ hổng đúng
// kiểu gotcha #82 (một màn khoá, màn kia hở).
// 🔸 Đây là rào HIỂN THỊ. Rào THẬT vẫn ở BE (`KeHoachKyKet.Update` + phase guard).
const p = viewPlan.data
const canEdit = !!p
&& (p.phase === KhkkPhase.DangSoanThao || p.phase === KhkkPhase.TraLai)
&& (!user?.id || p.drafterUserId === user.id)
const readOnly = !editPlan || !canEdit
return (
<div className="space-y-3">
<div className={cn(
'flex flex-wrap items-center justify-between gap-2 rounded-lg border px-4 py-2',
readOnly ? 'border-violet-200 bg-violet-50' : 'border-emerald-300 bg-emerald-50',
)}>
<span className={cn('text-[12px] font-medium', readOnly ? 'text-violet-800' : 'text-emerald-800')}>
{readOnly
? (editPlan && !canEdit
? 'Kế hoạch này không sửa được (đã gửi duyệt / đã duyệt, hoặc bạn không phải người soạn) — chỉ xem.'
: 'Đang xem kế hoạch — chỉ xem. Bấm bút chì ở danh sách để sửa.')
: '✎ Đang SỬA kế hoạch — thay đổi lưu thẳng vào phiếu.'}
</span>
<div className="flex items-center gap-2">
{readOnly && canEdit && (
<Button variant="outline" size="sm" onClick={() => setEditPlan(true)}>
<Pencil className="mr-1.5 h-3.5 w-3.5" />
Sửa
</Button>
)}
{!readOnly && (
<Button variant="outline" size="sm" onClick={() => setEditPlan(false)}>
Xong
</Button>
)}
<Button variant="outline" size="sm" onClick={() => navigate(`/khkk/${viewPlanId}`)}>
Mở trang đy đ
</Button>
<Button variant="ghost" size="sm" onClick={() => { setViewPlanId(null); setEditPlan(false) }}>
<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={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>
)
}