- {/* 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 && (
-
- {signed.comment?.trim() || '(duyệt — không ý kiến)'}
-
-
- {formatDateTime(signed.signedAt)}
-
- >
- ) : (
-
Chưa có ý kiến duyệt.
- )}
-
- )
- })}
-
- )}
-
-
- )
- })}
-
- )}
-
{/* Dialog thêm/sửa căn cứ */}
diff --git a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx
index d918fdf..e3a4c8f 100644
--- a/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx
+++ b/fe-admin/src/components/khkk/KhkkWorkListPanel.tsx
@@ -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 (
)
}
diff --git a/fe-admin/src/pages/khkk/KhkkCreatePage.tsx b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx
index 7b04bfd..b893b22 100644
--- a/fe-admin/src/pages/khkk/KhkkCreatePage.tsx
+++ b/fe-admin/src/pages/khkk/KhkkCreatePage.tsx
@@ -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(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(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={
-
- }
/>
@@ -294,55 +275,59 @@ export function KhkkCreatePage() {
-
+ {/* [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 `