[CLAUDE] Contract: UAT S174 vong-3 — 5 chinh KHKK theo anh chot (FE-only, 0-mig)

1. Ho thu: GO `PipelineTreePanel` khoi `/inbox` — anh: "cho hop thu vut panel nay vao lam j,
   tap trung vao KHKK thoi". Ho thu = hang-doi-viec-cua-toi, cay toan trinh = ban-do-toan-he
   => sai the loai. Cho trong nay tra ve dung vai: cho quy trinh duyet cua HD dang chon.
   🔸 HAI trang GD3/GD4 con lai (`MyContractsPage` · `HardCopiesPage`) GIU NGUYEN — anh chi
   dich danh Ho thu, khong noi go het; khong tu suy rong. (Lineage: panel vao day @S168
   GD3-D theo owner slot (63) + "mo rong = ca 3 trang"; nay owner thu lai DUNG trang nay.)

2. Panel KHKK: BO nut "+ Them moi". Man nay KHONG co duong "tao tu so 0" — ke hoach LUON
   sinh tu mot phieu Duyet NCC, ma cac phieu du dieu kien da nam nguyen o tang tren => nut
   chi xoa form chu khong tao them duoc gi. Khac CO CHU DICH voi `PeListPanel`.

3. Tang duoi CHI giu ke hoach DANG XU LY — an phieu `DaDuyet` (anh: "da duyet bo di luon").
   🔴 KHAI GIOI-HAN: loc o FE vi endpoint chi nhan `phase` DON, khong co tham so "loai tru"
   => (a) KHONG dung `total` cua server lam so hien thi (da gom phieu DaDuyet) — dem tu mang
   da loc; (b) cua-so 30 phieu moi nhat co the TOAN DaDuyet => tang duoi rong du con phieu
   nhap cu hon. Chap nhan o UAT; muon chuan thi BE them tham so loai-tru.
   Doi luon dong chu "x/total" -> "Da an ke hoach Da duyet": ti so cu doc thanh "dang cat
   bot" trong khi that ra la "dang loc bo".

4. Nhom duyet + Quy trinh duyet GAN CUNG theo leaf menu — anh: "gan cung luon theo tung nhom
   ko cho User dieu chinh cho nay luon".
   - `approvalGroup` khong con la state, DAN XUAT thang tu `?group=n`. Bo luon khoi dong-bo
     `lastPresetGroup` + ham `firstFreeGroup()` (chung chi ton tai de tu nhay nhom = dung
     hanh-vi vua bi cam).
   - Quy trinh: match theo ma tu NGUON BE, khong suy tu man hinh — `DbInitializer.cs:609`
     `var code = $"KHKK-N{n}"`. Khong khop => CHAN tao + noi ro thieu ma nao, TUYET DOI
     khong tu roi ve quy trinh nhom khac (gan nham = phieu di sai duong duyet).
   - UI: hien gia-tri co dinh, CO Y khong dung `<Select disabled>` (o xam van trong nhu
     "bam duoc nhung dang khoa" va van trong tab-order).

5. Panel giua: GO Section 5 "Y kien cap duyet" (anh: "bo luon cai nay ra luon"). So-do
   Buoc/Cap + chu ky da song o Panel 3 => de ca hai la ke mot chuyen hai lan tren cung man.
   DTO giu nguyen (`levelOpinions`/`workflowSteps` panel 3 van dung) — chi bo cho VE.

Verify: build 2 app PASS; mirror SHA256 3/3 cap KHOP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-08-05 15:50:17 +07:00
parent b7c6ee5a66
commit f86f30d291
7 changed files with 207 additions and 416 deletions

View File

