diff --git a/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx b/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx index aacff6d..4db6a7d 100644 --- a/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx +++ b/fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx @@ -1,16 +1,21 @@ // Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08). // Schema riêng UAT trước khi drop legacy. Cấu trúc: // Quy trình (Mã + Tên + ApplicableType) -// Bước 1 — Phòng A (FIXED 3 cấp) -// Cấp 1 — NV X thuộc Phòng A -// Cấp 2 — NV Y thuộc Phòng A -// Cấp 3 — NV Z thuộc Phòng A +// Bước 1 — Phòng A +// Cấp 1 (N NV duyệt) +// Cấp 2 (N NV duyệt) +// Cấp 3 (N NV duyệt) // -// Iteration 2 (UAT feedback 2026-05-08): -// - LOCK CỨNG 3 cấp/bước (không Add/Remove/Move). Logic backend giữ nguyên -// iterate Levels OrderBy Order — chỉ giới hạn UI. -// - Cùng 1 phòng có thể chọn 3 NV cùng cấp (không bắt khác cấp). -// - Select NV CHỈ filter theo Phòng đã chọn (đổi Phòng → reset 3 approver). +// Iteration 3 (UAT feedback 2026-05-08): +// - TỐI ĐA 3 cấp/bước (không có cấp 4). 1/2/3 cấp đều OK — quy trình +// chạy theo số cấp thật sự có. +// - MỖI CẤP CÓ N NV: convention multiple Level rows cùng Order = same Cấp. +// BE iterate group by Order; trong cùng cấp = OR-of-N approvers. +// - 3 section cố định C1/C2/C3 trong UI. Mỗi section có nút "+ Thêm NV" +// + Trash xóa từng NV. +// - Sequential gating: C2 disabled khi C1 chưa có NV. C3 disabled khi C2 +// chưa có NV. Chặn xóa NV cuối C1 khi C2/C3 còn entries. +// - Select NV CHỈ filter theo Phòng đã chọn (đổi Phòng → clear approvers). import { useMemo, useState, type FormEvent } from 'react' import { useParams } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' @@ -65,14 +70,16 @@ type TypeSummaryDto = { history: DefinitionDto[] } -type EditLevel = { name: string; approverUserId: string } -type EditStep = { name: string; departmentId: string | null; levels: EditLevel[] } +type LevelOrder = 1 | 2 | 3 +type EditLevelEntry = { order: LevelOrder; approverUserId: string } +type EditStep = { name: string; departmentId: string | null; levelEntries: EditLevelEntry[] } type ApproverUser = { id: string; fullName: string; email: string; departmentId: string | null } -// Lock cứng 3 cấp/bước per UAT 2026-05-08. -// Logic BE iterate Levels OrderBy Order vẫn cho phép N cấp — đây chỉ là giới hạn UI. -const FIXED_LEVELS_PER_STEP = 3 +// Tối đa 3 cấp/bước per UAT iter 3 (2026-05-08). Quy trình chạy theo số cấp +// thật sự có (1 / 2 / 3). Mỗi cấp có N NV (multiple Level rows cùng Order). +const MAX_LEVELS_PER_STEP = 3 +const LEVEL_ORDERS: LevelOrder[] = [1, 2, 3] // FE typeCode → BE int (giống MenuKeys ApplicableType) const TYPE_CODE_TO_INT: Record = { @@ -86,38 +93,23 @@ const DEFAULT_CODE_BY_TYPE: Record = { 3: 'QT-HD-V2-001', } -function makeEmptyLevels(): EditLevel[] { - return Array.from({ length: FIXED_LEVELS_PER_STEP }, (_, i) => ({ - name: `Cấp ${i + 1}`, - approverUserId: '', - })) -} - function makeEmptyStep(stepNo: number, deptId: string | null = null): EditStep { return { name: `Phòng ${stepNo}`, departmentId: deptId, - levels: makeEmptyLevels(), + levelEntries: [], } } -// Pad/truncate về đúng FIXED_LEVELS_PER_STEP cấp khi clone version cũ -// (phòng khi DB có data legacy >3 hoặc <3 levels). +// Clone existing definition: filter Order ∈ {1,2,3}, drop entries vượt giới hạn. function copyFromDefinition(d: DefinitionDto): EditStep[] { - return d.steps.map(s => { - const levels = s.levels.slice(0, FIXED_LEVELS_PER_STEP).map((l, i) => ({ - name: l.name ?? `Cấp ${i + 1}`, - approverUserId: l.approverUserId, - })) - while (levels.length < FIXED_LEVELS_PER_STEP) { - levels.push({ name: `Cấp ${levels.length + 1}`, approverUserId: '' }) - } - return { - name: s.name, - departmentId: s.departmentId, - levels, - } - }) + return d.steps.map(s => ({ + name: s.name, + departmentId: s.departmentId, + levelEntries: s.levels + .filter(l => l.order >= 1 && l.order <= MAX_LEVELS_PER_STEP) + .map(l => ({ order: l.order as LevelOrder, approverUserId: l.approverUserId })), + })) } // Filter NV theo Phòng. Nếu Phòng = null → fallback all (chưa chọn phòng). @@ -127,6 +119,16 @@ function usersForDept(all: ApproverUser[] | undefined, deptId: string | null): A return all.filter(u => u.departmentId === deptId) } +function entriesAtLevel(step: EditStep, order: LevelOrder): EditLevelEntry[] { + return step.levelEntries.filter(e => e.order === order) +} + +// Cấp k được phép thao tác khi Cấp k-1 có ≥1 NV (gating sequential). +function isLevelEnabled(step: EditStep, order: LevelOrder): boolean { + if (order === 1) return step.departmentId !== null + return entriesAtLevel(step, (order - 1) as LevelOrder).length > 0 +} + export function ApprovalWorkflowsV2Page() { const qc = useQueryClient() const { typeCode } = useParams<{ typeCode?: string }>() @@ -322,25 +324,39 @@ function DefinitionCard({ )} -