[CLAUDE] Contract: K4a+K4b SP-002 sidebar 8 nhom x 6 leaf + noi ?group= end-to-end (0-mig)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m23s
K4a (gate PASS-WITH-FLAGS 4, sub-reviewer-gate-k4a.md): 49 key Khkk_G{2-8} bo-sinh
by-construction 3-phia (All/seed/grant) MenuKeys.cs + DbInitializer 49 row + labelBackfill
G1 nhan A1 + site-2 Concat + va 7/10/11/13/15 (nhan N2 co "pha do"; comment 1893 -> mo-neo
grep). All 64->113, Policies 256->452 (test T2 khoa runtime). Test +7 KhkkGroupMenuSeedTests
(T7 revoker-khong-dung-Khkk: probe vi-phan 112->0, gotcha #84).
K4b (gate PASS-WITH-FLAGS 4M/5m 0-chan, sub-reviewer-gate-k4b.md): KhkkListPage x2 derive
phase+group tu searchParams (leaf->leaf khong F5, navKey reset) + queryKey {group,phase,
search,page,deletedView} + KhkkCreatePage preset ?group= + staticMap G1 6-leaf group=1 +
regex Khkk_G([1-8])_* fe-user (fe-admin GHI RANG BUOC isAdminHidden) + FLAG-1 lead-fix:
GET /deleted +group mirror List (Controller+Query+handler SAU-IDOR) + FE noi 2-nhanh.
2 wave 1 commit CO KHAI: Layout.tsx dinh ca 2 wave khong tach hunk phi-tuong-tac; moi dong
deu qua gate rieng. STATUS canonical 5-row dong bo (71/97/452/113/620->621) + bundle :479.
No K4c: MAJOR-A test /deleted+group filter + MAJOR-C WfView doc group. Suite 621/0
(45D+576I) + filter 41/41 + npm x2 + dotnet 0E.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -32,12 +32,17 @@ function resolvePath(key: string): string | null {
|
||||
// [W2 KHKK S161] GĐ2 hết skeleton — re-point trang thật (gotcha #50: thiếu route =
|
||||
// sidebar drop SILENT). WfView phía admin trỏ Designer type-10 (fe-admin CỐ Ý không có
|
||||
// matrix page — chốt 2026-07-27; admin "Luồng duyệt" = dựng/sửa quy trình).
|
||||
// [K4b S166 — spec K4b-7] 5 leaf trỏ `/khkk/*` mang `group=1` tường minh, mirror
|
||||
// fe-user (trang coi "vắng group" = xem tất cả, nên thiếu ở đây là nhóm 1 nói dối).
|
||||
// 🔴 `Khkk_WfView` CỐ Ý KHÔNG mang `group`: bên admin nó trỏ Designer quy trình,
|
||||
// không phải trang danh sách — Designer không đọc tham số này (fe-user mới đi
|
||||
// `/khkk/workflow-matrix?type=10&group=1`). Lệch có chủ đích, không mirror mù.
|
||||
Khkk_WfView: '/system/approval-workflows-v2/ContractSigningPlan',
|
||||
Khkk_List: '/khkk/list',
|
||||
Khkk_Create: '/khkk/create',
|
||||
Khkk_Pending: '/khkk/list?filter=ChoDuyet',
|
||||
Khkk_Approved: '/khkk/list?filter=DaDuyet',
|
||||
Khkk_Deleted: '/khkk/list?view=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
|
||||
Khkk_List: '/khkk/list?group=1',
|
||||
Khkk_Create: '/khkk/create?group=1',
|
||||
Khkk_Pending: '/khkk/list?group=1&filter=ChoDuyet',
|
||||
Khkk_Approved: '/khkk/list?group=1&filter=DaDuyet',
|
||||
Khkk_Deleted: '/khkk/list?group=1&view=deleted', // [S159-đợt5 FLAG-1] thiếu là leaf drop SILENT (gotcha #50)
|
||||
// [W7 KHKK S161] GĐ4 hết skeleton — 7 leaf `Hdc_*` re-point trang thật
|
||||
// `/hard-copies?type=N` (N = ContractType int, mirror TYPE_CODE_TO_INT ở trên).
|
||||
// gotcha #50: thiếu/để sai route ở đây = leaf DROP IM LẶNG khỏi sidebar.
|
||||
@ -191,6 +196,19 @@ function resolvePath(key: string): string | null {
|
||||
// [S57] BỎ ẩn "Danh mục" (Master/Catalogs + Cấu hình HRM re-parent vào đây) — gom
|
||||
// master data về 1 chỗ cho CẢ admin lẫn nhân viên (đảo S29 hide). Admin nay quản
|
||||
// master trực tiếp trên fe-admin; write vẫn khóa Admin/CatalogManager ở BE controller.
|
||||
// 🔴 [K4a — S166, vá-8] RÀNG BUỘC NGƯỢC với `resolvePath` bên trên — đọc trước khi
|
||||
// sửa bộ lọc này. fe-user có nhánh regex `^Khkk_G([1-8])_(WfView|List|Create|Pending|
|
||||
// Approved|Deleted)$`; fe-admin CỐ Ý KHÔNG có (admin không có `WorkflowMatrixViewPage`
|
||||
// — lead chốt 2026-07-27, giữ lệch). Hệ quả: 42 leaf `Khkk_G{2..8}_*` mà BE seed sẽ
|
||||
// rơi vào `resolvePath → null` ⇒ MenuLeaf trả null ⇒ DROP IM LẶNG (gotcha #50).
|
||||
// ⚠️ ĐO ĐĨA 2026-08-01: hàm này CHỈ ẩn `Ct_*` — `KeHoachKyKet` KHÔNG hề nằm trong tập
|
||||
// ẩn, tức ràng buộc trên KHÔNG phải chuyện tương lai mà ĐANG hiệu lực: sidebar admin
|
||||
// hiện root KHKK, 7 nhóm con, và 42 leaf của nhóm 2..8 im lặng biến mất (nhóm 1 sống
|
||||
// nhờ 6 dòng `Khkk_*` không-infix trong `staticMap`). Đây là trạng thái ĐÃ KHAI, chấp
|
||||
// nhận có chủ đích ở lượt K4a — KHÔNG phải bug mới phát sinh.
|
||||
// ⇒ Ai muốn admin thấy đủ 8 nhóm (hoặc ngược lại, thêm `KeHoachKyKet` vào tập ẩn rồi
|
||||
// sau đó gỡ ra) thì PHẢI nới regex `Khkk_G*` trong `resolvePath` TRƯỚC — và nới thì
|
||||
// phải quyết luôn `WfView` bên admin trỏ đâu, vì Designer không nhận `?group=`.
|
||||
function isAdminHidden(key: string): boolean {
|
||||
return key.startsWith('Ct_')
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
// 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'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { CheckCircle2, FileCheck, FileText, Layers, Plus, Save, Settings2, X } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { PageHeader } from '@/components/ui/PageHeader'
|
||||
@ -57,11 +57,31 @@ function firstFreeGroup(taken: number[]): number | null {
|
||||
|
||||
export function KhkkCreatePage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
// [K4b S166 — vá-6] Vào từ leaf "Thao tác" của một nhóm (`/khkk/create?group={n}`)
|
||||
// hoặc từ nút "Lập kế hoạch" trên màn danh sách đang lọc nhóm ⇒ ô chọn nhóm mở sẵn
|
||||
// đúng nhóm đó. Không có tham số (vào thẳng /khkk/create) thì giữ nguyên default cũ.
|
||||
// Chỉ nhận 1..8, rác coi như không có — ô chọn không được phép mang giá trị ngoài dải.
|
||||
const groupParam = searchParams.get('group')
|
||||
const presetGroup = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
|
||||
|
||||
const [purchaseEvaluationId, setPurchaseEvaluationId] = useState('')
|
||||
const [approvalWorkflowId, setApprovalWorkflowId] = useState('')
|
||||
const [approvalGroup, setApprovalGroup] = useState<number>(KHKK_APPROVAL_GROUP_DEFAULT)
|
||||
const [approvalGroup, setApprovalGroup] = useState<number>(presetGroup ?? KHKK_APPROVAL_GROUP_DEFAULT)
|
||||
const [ghiChu, setGhiChu] = useState('')
|
||||
|
||||
// Cùng lý do với `KhkkListPage` (vá-5): 8 leaf "Thao tác" dùng chung route
|
||||
// `/khkk/create`, nhảy leaf→leaf không dựng lại component nên giá trị khởi tạo ở
|
||||
// trên KHÔNG chạy lại. Đồng bộ lại ngay trong render khi tham số nhóm đổi.
|
||||
// 🔴 Chỉ đổi khi CHÍNH THAM SỐ URL đổi — nếu 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, không bấm đi đâu được.
|
||||
const [lastPresetGroup, setLastPresetGroup] = useState(presetGroup)
|
||||
if (presetGroup !== lastPresetGroup) {
|
||||
setLastPresetGroup(presetGroup)
|
||||
if (presetGroup !== null) setApprovalGroup(presetGroup)
|
||||
}
|
||||
|
||||
// Phiếu PE đủ điều kiện: Phase=DaDuyet VÀ có ≥1 NCC trúng thầu (BE lọc — khớp
|
||||
// ĐÚNG 2 rào còn lại của Create nên picker không mời bấm vào thứ chắc chắn 409).
|
||||
// [K2 S164] Vị ngữ "chưa có kế hoạch sống" ĐÃ BỎ (`Features.cs:1291-1295`).
|
||||
|
||||
@ -3,13 +3,20 @@
|
||||
// KpiCard làm bộ lọc + bảng + phân trang). File MIRROR SHA256 identical với
|
||||
// fe-admin counterpart.
|
||||
//
|
||||
// URL vào từ menu (Layout.tsx staticMap — chỗ thứ 4 của Pattern 16-bis):
|
||||
// Khkk_List → /khkk/list
|
||||
// Khkk_Pending → /khkk/list?filter=ChoDuyet
|
||||
// Khkk_Approved → /khkk/list?filter=DaDuyet
|
||||
// Khkk_Deleted → /khkk/list?view=deleted
|
||||
// URL vào từ menu (Layout.tsx — staticMap cho nhóm 1, regex `Khkk_G{n}_*` cho nhóm
|
||||
// 2..8; đây là "chỗ thứ 4" của Pattern 16-bis):
|
||||
// Khkk_List / Khkk_G{n}_List → /khkk/list?group={n}
|
||||
// Khkk_Pending / Khkk_G{n}_Pending → /khkk/list?group={n}&filter=ChoDuyet
|
||||
// Khkk_Approved / Khkk_G{n}_Approved → /khkk/list?group={n}&filter=DaDuyet
|
||||
// Khkk_Deleted / Khkk_G{n}_Deleted → /khkk/list?group={n}&view=deleted
|
||||
// 🔴 Chip lọc chỉ đổi STATE, KHÔNG ghi lại URL — giữ nguyên URL của leaf đang đứng
|
||||
// nên menu không sáng nhầm (bug UAT S155 khi URL leaf trùng URL user tự lọc).
|
||||
//
|
||||
// [K4b S166] 🔴 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ông chụp một lần lúc mount: đi từ leaf nhóm 2 sang leaf nhóm 5 là đổi
|
||||
// query chứ không đổi route, React Router không dựng lại component nên `useState(...)`
|
||||
// khởi tạo một lần sẽ giữ nguyên bộ lọc cũ (trước K4b phải F5 mới đúng — vá-5).
|
||||
// Bộ lọc người dùng bấm được giữ ở lớp override và tự nhả ra khi URL đổi (xem dưới).
|
||||
import { useMemo, useState, type ReactNode } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
@ -29,6 +36,7 @@ import {
|
||||
KHKK_PHASE_BADGE,
|
||||
KHKK_PHASE_LABELS,
|
||||
KhkkPhase,
|
||||
khkkGroupMenuLabel,
|
||||
type KhkkListItemDto,
|
||||
type KhkkPhaseValue,
|
||||
type PagedResult,
|
||||
@ -58,17 +66,50 @@ function formatDate(iso: string): string {
|
||||
export function KhkkListPage() {
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
const initialFilter = searchParams.get('filter')
|
||||
const deletedView = searchParams.get('view') === 'deleted'
|
||||
|
||||
const [phase, setPhase] = useState<KhkkPhaseValue | null>(
|
||||
initialFilter ? (FILTER_TO_PHASE[initialFilter] ?? null) : null,
|
||||
)
|
||||
// ---- [K4b S166] 3 trục DANH TÍNH đọc thẳng từ URL, KHÔNG chụp một lần lúc mount ----
|
||||
// Cả 8 nhóm dùng CHUNG một route `/khkk/list`; bấm leaf này sang leaf kia chỉ đổi
|
||||
// query, React Router KHÔNG remount ⇒ giá trị khởi tạo của `useState` không chạy lại.
|
||||
// Đó là lý do trước K4b bấm leaf→leaf phải F5 mới thấy đúng (vá-5).
|
||||
const urlFilter = searchParams.get('filter')
|
||||
const urlPhase: KhkkPhaseValue | null = urlFilter ? (FILTER_TO_PHASE[urlFilter] ?? null) : null
|
||||
const deletedView = searchParams.get('view') === 'deleted'
|
||||
// `?group=` — chỉ nhận 1..8; rác/thiếu ⇒ null = XEM TẤT CẢ NHÓM (không tự đoán nhóm 1,
|
||||
// vì "vắng tham số" và "nhóm 1" là hai ý khác nhau; leaf nhóm 1 nay mang group=1 tường minh).
|
||||
const groupParam = searchParams.get('group')
|
||||
const group = groupParam !== null && /^[1-8]$/.test(groupParam) ? Number(groupParam) : null
|
||||
|
||||
// ---- Bộ lọc do NGƯỜI DÙNG bấm (6 chip KpiCard) đè lên bộ lọc của URL ----
|
||||
// 🔴 3 trạng thái, KHÔNG gộp được: `undefined` = chưa bấm chip ⇒ nghe theo URL;
|
||||
// `null` = user chủ động chọn "Tất cả"; số = user chọn một trạng thái.
|
||||
// Gộp `undefined` với `null` thì chọn "Tất cả" sẽ bị URL kéo ngược về filter cũ.
|
||||
const [chipPhase, setChipPhase] = useState<KhkkPhaseValue | null | undefined>(undefined)
|
||||
const [search, setSearch] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// Đổi leaf ⇒ trả bộ lọc chip về cho URL và về trang 1 (đang đứng trang 5 của nhóm 2
|
||||
// mà nhảy sang nhóm 5 thì trang 5 gần như chắc chắn rỗng). Điều chỉnh state NGAY
|
||||
// TRONG RENDER theo khuôn React "reset state khi input đổi" — dùng `useEffect` sẽ
|
||||
// vẽ một lượt bằng dữ liệu cũ rồi mới sửa (nháy). `search` CỐ Ý giữ lại: đang tìm
|
||||
// một mã rồi đổi nhóm để dò tiếp là thao tác có thật.
|
||||
const navKey = `${group ?? ''}|${urlFilter ?? ''}|${deletedView ? 'del' : ''}`
|
||||
const [lastNavKey, setLastNavKey] = useState(navKey)
|
||||
if (navKey !== lastNavKey) {
|
||||
setLastNavKey(navKey)
|
||||
setChipPhase(undefined)
|
||||
setPage(1)
|
||||
}
|
||||
const phase: KhkkPhaseValue | null = chipPhase !== undefined ? chipPhase : urlPhase
|
||||
|
||||
const list = useQuery({
|
||||
queryKey: ['khkk-list', { phase, search, page, deletedView }],
|
||||
// 🔴 [vá-3] `group` PHẢI nằm trong khoá cache. Thiếu nó thì 8 leaf "Danh sách" của
|
||||
// 8 nhóm dùng CHUNG một ô cache ⇒ mở nhóm 5 lại thấy dữ liệu nhóm 2 trong khoảnh
|
||||
// khắc trước khi refetch, và tệ hơn là không refetch nếu còn fresh (sai CÂM, không
|
||||
// có lỗi nào nổ). Trục `?filter=` đã nằm sẵn trong khoá dưới dạng ĐÃ GIẢI MÃ là
|
||||
// `phase` (`FILTER_TO_PHASE` đơn ánh, và `phase` cũng chính là thứ gửi lên server)
|
||||
// ⇒ khoá này phủ đủ cả hai trục vá-3 nhắc; thêm `filter` thô nữa chỉ tách đôi ô
|
||||
// cache cho hai request y hệt nhau.
|
||||
queryKey: ['khkk-list', { group, phase, search, page, deletedView }],
|
||||
queryFn: async () => {
|
||||
// Phiếu xóa mềm bị HasQueryFilter che ở list thường — chỉ endpoint riêng
|
||||
// `/deleted` thấy (mirror PE `/purchase-evaluations/deleted`, S155).
|
||||
@ -76,6 +117,11 @@ export function KhkkListPage() {
|
||||
return (await api.get<PagedResult<KhkkListItemDto>>(url, {
|
||||
params: {
|
||||
phase: deletedView ? undefined : (phase ?? undefined),
|
||||
// [K4b S166 — FLAG-1 ĐÃ NỐI CẢ 2 NHÁNH] `/deleted` nay nhận `group` mirror
|
||||
// List (lead vá BE cùng lượt: Controller + `ListDeletedContractSigningPlansQuery`
|
||||
// + handler lọc SAU vế IDOR). Gửi thẳng cho cả hai — lọc là việc server,
|
||||
// total/số-trang vì thế nói thật ở cả màn sống lẫn màn đã-xóa.
|
||||
group: group ?? undefined,
|
||||
search: search.trim() || undefined,
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
@ -115,17 +161,29 @@ export function KhkkListPage() {
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
eyebrow="Giai đoạn 2"
|
||||
title={deletedView ? 'Kế hoạch ký kết HĐ — đã xóa' : 'Kế hoạch ký kết HĐ'}
|
||||
subtitle={
|
||||
title={
|
||||
deletedView
|
||||
? 'Phiếu đã xóa mềm — chỉ xem danh sách'
|
||||
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
|
||||
? (group !== null ? `Kế hoạch ký kết HĐ — N${group} · đã xóa` : 'Kế hoạch ký kết HĐ — đã xóa')
|
||||
: group !== null
|
||||
? `Kế hoạch ký kết HĐ — N${group}`
|
||||
: 'Kế hoạch ký kết HĐ'
|
||||
}
|
||||
subtitle={
|
||||
// [K4b S166] Màn "Đã xóa" nay ĐƯỢC in tên nhóm — `/deleted` đã lọc server-side
|
||||
// theo `group` (FLAG-1 nối cùng lượt), tên nhóm hết là lời nói dối.
|
||||
deletedView
|
||||
? (group !== null ? `${khkkGroupMenuLabel(group)} — phiếu đã xóa mềm, chỉ xem` : 'Phiếu đã xóa mềm — chỉ xem danh sách')
|
||||
: group !== null
|
||||
? khkkGroupMenuLabel(group)
|
||||
: 'Chốt giá per-NCC trúng thầu + căn cứ hồ sơ trước khi ký hợp đồng'
|
||||
}
|
||||
icon={deletedView ? <Trash2 className="h-5 w-5" /> : <FileCheck className="h-5 w-5" />}
|
||||
accent="brand"
|
||||
actions={
|
||||
deletedView ? undefined : (
|
||||
<Button onClick={() => navigate('/khkk/create')}>
|
||||
// Mang nhóm đang xem sang màn lập phiếu — không bắt người dùng chọn lại
|
||||
// đúng cái nhóm họ vừa đứng (bắt tay với vá-6 bên `KhkkCreatePage`).
|
||||
<Button onClick={() => navigate(group !== null ? `/khkk/create?group=${group}` : '/khkk/create')}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Lập kế hoạch
|
||||
</Button>
|
||||
@ -151,7 +209,9 @@ export function KhkkListPage() {
|
||||
accent={c.accent}
|
||||
active={phase === c.value}
|
||||
onClick={() => {
|
||||
setPhase(c.value)
|
||||
// Ghi vào lớp override, KHÔNG ghi vào URL — giữ URL của leaf đang đứng
|
||||
// để sidebar không sáng nhầm mục khác (bài S155, xem ghi chú đầu file).
|
||||
setChipPhase(c.value)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -126,6 +126,12 @@ export const KHKK_APPROVAL_GROUPS = [1, 2, 3, 4, 5, 6, 7, 8] as const
|
||||
// đề xuất", **[OG-6] anh soát SAU trên UI**). Cùng bộ chữ này K4a sẽ seed làm LABEL menu
|
||||
// ⇒ nếu K4a land nhãn khác thì SỬA Ở ĐÂY (1 const, 2 app), đừng vá rải trong page.
|
||||
// Rút gọn có chủ đích: bỏ số thứ tự đầu dòng của bảng (ô select đã có "N{n}").
|
||||
//
|
||||
// [K4a S166 — acceptance-5 single-source] ĐÃ ĐỐI CHIẾU TỪNG KÝ TỰ với nhãn BE seed
|
||||
// (`DbInitializer.cs:1960` cho nhóm 1 + dict `khkkGroupLabels` `:2127-2136` cho nhóm
|
||||
// 2..8, cùng `labelBackfill:2235`): 8/8 nhóm khớp theo quan hệ `"{n}. " + chuỗi dưới đây`.
|
||||
// ⇒ K4a KHÔNG đẻ map thứ hai. Bản đầy đủ (có số thứ tự) lấy qua `khkkGroupMenuLabel(n)`
|
||||
// ngay bên dưới — DERIVE chứ không chép, để sửa chữ vẫn chỉ đụng đúng 1 chỗ này.
|
||||
export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
|
||||
1: 'Thiết bị, máy móc, vật tư phụ (A1)',
|
||||
2: 'Kết cấu, hạ tầng, phá dỡ & DV hỗ trợ thi công (A2-A3)',
|
||||
@ -137,6 +143,25 @@ export const KHKK_APPROVAL_GROUP_LABELS: Record<number, string> = {
|
||||
8: 'HĐ chưa ủy quyền — CEO (5.2)',
|
||||
}
|
||||
|
||||
// [K4a S166] Nhãn nhóm ĐẦY ĐỦ = đúng chuỗi BE ghi vào `MenuItems.Label` khi seed.
|
||||
// 🔴 ĐÂY KHÔNG PHẢI NGUỒN CỦA SIDEBAR. Sidebar render nhãn từ payload `/api/menus/me`
|
||||
// (bảng `MenuItems`, admin còn đổi được qua `displayLabel` — Mig 27), nên đừng ai
|
||||
// tưởng sửa chuỗi ở đây thì sidebar đổi theo. Hàm này phục vụ chỗ FE tự vẽ tên nhóm
|
||||
// mà không đi qua menu: cây 8 thư mục nhóm (K5), tiêu đề trang khi vào từ leaf nhóm,
|
||||
// màn xem luồng duyệt theo nhóm (WfView).
|
||||
// ⚠️ Ràng buộc ngầm: BE đang đặt nhãn theo khuôn "{n}. {tên}". Nếu BE bỏ tiền tố số thì
|
||||
// hàm này lệch một cách CÂM (không đỏ build) — lúc đó sửa chính hàm này, đừng vá page.
|
||||
export function khkkGroupMenuLabel(n: number): string {
|
||||
const label = KHKK_APPROVAL_GROUP_LABELS[n]
|
||||
return label ? `${n}. ${label}` : `Nhóm ${n}`
|
||||
}
|
||||
|
||||
// [K4a S166 — O-2] Mã quy trình duyệt của nhóm n. BE khớp bằng regex `^KHKK-N([1-8])$`
|
||||
// (`ContractSigningPlanFeatures.cs:420`, rào 409 "phiếu nhóm 3 pin quy trình nhóm 5");
|
||||
// 8 workflow do `DbInitializer.SeedKhkkGroupWorkflowsAsync` seed, ApplicableType=10.
|
||||
// Gom về 1 hàm để FE không rải chuỗi `KHKK-N` ở nhiều nơi rồi lệch nhau.
|
||||
export const khkkGroupWorkflowCode = (n: number) => `KHKK-N${n}`
|
||||
|
||||
export interface KhkkListItemDto {
|
||||
id: string
|
||||
maKeHoach: string | null
|
||||
|
||||
Reference in New Issue
Block a user