@ -21,7 +21,7 @@
import { useState, type ReactNode } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Check, ClipboardList, FileCheck, MessageSquare, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
ClipboardList, FileCheck, Paperclip, Pencil, Plus, Save, Trash2, Upload, Users,
} from 'lucide-react'
import { toast } from 'sonner'
import { PePipelineStrip } from '@/components/pe/PePipelineStrip'
@ -255,9 +255,10 @@ export function KhkkDetailContent({
// File "đính kèm khi duyệt" thuộc panel 3 (mục 5) — Section 4 chỉ liệt kê hồ-sơ của phiếu.
const planAttachments = plan.attachments.filter(a => a.purpose !== KhkkAttachmentPurpose.ApprovalAttachment)
// ── Section 5: ý-kiến cấp duyệt, gom theo BƯỚC (Phòng) → CẤP → NV ──────────────────
const opinions = plan.levelOpinions ?? []
const steps = plan.workflowSteps ?? []
// 🧊 [S174 owner] Section 5kiến cấp duyệt" ĐÃ GỠ khỏi panel giữa — anh: "bỏ luôn cái
// này ra luôn". Sơ-đồ Bước/Cấp + chữ ký đã sống ở PANEL 3 (`KhkkWorkflowPanel`), để cả hai
// là kể một chuyện hai lần trên cùng màn hình. Dữ-liệu `plan.levelOpinions` /
// `plan.workflowSteps` VẪN có trong DTO (panel 3 dùng) — chỉ bỏ chỗ VẼ ở đây.
return (
<div className="card-accent overflow-hidden" style={{ ['--accent' as string]: 'var(--color-brand-500)' }}>
@ -569,112 +570,6 @@ export function KhkkDetailContent({
</ul>
)}
</Section>
{/* ─────────────── 5 — Ảnh 3 khoanh đỏ ───────────────
Gom theo BƯỚC (kèm PHÒNG) → lưới 2 cột các ô ý-kiến của từng CẤP, đúng khuôn
Section "Ý kiến cấp duyệt" V2 của Duyệt NCC (`PeDetailTabs.tsx:705-774`):
forEach Bước → forEach Cấp → 1 ô. Ô chưa ký vẫn HIỆN (chỗ trống có tên người) —
vắng ô là người đọc không biết còn thiếu chữ ký của ai. */}
<Section
title="5. Ý kiến cấp duyệt"
icon={<MessageSquare className="h-4 w-4" />}
accent="var(--color-violet-500)"
head="text-violet-700"
chipBg="var(--color-violet-50)"
chipFg="var(--color-violet-700)"
count={opinions.length}
>
{steps.length === 0 ? (
<div className="text-sm text-muted-foreground">
Phiếu chưa pin quy trình duyệt chưa cấp nào đ ghi ý kiến.
</div>
) : (
<div className="space-y-3">
{steps.map((step) => {
const signedCount = step.levels.filter(lv =>
opinions.some(o => o.approvalWorkflowLevelId === lv.id)).length
return (
<div key={step.id} className="rounded-lg border border-slate-200 bg-slate-50/40">
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 bg-slate-50/80 px-3 py-2">
<span className="text-[12px] font-bold uppercase tracking-wide text-slate-700">
Bước {step.order} {step.name}
</span>
{step.departmentName && (
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
{step.departmentName}
</span>
)}
<span
className="ml-auto text-[10px] text-slate-500"
title={`Tổng ${step.levels.length} NV tham gia (mỗi cấp chỉ cần 1 NV ký — OR-of-N)`}
>
{signedCount}/{step.levels.length} cấp đã duyệt · {step.levels.length} NV tham gia
</span>
</div>
<div className="p-3">
{step.levels.length === 0 ? (
<div className="text-[12px] italic text-slate-400"> Bước này chưa cấu hình cấp duyệt.</div>
) : (
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{step.levels.map((lv) => {
const signed = opinions.find(o => o.approvalWorkflowLevelId === lv.id)
return (
<div
key={lv.id}
className={cn(
'rounded-lg border bg-white p-3',
signed ? 'border-emerald-200' : 'border-dashed border-slate-200',
)}
>
<div className="mb-2 flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<h4 className="text-[13px] font-semibold text-slate-700">
{lv.name || `Cấp ${lv.order}`} {' '}
<span className="text-slate-900">{lv.approverFullName ?? '(chưa cấu hình)'}</span>
</h4>
{/* Không dùng `signed!` — thu hẹp bằng chính `signed &&` để
khỏi mượn null-forgiving trên dữ-liệu từ API (gotcha #80). */}
{signed && signed.approverUserId
&& signed.approverUserId !== signed.signedByUserId && (
<div className="mt-1 inline-flex items-center gap-1 rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700">
Admin <strong>{signed.signedByFullName}</strong> duyệt thay
</div>
)}
</div>
{signed ? (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
<Check className="h-3 w-3" /> Đã duyệt
</span>
) : (
<span className="inline-flex shrink-0 items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-500">
Chưa
</span>
)}
</div>
{signed ? (
<>
<div className="whitespace-pre-wrap text-sm text-slate-800">
{signed.comment?.trim() || '(duyệt — không ý kiến)'}
</div>
<div className="mt-2 border-t border-slate-100 pt-1.5 text-[11px] text-slate-500">
{formatDateTime(signed.signedAt)}
</div>
</>
) : (
<div className="text-sm italic text-slate-400">Chưa ý kiến duyệt.</div>
)}
</div>
)
})}
</div>
)}
</div>
</div>
)
})}
</div>
)}
</Section>
</div>
{/* Dialog thêm/sửa căn cứ */}

View File

@ -31,6 +31,7 @@ import { cn } from '@/lib/cn'
import {
KHKK_PHASE_BADGE,
KHKK_PHASE_LABELS,
KhkkPhase,
type KhkkListItemDto,
type KhkkPeCandidateDto,
type PagedResult,
@ -78,7 +79,6 @@ export function KhkkWorkListPanel({
onPickPlan,
selectedPeId,
selectedPlanId,
footer,
}: {
/** Bấm PE ở tầng trên → host nạp phiếu đó vào form tạo (Ở LẠI màn, không điều hướng). */
onPickPe: (peId: string) => void
@ -87,8 +87,6 @@ export function KhkkWorkListPanel({
onPickPlan: (planId: string) => void
selectedPeId?: string | null
selectedPlanId?: string | null
/** Nút "+ Thêm mới" dính đáy — mirror `PeListPanel` của màn Duyệt NCC — Thao tác. */
footer?: React.ReactNode
}) {
const { can } = usePermission()
// 🔴 ĐÚNG key policy của endpoint — KHÔNG dùng `Khkk_*` (đó là key MENU, tầng hiển thị).
@ -126,7 +124,14 @@ export function KhkkWorkListPanel({
else byProject.set(key, [pe])
}
const planItems = plans.data?.items ?? []
// [S174 owner] "đã duyệt bỏ đi luôn" — tầng dưới chỉ giữ kế hoạch CHƯA xong việc
// (nháp / chờ duyệt / trả lại / từ chối). Phiếu `DaDuyet` đã hết việc ở màn Thao tác.
// 🔴 KHAI GIỚI-HẠN (lọc ở FE): endpoint chỉ nhận `phase` ĐƠN, không có tham số "loại trừ"
// ⇒ (1) `plans.data.total` của server ĐÃ GỒM phiếu DaDuyet nên KHÔNG được dùng làm số
// hiển thị — đếm từ chính mảng đã lọc; (2) cửa-sổ 30 phiếu mới nhất có thể TOÀN DaDuyet,
// khi đó tầng dưới rỗng dù còn phiếu nháp cũ hơn. Chấp nhận ở UAT; muốn chuẩn thì BE thêm
// tham số loại-trừ rồi lọc SERVER-SIDE.
const planItems = (plans.data?.items ?? []).filter((k) => k.phase !== KhkkPhase.DaDuyet)
return (
<aside className="flex min-h-0 flex-col overflow-hidden border-r border-slate-200 bg-white">
@ -209,8 +214,9 @@ export function KhkkWorkListPanel({
<div className="shrink-0 border-t-2 border-slate-200" />
<Tier
title="Kế hoạch ký kết đã tạo"
count={plans.data?.total ?? null}
title="Kế hoạch ký kết đang xử lý"
// Đếm từ mảng ĐÃ LỌC, KHÔNG lấy `total` của server (xem khối lọc phía trên).
count={plans.data ? planItems.length : null}
accent="bg-violet-50 text-violet-600"
icon={<FileSignature className="h-3 w-3" />}
>
@ -228,7 +234,10 @@ export function KhkkWorkListPanel({
</div>
)}
{!plans.isLoading && !plans.isError && planItems.length === 0 && (
<p className="p-3 text-[11px] italic text-slate-400">Chưa kế hoạch kết nào.</p>
<p className="p-3 text-[11px] italic text-slate-400">
Không còn kế hoạch nào đang xử . Phiếu đã duyệt không hiện đây xem tại
menu Kế hoạch kết .
</p>
)}
<ul className="divide-y divide-slate-100">
{planItems.map((k) => (
@ -267,19 +276,23 @@ export function KhkkWorkListPanel({
</li>
))}
</ul>
{plans.data && plans.data.total > planItems.length && (
{/* 🔴 KHÔNG in "x/total" nữa: `total` gồm cả phiếu đã duyệt vừa bị lọc ⇒ tỉ số đó
sẽ đọc thành "đang cắt bớt" trong khi thật ra là "đang lọc bỏ". Nói đúng việc
đang làm, và chỉ nói khi thật sự có cái bị giấu. */}
{plans.data && plans.data.items.length > planItems.length && (
<p className="border-t border-slate-100 px-3 py-1.5 text-[10px] italic text-slate-400">
Hiện {planItems.length}/{plans.data.total} kế hoạch mới nhất xem đ menu Kế hoạch kết .
Đã n kế hoạch trạng thái Đã duyệt xem đ menu Kế hoạch kết .
</p>
)}
</Tier>
</>
)}
{/* Nút hành-động dính ĐÁY, ngoài vùng cuộn của 2 tầng — mirror "+ Thêm mới" của
`PeListPanel` (màn Duyệt NCC — Thao tác). Hiện KỂ CẢ khi thiếu quyền KHKK: cột
không dựng được thì vẫn phải còn đường bấm, không thì màn thành ngõ cụt. */}
{footer && <div className="shrink-0 border-t border-slate-200 p-2">{footer}</div>}
{/* 🧊 [S174] KHÔNG có nút "+ Thêm mới" ở đáy — anh chốt bỏ. Lý do đứng vững: màn này
KHÔNG có đường "tạo từ số 0". Một kế hoạch LUÔN sinh ra từ một phiếu Duyệt NCC, mà
các phiếu đủ điều kiện đã nằm nguyên ở tầng trên ⇒ nút chỉ xoá form chứ không tạo
thêm được gì. Đây là chỗ khác CÓ CHỦ ĐÍCH với `PeListPanel` (bên đó tạo phiếu PE
mới là việc có thật). Ai định thêm lại: phải trả lời được "thêm mới CÁI GÌ". */}
</aside>
)
}

View File

@ -23,19 +23,17 @@
import { useState, type FormEvent, type ReactNode } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { FileCheck, Plus, X } from 'lucide-react'
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 { Select } from '@/components/ui/Select'
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_GROUPS,
KHKK_APPROVAL_GROUP_DEFAULT,
KHKK_APPROVAL_GROUP_LABELS,
khkkGroupMenuLabel,
@ -55,11 +53,9 @@ interface WorkflowDto {
isUserSelectable: boolean
}
// Nhóm trống ĐẦU TIÊN của một PE (null = đã dùng hết 8 nhóm)để ô chọn nhóm không đứng
// yên ở một nhóm chắc chắn 409.
function firstFreeGroup(taken: number[]): number | null {
return KHKK_APPROVAL_GROUPS.find((g) => !taken.includes(g)) ?? null
}
// 🧊 [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()
@ -69,21 +65,17 @@ export function KhkkCreatePage() {
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
const [approvalWorkflowId, setApprovalWorkflowId] = useState('')
const [approvalGroup, setApprovalGroup] = useState<number>(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)
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)
// Đổi leaf nhóm ⇒ đồng bộ ô chọn nhóm. CHỈ đổi khi CHÍNH THAM SỐ URL đổi — so với
// `approvalGroup` thì người dùng tự chọn nhóm khác sẽ bị kéo ngược về nhóm của URL.
const [lastPresetGroup, setLastPresetGroup] = useState(presetGroup)
if (presetGroup !== lastPresetGroup) {
setLastPresetGroup(presetGroup)
if (presetGroup !== null) setApprovalGroup(presetGroup)
}
// 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'],
@ -103,6 +95,16 @@ export function KhkkCreatePage() {
},
})
// [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],
@ -113,10 +115,10 @@ export function KhkkCreatePage() {
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')
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,
approvalWorkflowId: autoWorkflow.id,
ghiChu: ghiChu.trim() || null,
approvalGroup, // [K2] gửi TƯỜNG MINH, không dựa default ngầm của BE
}
@ -127,7 +129,6 @@ export function KhkkCreatePage() {
onSuccess: (data) => {
toast.success(data.maKeHoach ? `Đã lập kế hoạch ${data.maKeHoach}` : 'Đã lập kế hoạch ký kết')
setPurchaseEvaluationId('')
setApprovalWorkflowId('')
setGhiChu('')
setViewPlanId(data.id)
},
@ -141,21 +142,17 @@ export function KhkkCreatePage() {
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)`,
!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 && !create.isPending
const canSubmit = missingCreate.length === 0 && !!autoWorkflow && !create.isPending
// Bấm PE ở panel 1: nạp vào form + rời chế-độ xem. 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ò.
// [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 taken = list.find((p) => p.id === peId)?.existingGroups ?? []
if (taken.includes(approvalGroup)) {
const free = firstFreeGroup(taken)
if (free !== null) setApprovalGroup(free)
}
}
const onSubmit = (e: FormEvent) => {
@ -185,22 +182,6 @@ export function KhkkCreatePage() {
onPickPlan={setViewPlanId}
selectedPeId={purchaseEvaluationId || null}
selectedPlanId={viewPlanId}
footer={
<Button
type="button"
className="w-full"
// "Thêm mới" = trả panel 2 về form TRẮNG. Không điều hướng đi đâu.
onClick={() => {
setViewPlanId(null)
setPurchaseEvaluationId('')
setApprovalWorkflowId('')
setGhiChu('')
}}
>
<Plus className="mr-2 h-4 w-4" />
Thêm mới
</Button>
}
/>
<main className="hidden overflow-y-auto bg-slate-50 p-6 lg:block">
@ -294,55 +275,59 @@ export function KhkkCreatePage() {
</div>
</Section>
<Section title="2. Nhóm duyệt & quy trình">
{/* [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 htmlFor="approvalGroup" className="text-[11px]">Nhóm duyệt (N1N8) *</Label>
<Select
id="approvalGroup"
value={approvalGroup}
onChange={(e) => setApprovalGroup(Number(e.target.value))}
>
{KHKK_APPROVAL_GROUPS.map((g) => {
const used = takenGroups.includes(g)
return (
<option key={g} value={g} disabled={used}>
N{g} - {KHKK_APPROVAL_GROUP_LABELS[g] ?? `Nhóm ${g}`}
{used ? ' (đã có kế hoạch)' : ''}
</option>
)
})}
</Select>
<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 nhóm khác.
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">
Mỗi phiếu Duyệt NCC lập đưc 1 kế hoạch cho MỖI nhóm duyệt
Lấy theo mục đang mở menu đi nhóm bằng ch sang mục khác
</p>
)}
</div>
<div>
<Label htmlFor="workflow" className="text-[11px]">Quy trình duyệt *</Label>
<Select
id="workflow"
value={approvalWorkflowId}
onChange={(e) => setApprovalWorkflowId(e.target.value)}
>
<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>
{workflows.data && workflows.data.length === 0 ? (
<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 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.
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">
Chỉ hiển thị quy trình admin đã ghim cho user chọn
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>
@ -380,7 +365,7 @@ export function KhkkCreatePage() {
type="button"
variant="ghost"
className="text-xs"
onClick={() => { setPurchaseEvaluationId(''); setApprovalWorkflowId(''); setGhiChu('') }}
onClick={() => { setPurchaseEvaluationId(''); setGhiChu('') }}
>
Hủy
</Button>