// 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
//       Cấp 1 (N NV duyệt)
//       Cấp 2 (N NV duyệt)
//       Cấp 3 (N NV duyệt)
//
// 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'
import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown, Pin, PinOff, Pencil, AlertTriangle } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader'
import { Button } from '@/components/ui/Button'
import { Dialog } from '@/components/ui/Dialog'
import { Input } from '@/components/ui/Input'
import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select'
import { Textarea } from '@/components/ui/Textarea'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import type { Department, Paged } from '@/types/master'
// [W2 S182] 2 hàm dùng chung với màn nghiệp vụ (fe-user) — KHÔNG chép chuỗi
// `KHKK-N{n}` / nhãn nhóm vào file này, để 1 nguồn duy nhất khi BE đổi.
import { khkkGroupMenuLabel, khkkGroupWorkflowCode } from '@/types/khkk'
// [W6 S187] Họ 7 quy trình duyệt HĐ (type-3). Cùng lý do như trên: mã `HD-*` KHÔNG gõ vào
// file này — bảng mirror ở `lib/contractWorkflowFamily.ts`, dùng chung với `Layout.resolvePath`.
import { AW_TYPE_INT_CONTRACT, findContractWfByTypeCode } from '@/lib/contractWorkflowFamily'
// Nhãn 7 loại HĐ — tra bảng SẴN CÓ, không đẻ bảng nhãn thứ hai trong file này.
import { ContractTypeLabel } from '@/types/forms'

// ===== Types (mirror BE AwAdminOverviewDto) =====

type LevelDto = {
  id: string
  order: number
  name: string | null
  approverUserId: string
  approverUserName: string | null
  approverEmail: string | null
  // Mig 29 (S21 t5) — 5 Allow* options per slot Approver
  // Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách
  // Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 storage swap Users→Level (per-Approver-slot)
  allowReturnOneLevel: boolean
  allowReturnOneStep: boolean
  allowReturnToAssignee: boolean
  allowReturnToDrafter: boolean
  allowApproverEditDetails: boolean
  allowApproverEditBudget: boolean
  allowApproverSkipToFinal: boolean
  allowApproverFinalize: boolean       // [Mig 58 CCM item 4] slot Duyệt = KẾT THÚC (không trình CEO)
  // [F6 S155] PER-NGƯỜI (per-row, KHÔNG per-Cấp): NV slot này được XÓA phiếu khi
  // đang ChoDuyet và đến lượt duyệt của chính mình. 1 row = 1 người ⇒ admin chỉ
  // đích danh, không mở cho cả Cấp.
  allowApproverDelete: boolean
  // [S76] Hiển thị-only: approver được nhập/điều chỉnh ngân sách cột nào (suy từ role).
  canEditProBudget: boolean
  canEditCcmBudget: boolean
}
type StepDto = {
  id: string
  order: number
  name: string                      // "Phòng A" — display
  departmentId: string | null
  departmentName: string | null
  levels: LevelDto[]
}
type DefinitionDto = {
  id: string
  code: string
  version: number
  applicableType: number
  applicableTypeLabel: string
  name: string
  description: string | null
  isActive: boolean
  isUserSelectable: boolean    // Mig 25 — admin toggle cho user pick
  // S69 — ngưỡng giá trị "gói CEO". Null = không áp ngưỡng (luồng đủ quy trình).
  // Gói < ngưỡng: CCM duyệt-final không cần CEO. ≥ ngưỡng: phải lên CEO.
  ceoApprovalThreshold: number | null
  // Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
  // ALL xuống per slot Level (xem LevelDto). Admin opt-in per-Approver-slot
  // pattern proven 3× cumulative: F1+F3 (5 flag Mig 29) + F4 (Mig 30) + F2
  // (Mig 31 refactor sang Approver scope, storage cũ Users đã drop).
  activatedAt: string | null
  createdAt: string
  steps: StepDto[]
}
type TypeSummaryDto = {
  applicableType: number
  applicableTypeLabel: string
  active: DefinitionDto | null
  history: DefinitionDto[]
}

type LevelOrder = 1 | 2 | 3
type EditLevelEntry = {
  // [F-7 S155] Id của dòng Cấp ĐANG TỒN TẠI trong DB. `undefined` = dòng MỚI
  // (admin vừa bấm "+ Thêm NV"). CHỈ dùng cho đường PUT sửa-tại-chỗ:
  // `UpdateAwLevelInput.Id` là `Guid?` — có Id = sửa dòng cũ, null = thêm người
  // vào Cấp có sẵn (ApprovalWorkflowV2AdminFeatures.cs:424-426).
  // Đường POST tạo version mới KHÔNG gửi field này (BE sinh Id mới).
  id?: string
  order: LevelOrder
  approverUserId: string
  // Mig 29 (S21 t5) — 5 Allow* per slot (default backward compat S17: chỉ
  // AllowReturnToDrafter=true, 4 còn lại false).
  // Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách (default false).
  // Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 admin opt-in per-Approver-slot (default false).
  allowReturnOneLevel: boolean
  allowReturnOneStep: boolean
  allowReturnToAssignee: boolean
  allowReturnToDrafter: boolean
  allowApproverEditDetails: boolean
  allowApproverEditBudget: boolean
  allowApproverSkipToFinal: boolean
  allowApproverFinalize: boolean
  // [F6 S155] Cho phép NV này xóa phiếu khi đến lượt duyệt của mình (default false).
  allowApproverDelete: boolean
}
type EditStep = {
  // [F-7 S155] Id + Order GỐC của Bước trong DB — BẮT BUỘC cho PUT sửa-tại-chỗ
  // (`UpdateAwStepInput.Id` là Guid **không** nullable: lệnh này không có đường
  // tạo Bước mới) và `reqStep.Order` phải KHỚP `dbStep.Order` (:554) — nên KHÔNG
  // được suy ra bằng index `i+1` như đường POST (Order trong DB có thể không liền mạch).
  id?: string
  order?: number
  name: string
  departmentId: string | null
  levelEntries: EditLevelEntry[]
}

type ApproverUser = { id: string; fullName: string; email: string; departmentId: string | null }

// 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]

// [W1 KHKK S161] "Kế hoạch ký kết HĐ" = slot 10 của `ApprovalWorkflowApplicableType`
// (`ApprovalWorkflow.cs:66` trước đó dừng ở `TravelRequest = 9`). Đặt TÊN cho số 10
// vì nó dùng ở HAI nơi trong file: map deep-link ngay bên dưới + gợi ý mã quy trình
// mặc định — số trần rải 2 chỗ là kiểu sửa 1 quên 1.
// [K3 S164 — GỠ KHOÁ] Chỗ dùng THỨ BA của hằng này (rào 2 ô "duyệt sớm" trong
// Designer) ĐÃ GỠ, xem khối ghi chú ngay dưới.
const TYPE_INT_CONTRACT_SIGNING_PLAN = 10

// [K3 S164 — SUPERSEDE W1 S161] 🔴 Duyệt sớm ĐÃ MỞ cho KHKK từ K3 (owner đảo phán
// quyết S155 @S164: TP.CCM quyết lên CEO hay kết thúc). Trước đó file này có hằng
// `KHKK_EARLY_FINALIZE_NOTE` + cờ `earlyFinalizeLocked` ép 2 ô "Duyệt thẳng Cấp cuối"
// và "Duyệt là KẾT THÚC" về mờ-và-false cho type 10, vì backend chưa port đường
// finalize. BE đã port nhánh finalize ở K3 CÙNG WAVE ⇒ cờ hết là "cờ chết", khoá lại
// thành khoá SAI (chặn đúng cái owner vừa yêu cầu mở). Gỡ ở CẢ HAI lớp đã rào —
// render (banner + `disabled`) và payload (`levelFlagsPayload` ép false) — vì chỉ gỡ
// 1 lớp thì màn hình và dữ liệu nói hai đằng.
// Banner giải thích đi kèm cũng XOÁ (không đổi chữ): nội dung nó chỉ để cảnh báo
// khoá, hết khoá là hết việc; còn câu supersede mang từ vựng nội bộ ("K3", "S155/
// S164") nên thuộc về comment này, không đẩy lên UI admin.

// FE typeCode → BE int (giống MenuKeys ApplicableType)
const TYPE_CODE_TO_INT: Record<string, number> = {
  DuyetNcc: 1,
  DuyetNccPhuongAn: 2,
  Contract: 3,
  // [W1 KHKK S161] Mở deep-link `/system/approval-workflows-v2/ContractSigningPlan`.
  // KHÔNG chép nhãn tiếng Việt vào FE: nhãn hiển thị lấy từ BE qua
  // `applicableTypeLabel` (AwAdminOverviewDto) — file này không có bảng nhãn riêng.
  ContractSigningPlan: TYPE_INT_CONTRACT_SIGNING_PLAN,
}
const DEFAULT_CODE_BY_TYPE: Record<number, string> = {
  1: 'QT-DN-V2-001',
  2: 'QT-DN-PA-V2-001',
  3: 'QT-HD-V2-001',
  // Chỉ là giá trị GỢI Ý điền sẵn ô "Mã quy trình" — admin sửa được trước khi lưu.
  // Không khai thì rơi về fallback chung 'QT-V2-001' (hằng fallback trong TypePanel — trỏ TÊN, đừng neo số dòng: reviewer F-9).
  [TYPE_INT_CONTRACT_SIGNING_PLAN]: 'QT-KHKK-V2-001',
}

function makeEmptyStep(stepNo: number, deptId: string | null = null): EditStep {
  return {
    name: `Phòng ${stepNo}`,
    departmentId: deptId,
    levelEntries: [],
  }
}

// Clone existing definition: filter Order ∈ {1,2,3}, drop entries vượt giới hạn.
// Mig 29 (S21 t5) — clone 5 Allow* per slot từ existing Level.
function copyFromDefinition(d: DefinitionDto): EditStep[] {
  return d.steps.map(s => ({
    // [F-7 S155] Giữ Id + Order gốc. Đường POST (tạo version mới) KHÔNG dùng 2
    // field này; đường PUT (sửa tại chỗ) BẮT BUỘC có. Cùng 1 hàm nạp cho cả 2
    // chế độ để không sinh nhánh nạp thứ hai — nơi 2 nhánh dễ lệch nhau.
    id: s.id,
    order: s.order,
    name: s.name,
    departmentId: s.departmentId,
    levelEntries: s.levels
      .filter(l => l.order >= 1 && l.order <= MAX_LEVELS_PER_STEP)
      .map(l => ({
        id: l.id,
        order: l.order as LevelOrder,
        approverUserId: l.approverUserId,
        allowReturnOneLevel: l.allowReturnOneLevel ?? false,
        allowReturnOneStep: l.allowReturnOneStep ?? false,
        allowReturnToAssignee: l.allowReturnToAssignee ?? false,
        allowReturnToDrafter: l.allowReturnToDrafter ?? true,
        allowApproverEditDetails: l.allowApproverEditDetails ?? false,
        allowApproverEditBudget: l.allowApproverEditBudget ?? false,
        allowApproverSkipToFinal: l.allowApproverSkipToFinal ?? false,
        allowApproverFinalize: l.allowApproverFinalize ?? false,
        // [F6 S155] `?? false` giữ nguyên khuôn F5: bản cũ (BE chưa có cột) clone
        // ra vẫn hợp lệ thay vì undefined lọt vào checked → uncontrolled input.
        allowApproverDelete: l.allowApproverDelete ?? false,
      })),
  }))
}

// Mig 29 — Factory default cho entry mới (admin click "+ Thêm NV"). 5 flag
// default backward compat S17: chỉ AllowReturnToDrafter=true.
function makeDefaultLevelEntry(order: LevelOrder, approverUserId: string): EditLevelEntry {
  return {
    order,
    approverUserId,
    allowReturnOneLevel: false,
    allowReturnOneStep: false,
    allowReturnToAssignee: false,
    allowReturnToDrafter: true,
    allowApproverEditDetails: false,
    allowApproverEditBudget: false,
    allowApproverSkipToFinal: false,
    allowApproverFinalize: false,
    // [F6 S155] default false — quyền xóa phiếu đang duyệt phải admin tick tay.
    allowApproverDelete: false,
  }
}

// [F-7 S155] 🔴 CHỐNG BẪY "silent drop": payload gửi BE là object literal LIỆT KÊ
// TAY từng field (không spread `...e`) — thiếu 1 dòng thì TS vẫn build sạch, UI vẫn
// tick, BE nhận default `false` ⇒ tick vô hình (đã xảy ra thật với F6, xem
// sub-implementer-frontend-2.md §2.4). Từ đợt này có HAI call-site (POST + PUT) ⇒
// nguy cơ nhân đôi. Gom 9 cờ vào ĐÚNG MỘT hàm, cả 2 call-site cùng spread nó:
// thêm cờ thứ 10 chỉ phải sửa 1 chỗ, không thể lệch giữa 2 đường.
//   - 7 cờ QUYỀN-TẠI-CHỖ (4×Return + EditDetails + EditBudget + Delete): PUT ghi được.
//   - 2 cờ ĐỔI-ĐƯỜNG-ĐI (SkipToFinal, Finalize): PUT chỉ ECHO — gửi lệch ⇒ 409
//     (ApprovalWorkflowV2AdminFeatures.cs:580-585). Vẫn PHẢI gửi đúng giá trị đang lưu.
//
// [K3 S164 — GỠ KHOÁ] Tham số thứ 2 `lockEarlyFinalize` (W1 S161, ép 2 cờ "duyệt sớm"
// về `false` cho type 10) ĐÃ BỎ: mọi loại quy trình nay gửi đúng giá trị admin tick.
// Xoá hẳn tham số chứ không để nó nằm chết — `noUnusedParameters` bật, và param mồ côi
// ở hàm 2 call-site là mồi cho lượt sau tưởng còn rào.
function levelFlagsPayload(e: EditLevelEntry) {
  return {
    allowReturnOneLevel: e.allowReturnOneLevel,
    allowReturnOneStep: e.allowReturnOneStep,
    allowReturnToAssignee: e.allowReturnToAssignee,
    allowReturnToDrafter: e.allowReturnToDrafter,
    allowApproverEditDetails: e.allowApproverEditDetails,
    allowApproverEditBudget: e.allowApproverEditBudget,
    allowApproverSkipToFinal: e.allowApproverSkipToFinal,
    allowApproverFinalize: e.allowApproverFinalize,
    allowApproverDelete: e.allowApproverDelete,
  }
}

// Filter NV theo Phòng. Nếu Phòng = null → fallback all (chưa chọn phòng).
function usersForDept(all: ApproverUser[] | undefined, deptId: string | null): ApproverUser[] {
  if (!all) return []
  if (!deptId) return all
  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 }>()

  // [W2a S182] 8 leaf `AwV2_KhkkN1..8` deep-link về CHÍNH trang này với typeCode
  // `KhkkN{1..8}` (`Layout.tsx:188` — chỗ-thứ-4 Pattern 16-bis đã nối từ K3 S164).
  // `KhkkN{n}` KHÔNG phải một ApplicableType riêng: cả 8 nhóm đều là type 10
  // (Kế hoạch ký kết HĐ), phân biệt nhau bằng MÃ quy trình `KHKK-N{n}`. Nên chỗ này
  // tách 2 khái niệm:
  //   · `khkkGroup`  = nhóm duyệt 1..8 (null nếu không vào từ leaf nhóm)
  //   · `selectedTypeInt` = ApplicableType gửi BE — nhóm nào cũng là 10.
  // Hệ quả CÓ CHỦ ĐÍCH: queryKey của 8 panel nhóm + panel `/ContractSigningPlan`
  // TRÙNG NHAU ⇒ 1 lần tải dùng chung, và invalidate sau khi lưu làm tươi cả 9 màn.
  const khkkGroupMatch = typeCode?.match(/^KhkkN([1-8])$/) ?? null
  const khkkGroup = khkkGroupMatch ? Number(khkkGroupMatch[1]) : null

  // [W6 S187 · YC-023] HỌ THỨ HAI cùng cơ chế: 7 leaf `AwV2_Hd*` deep-link về CHÍNH trang này
  // với typeCode `Hd{TOKEN}` (`Layout.tsx` — chỗ-thứ-4 Pattern 16-bis). `HdTP`/`HdNTNCC`… KHÔNG
  // phải ApplicableType riêng: cả 7 loại đều là type 3 (Hợp đồng), phân biệt nhau bằng MÃ quy
  // trình `HD-TP`/`HD-NT-NCC`… ⇒ y hệt quan hệ `KhkkN{n}` ⟂ type 10.
  const contractFamily = findContractWfByTypeCode(typeCode)

  const selectedTypeInt = khkkGroup !== null
    ? TYPE_INT_CONTRACT_SIGNING_PLAN
    : contractFamily !== null
      ? AW_TYPE_INT_CONTRACT
      : typeCode ? TYPE_CODE_TO_INT[typeCode] : null

  const overview = useQuery({
    queryKey: ['approval-workflow-v2-overview', selectedTypeInt],
    queryFn: async () => {
      const params = selectedTypeInt ? { applicableType: selectedTypeInt } : {}
      return (await api.get<{ types: TypeSummaryDto[] }>('/approval-workflows-v2', { params })).data
    },
  })

  const typeBucket = selectedTypeInt
    ? overview.data?.types.find(t => t.applicableType === selectedTypeInt) ?? null
    : null

  // [W2b S182 · mở rộng W6 S187] MÃ quy trình mà panel đang xem — `null` = panel theo LOẠI
  // (xem cả bucket, hành vi từ Mig 22). Hai HỌ per-Code dùng CHUNG một đường, không đẻ nhánh
  // cắt thứ hai: KHKK `KHKK-N{n}` (type 10) · HĐ `HD-*` (type 3).
  const familyCode = khkkGroup !== null
    ? khkkGroupWorkflowCode(khkkGroup)
    : contractFamily?.code ?? null

  // [W2b S182] Lát cắt TỔNG HỢP cho panel per-Code. BE trả 1 bucket cho CẢ loại
  // (`active` = 1-trong-N quy trình của họ, tuỳ bản nào kích hoạt sau cùng), nên
  // panel per-Code KHÔNG được đọc `bucket.active` — sẽ hiện nhầm quy trình của mã khác.
  // Cắt lại từ `history`:
  //   🔴 `history` là DANH SÁCH ĐẦY ĐỦ, KỂ CẢ bản đang áp dụng (verified BE — `AwTypeSummaryDto`
  //      chú thích `History` = "KHO PHIÊN BẢN — MỌI version của loại, kể cả bản đang áp dụng";
  //      trỏ TÊN field thay vì số dòng vì khối đó vừa đổi ở W1b);
  //      chính `TypePanel` mới lọc `!isActive` lúc render mục "Lịch sử versions".
  //   · `active` suy TỪ chính lát cắt (`find(v => v.isActive)`), không mượn ô ngoài.
  // Giữ nguyên shape `TypeSummaryDto` ⇒ `TypePanel`/`Designer` không cần biết
  // chúng đang xem một mã hay cả loại.
  //
  // 🔴 [W6 S187 — QUYẾT F-3, phương án (ii)] KHÔNG đọc `ActiveOthers` mà W1b thêm vào DTO.
  //    Bài toán F-3 ("type-3 nhiều bản cùng IsActive ⇒ Designer giấu N-1 bản") tan ngay khi
  //    cắt theo `Code`: mỗi panel chỉ còn ĐÚNG 1 bản active của chính mã đó, N-1 bản kia thuộc
  //    về panel của mã khác chứ không "bị giấu". Đọc thêm `activeOthers` ở đây sẽ kéo quy trình
  //    của 6 loại HĐ khác vào panel này — đúng cái lỗi lát cắt sinh ra để chặn.
  //    ⚠️ Hệ quả cho BE (NGOÀI lane FE, báo lead): sau bản này `ActiveOthers` KHÔNG CÒN consumer
  //    FE nào — gỡ field + re-check test W1b là việc của `implementer-backend`, đừng tưởng đã xong.
  const currentType: TypeSummaryDto | null = useMemo(() => {
    if (familyCode === null) return typeBucket
    if (!typeBucket) return null
    const versions = typeBucket.history.filter(d => d.code === familyCode)
    return {
      applicableType: typeBucket.applicableType,
      applicableTypeLabel: typeBucket.applicableTypeLabel,
      active: versions.find(v => v.isActive) ?? null,
      history: versions,
    }
  }, [familyCode, typeBucket])

  // [W2c S182 · mở rộng W6 S187] Mã quy trình của panel per-Code là KHOÁ, không phải gợi ý:
  // mỗi thành viên của họ đúng MỘT chuỗi phiên bản. Gõ mã khác trên panel này = đẻ quy trình
  // mồ côi — KHKK: phiếu nhóm đó vẫn tìm `KHKK-N{n}` (`KhkkCreatePage.tsx:113`); HĐ: BE khoá
  // CỨNG theo (86) — `ContractWorkflowGuards.EnsureFamilyLock` bắt HĐ loại X phải pin đúng
  // `ContractApprovalWorkflowFamily.CodeFor(X)`, mã lệch thì không HĐ nào pin được nó.
  const lockedCode = familyCode

  // [W2h/D4 S182 · mở rộng W6 S187] Vào từ leaf per-Code thì tiêu đề phải nói ĐÚNG mã nào —
  // `applicableTypeLabel` của BE chỉ có MỘT chuỗi chung cho cả họ ("Kế hoạch ký kết HĐ" cho 8
  // nhóm, "Hợp đồng" cho 7 loại) ⇒ để nguyên thì 15 panel trông y hệt nhau.
  // 🔴 Truthy-check chứ không dùng thẳng `ContractTypeLabel[n]`: `Record<number, string>` trả
  // `string` cho MỌI số, nên loại nằm ngoài bảng nhãn sẽ in ra `undefined` giữa tiêu đề thay vì
  // rơi về nhãn của BE (cùng lý do `khkkGroupMenuLabel` phải tự kiểm — `types/khkk.ts`).
  const panelLabel = khkkGroup !== null
    ? khkkGroupMenuLabel(khkkGroup)
    : contractFamily !== null
      ? ContractTypeLabel[contractFamily.contractType] ?? currentType?.applicableTypeLabel ?? null
      : currentType?.applicableTypeLabel ?? null

  const panelDescription = khkkGroup !== null
    ? `${currentType?.applicableTypeLabel ?? 'Kế hoạch ký kết HĐ'} — nhóm duyệt N${khkkGroup}. Mã quy trình khoá theo nhóm: ${lockedCode}; mỗi lần lưu là một phiên bản mới của CHÍNH nhóm này, 7 nhóm còn lại không đổi.`
    : contractFamily !== null
      // Câu cuối KHÔNG phải trang trí: nó nói vì sao ô Mã bị khoá — HĐ loại này tự pin đúng mã
      // đó lúc trình duyệt, quy trình mang mã khác thì không HĐ nào với tới được.
      ? `${currentType?.applicableTypeLabel ?? 'Hợp đồng'} — ${panelLabel}. Mã quy trình khoá theo loại HĐ: ${lockedCode}; mỗi lần lưu là một phiên bản mới của CHÍNH loại này, 6 loại còn lại không đổi. Hợp đồng loại này tự pin đúng mã trên khi trình duyệt.`
      : currentType
        ? 'Mỗi Bước = 1 Phòng. Mỗi Cấp trong Bước = 1 nhân viên cụ thể duyệt. Tuần tự: Cấp 1 → Cấp 2 → ... → Bước kế.'
        : 'Schema mới UAT — chọn loại quy trình từ menu bên trái.'

  return (
    <div className="p-6">
      <PageHeader
        title={
          <span className="flex items-center gap-2">
            <Workflow className="h-5 w-5" />
            {panelLabel ? `Quy trình duyệt (Mới): ${panelLabel}` : 'Quy trình duyệt (Mới)'}
          </span>
        }
        description={panelDescription}
      />

      {overview.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}

      {/* Lưới landing (không có `:typeCode`) = THẺ TÓM TẮT theo ApplicableType.
          ⚠️ KHAI THẲNG giới hạn còn lại: với 2 loại per-Code-family (10 KHKK · 3 HĐ) thẻ này in
          `t.active` = MỘT trong N bản đang áp dụng và `t.history.length` = version của CẢ họ —
          nó tóm tắt LOẠI, không tóm tắt từng mã. Đây là hình dạng có từ S182 (KHKK), W6 không
          đổi: đường vào thật của admin là 15 leaf per-Code ở sidebar, mỗi leaf mở đúng panel đã
          cắt theo `Code` nên không có bản nào bị giấu trên đường người dùng đi. */}
      {overview.data && !currentType && (
        <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
          {overview.data.types.map(t => (
            <div key={t.applicableType} className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
              <div className="flex items-center justify-between">
                <h3 className="text-sm font-semibold text-slate-800">{t.applicableTypeLabel}</h3>
                {t.active && (
                  <span className="rounded bg-brand-50 px-2 py-0.5 font-mono text-[10px] font-medium text-brand-700">
                    {t.active.code} v{String(t.active.version).padStart(2, '0')}
                  </span>
                )}
              </div>
              <div className="mt-2 text-xs text-slate-500">
                {t.active
                  ? `${t.active.steps.length} bước · ${t.active.steps.reduce((s, x) => s + x.levels.length, 0)} cấp · ${t.history.length} version`
                  : 'Chưa có quy trình'}
              </div>
            </div>
          ))}
        </div>
      )}

      {currentType && (
        <TypePanel
          // [W2b S182] Panel nhóm và panel loại dùng CHUNG component: khác nhau duy
          // nhất ở lát cắt truyền vào + `lockedCode` (null = panel loại, tự do đặt mã).
          key={typeCode ?? 'all'}
          type={currentType}
          lockedCode={lockedCode}
          onSaved={() => qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })}
        />
      )}
    </div>
  )
}

// ===== Per-type panel =====

function TypePanel({
  type,
  // [W2c S182] `null` = panel theo LOẠI (admin tự đặt mã, hành vi cũ).
  // Khác null = panel theo NHÓM KHKK: mã bị khoá, mọi version đều thuộc chuỗi đó.
  lockedCode,
  onSaved,
}: {
  type: TypeSummaryDto
  lockedCode: string | null
  onSaved: () => void
}) {
  const [designerOpen, setDesignerOpen] = useState(false)
  const [cloneFrom, setCloneFrom] = useState<DefinitionDto | null>(null)
  // [F-7 S155] Khác null ⇒ Designer mở ở chế độ SỬA TẠI CHỖ (PUT, giữ nguyên Id +
  // Version). Null ⇒ chế độ tạo version MỚI (POST) như cũ. Hai đường loại trừ nhau.
  const [editTarget, setEditTarget] = useState<DefinitionDto | null>(null)
  const qc = useQueryClient()

  const del = useMutation({
    mutationFn: async (id: string) => api.delete(`/approval-workflows-v2/${id}`),
    onSuccess: () => {
      toast.success('Đã xoá version')
      qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })
    },
    onError: err => toast.error(getErrorMessage(err)),
  })

  // Mig 25 — toggle "cho user pick lúc create phiếu" (stick/unstick)
  const toggleSelectable = useMutation({
    mutationFn: async ({ id, isUserSelectable }: { id: string; isUserSelectable: boolean }) =>
      api.patch(`/approval-workflows-v2/${id}/user-selectable`, { isUserSelectable }),
    onSuccess: (_data, vars) => {
      toast.success(vars.isUserSelectable
        ? 'Đã ghim — user có thể chọn quy trình này'
        : 'Đã bỏ ghim — user không thấy quy trình này')
      qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })
    },
    onError: err => toast.error(getErrorMessage(err)),
  })

  return (
    <div className="space-y-4">
      {type.active ? (
        <DefinitionCard
          def={type.active}
          isActive
          onClone={d => { setEditTarget(null); setCloneFrom(d); setDesignerOpen(true) }}
          onEditInPlace={d => { setCloneFrom(null); setEditTarget(d); setDesignerOpen(true) }}
          onToggleSelectable={() => toggleSelectable.mutate({
            id: type.active!.id,
            isUserSelectable: !type.active!.isUserSelectable,
          })}
          onDelete={() => {
            if (confirm(`Xoá version đang áp dụng "${type.active!.code} v${type.active!.version}"?`)) {
              del.mutate(type.active!.id)
            }
          }}
        />
      ) : (
        <div className="rounded-xl border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500">
          Chưa có quy trình cho loại này. Tạo version đầu tiên bên dưới.
        </div>
      )}

      <div className="flex items-center justify-between">
        <h3 className="text-sm font-semibold text-slate-700">Lịch sử versions</h3>
        <Button onClick={() => { setEditTarget(null); setCloneFrom(type.active); setDesignerOpen(true) }}>
          <Plus className="h-4 w-4" />
          Tạo quy trình mới
        </Button>
      </div>

      {type.history.filter(d => !d.isActive).length === 0 && (
        <div className="rounded-md border border-slate-200 bg-slate-50 p-4 text-xs text-slate-500">
          Chưa có version cũ. Khi tạo version mới, version hiện tại tự động archive.
        </div>
      )}

      <div className="space-y-2">
        {type.history.filter(d => !d.isActive).map(d => (
          <DefinitionCard
            key={d.id}
            def={d}
            isActive={false}
            onClone={dd => { setEditTarget(null); setCloneFrom(dd); setDesignerOpen(true) }}
            // 🔴 Version ARCHIVED cũng PHẢI sửa-tại-chỗ được: đúng kịch bản F3 —
            // admin lỡ bấm "Tạo quy trình mới", bản cũ archive, nhưng phiếu đang
            // treo vẫn pin Id bản cũ đó. Cứu phiếu = sửa CHÍNH bản archived này.
            onEditInPlace={dd => { setCloneFrom(null); setEditTarget(dd); setDesignerOpen(true) }}
            onToggleSelectable={() => toggleSelectable.mutate({
              id: d.id,
              isUserSelectable: !d.isUserSelectable,
            })}
            onDelete={() => {
              if (confirm(`Xoá version "${d.code} v${d.version}"?`)) del.mutate(d.id)
            }}
          />
        ))}
      </div>

      {designerOpen && (
        <Designer
          applicableType={type.applicableType}
          applicableTypeLabel={type.applicableTypeLabel}
          lockedCode={lockedCode}
          cloneFrom={cloneFrom}
          editTarget={editTarget}
          onClose={() => { setDesignerOpen(false); setCloneFrom(null); setEditTarget(null) }}
          onSaved={() => { setDesignerOpen(false); setCloneFrom(null); setEditTarget(null); onSaved() }}
        />
      )}
    </div>
  )
}

// ===== Definition card (read-only) =====

function DefinitionCard({
  def,
  isActive,
  onClone,
  onEditInPlace,
  onToggleSelectable,
  onDelete,
}: {
  def: DefinitionDto
  isActive: boolean
  onClone: (d: DefinitionDto) => void
  onEditInPlace: (d: DefinitionDto) => void
  onToggleSelectable: () => void
  onDelete: () => void
}) {
  return (
    <div className={`rounded-xl border bg-white p-5 shadow-sm ${isActive ? 'border-brand-200' : 'border-slate-200'}`}>
      <div className="flex items-start justify-between gap-4">
        <div className="min-w-0 flex-1">
          <div className="flex items-center gap-2">
            <h3 className="text-[15px] font-semibold text-slate-900">{def.name}</h3>
            <span className="rounded bg-slate-100 px-2 py-0.5 font-mono text-[11px] text-slate-600">
              {def.code} v{String(def.version).padStart(2, '0')}
            </span>
            {isActive ? (
              <span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
                <CheckCircle2 className="h-3 w-3" />
                Đang áp dụng
              </span>
            ) : (
              <span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">
                <History className="h-3 w-3" />
                Archived
              </span>
            )}
            {/* Mig 25 — badge IsUserSelectable: ghim cho user pick */}
            {def.isUserSelectable && (
              <span className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-medium text-amber-700" title="User có thể chọn quy trình này khi tạo phiếu">
                <Pin className="h-3 w-3" />
                Cho user chọn
              </span>
            )}
            {/* S69 — badge ngưỡng gói CEO (nếu có set) */}
            {def.ceoApprovalThreshold != null && (
              <span
                className="inline-flex items-center gap-1 rounded-full bg-rose-100 px-2 py-0.5 text-[10px] font-medium text-rose-700"
                title="Gói ≥ ngưỡng phải lên CEO duyệt. Gói < ngưỡng: CCM duyệt là xong."
              >
                Ngưỡng CEO: {def.ceoApprovalThreshold.toLocaleString('vi-VN')}đ
              </span>
            )}
          </div>
          {def.description && <p className="mt-1 text-xs leading-relaxed text-slate-500">{def.description}</p>}

          <ol className="mt-3 space-y-2">
            {def.steps.map(s => (
              <li key={s.id} className="rounded-lg border border-slate-100 bg-slate-50/30 p-3">
                <div className="flex items-center gap-2">
                  <div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand-600 text-[11px] font-bold text-white">
                    {s.order}
                  </div>
                  <span className="text-sm font-medium text-slate-800">Bước {s.order} — {s.name}</span>
                  {s.departmentName && (
                    <span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
                      {s.departmentName}
                    </span>
                  )}
                </div>
                {/* Group by Order — 1 cấp có N NV */}
                <ul className="mt-2 ml-9 space-y-1.5">
                  {s.levels.length === 0 ? (
                    <li className="text-[11px] italic text-slate-400">Chưa có cấp duyệt</li>
                  ) : (
                    Array.from(
                      s.levels.reduce((map, l) => {
                        const arr = map.get(l.order) ?? []
                        arr.push(l)
                        map.set(l.order, arr)
                        return map
                      }, new Map<number, LevelDto[]>()).entries(),
                    )
                      .sort(([a], [b]) => a - b)
                      .map(([order, group]) => (
                        <li key={order} className="flex items-start gap-2 text-xs">
                          <span className="mt-0.5 shrink-0 rounded-full bg-violet-100 px-2 py-0.5 font-mono text-[10px] font-bold text-violet-700">
                            Cấp {order}
                          </span>
                          <div className="flex-1 space-y-0.5">
                            {group.map(l => (
                              <div key={l.id} className="flex flex-wrap items-center gap-1.5">
                                <span className="font-medium text-slate-800">
                                  {l.approverUserName ?? l.approverUserId}
                                </span>
                                {l.approverEmail && (
                                  <span className="text-[10px] text-slate-400">({l.approverEmail})</span>
                                )}
                                {/* [S76] Badge quyền nhập/điều chỉnh ngân sách (hiển thị-only, suy từ role) */}
                                {l.canEditProBudget && (
                                  <span className="rounded bg-amber-100 px-1.5 py-0.5 text-[9px] font-semibold text-amber-700" title="Được nhập/điều chỉnh ngân sách cột PRO">✎ NS PRO</span>
                                )}
                                {l.canEditCcmBudget && (
                                  <span className="rounded bg-sky-100 px-1.5 py-0.5 text-[9px] font-semibold text-sky-700" title="Được nhập/điều chỉnh ngân sách cột CCM">✎ NS CCM</span>
                                )}
                              </div>
                            ))}
                          </div>
                        </li>
                      ))
                  )}
                </ul>
              </li>
            ))}
          </ol>
        </div>
        <div className="flex w-44 shrink-0 flex-col gap-1.5">
          {/* [F-7 S155] HAI hành động TÁCH BẠCH — hệ quả khác hẳn nhau, nhãn phải nói rõ:
              (1) SỬA TẠI CHỖ → PUT, giữ nguyên Id + phiên bản ⇒ phiếu ĐANG CHẠY nhận NGAY.
              (2) TẠO PHIÊN BẢN MỚI → POST, sinh Id mới ⇒ CHỈ áp cho phiếu tạo SAU đó. */}
          <Button
            size="sm"
            onClick={() => onEditInPlace(def)}
            title="Sửa ngay trên phiên bản này: thêm/bớt người trong Cấp có sẵn, đổi quyền duyệt. Giữ nguyên số phiên bản — phiếu ĐANG CHẠY áp dụng NGAY."
          >
            <Pencil className="h-3.5 w-3.5" />
            Sửa tại chỗ
          </Button>
          <Button
            variant="outline"
            size="sm"
            onClick={() => onClone(def)}
            title="Chép bản này thành phiên bản MỚI (số phiên bản tăng). Phiếu đang chạy VẪN theo bản cũ — chỉ phiếu tạo sau mới theo bản mới."
          >
            <Plus className="h-3.5 w-3.5" />
            Tạo phiên bản mới
          </Button>
          {/* Mig 25 — toggle stick: cho user chọn quy trình này khi tạo phiếu */}
          <Button
            variant="outline"
            size="sm"
            onClick={onToggleSelectable}
            title={def.isUserSelectable ? 'Bỏ ghim — user sẽ không thấy quy trình này' : 'Ghim — user có thể chọn quy trình này'}
          >
            {def.isUserSelectable ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
            {def.isUserSelectable ? 'Bỏ ghim' : 'Ghim cho user'}
          </Button>
          <Button variant="outline" size="sm" onClick={onDelete}>
            <Trash2 className="h-3.5 w-3.5" />
            Xoá version
          </Button>
        </div>
      </div>
    </div>
  )
}

// ===== Designer dialog =====

function Designer({
  applicableType,
  applicableTypeLabel,
  lockedCode,
  cloneFrom,
  editTarget,
  onClose,
  onSaved,
}: {
  applicableType: number
  applicableTypeLabel: string
  // [W2c S182] Mã bị KHOÁ theo nhóm KHKK (`KHKK-N{n}`), null = tự do như cũ.
  lockedCode: string | null
  cloneFrom: DefinitionDto | null
  // [F-7 S155] Khác null ⇒ chế độ SỬA TẠI CHỖ (PUT). Loại trừ nhau với cloneFrom.
  editTarget: DefinitionDto | null
  onClose: () => void
  onSaved: () => void
}) {
  // [F-7 S155] `isEdit` quyết định TOÀN BỘ hành vi khác biệt của Designer:
  // nguồn nạp, field nào khoá, thao tác nào chặn, và gọi PUT hay POST.
  const isEdit = editTarget !== null
  const source = editTarget ?? cloneFrom

  // [W2d/W2f S182 — D3=C] Kế hoạch ký kết HĐ (type 10, gồm cả 8 panel nhóm) chưa
  // dùng tới một phần cờ quyền của khuôn Duyệt NCC. Gom về ĐÚNG MỘT cờ để 8 ô
  // (7 ô quyền + ô Ngưỡng CEO) không mỗi chỗ tự viết lại điều kiện rồi lệch nhau.
  const isKhkk = applicableType === TYPE_INT_CONTRACT_SIGNING_PLAN
  // Câu dùng chung cho các ô CHƯA có đường chạy ở KHKK. Ô nào có lý do RIÊNG thì
  // viết riêng tại chỗ (xem ô "Trả về Drafter") — trạng thái tick vẫn là THẬT.
  // 🔴 Câu nhắc phải gắn `title` trên <label>, TUYỆT ĐỐI KHÔNG trên <input disabled>:
  // Chromium (Chrome/Edge — trình duyệt đích) nuốt sự kiện chuột của form-control
  // disabled ⇒ tooltip KHÔNG bao giờ bật, admin chỉ còn thấy mấy ô xám vô cớ. Label
  // không disabled nên rê vào chữ là hiện. Ai dời ngược lên input = làm câm lại.
  const khkkNotYetHint = 'Chưa áp dụng cho KHKK'
  // Ô "Trả về Drafter" (#4) khác 6 ô kia về NGHĨA, nên câu riêng — và câu này còn
  // hiện thành chữ NHÌN THẤY ĐƯỢC ngay dưới ô: đây là thông tin PHÂN BIỆT #4 với 6 ô
  // "chưa áp dụng", không được để nó phụ thuộc vào việc người dùng có rê chuột hay không.
  // Một chuỗi dùng cho cả tooltip lẫn dòng chữ, để hai chỗ không trôi lệch nhau.
  const khkkDrafterHint = 'KHKK hiện luôn trả về người soạn khi Trả lại — cờ chưa phân nhánh'

  // [K3 S164 — GỠ KHOÁ · vá chữ W2h S182] Cờ `earlyFinalizeLocked` (W1 S161) ĐÃ BỎ HẲN
  // và ô "Duyệt là KẾT THÚC" (#8) nay SỐNG cho mọi loại — BE đã port đường finalize ở K3
  // cùng wave, owner chốt TP.CCM là người quyết lên CEO hay kết thúc.
  // 🔴 Đính chính câu cũ ("2 ô duyệt sớm sống cho MỌI loại"): chỉ có MỘT trong hai ô đó
  // sống cho KHKK. Ô còn lại — "Duyệt thẳng Cấp cuối" (#7) — vẫn KHOÁ cho type 10 từ K3
  // S165 (service KHKK chưa port nhánh SkipToFinal, BE Create còn ép-false), xem ngay
  // tại ô đó. Lý do đầy đủ ở khối ghi chú đầu file (gần `TYPE_INT_CONTRACT_SIGNING_PLAN`).
  const initialSteps: EditStep[] = useMemo(
    () => (source ? copyFromDefinition(source) : [makeEmptyStep(1)]),
    [source],
  )

  // Panel nhóm: mã đã khoá thì ô Mã phải hiện ĐÚNG mã sẽ gửi đi, không phải mã gợi ý
  // chung của loại (`QT-KHKK-V2-001`) — hiện một đằng gửi một nẻo là kiểu bẫy tệ nhất.
  const defaultCode = lockedCode ?? DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
  const [code, setCode] = useState(source?.code ?? defaultCode)
  const [name, setName] = useState(source ? source.name : `Quy trình ${applicableTypeLabel}`)
  const [description, setDescription] = useState(source?.description ?? '')
  // S69 — Ngưỡng gói CEO (nullable). String form, '' = null. Clone giữ ngưỡng cũ.
  // [F-7 S155] Ở chế độ sửa-tại-chỗ ô này bị KHOÁ và giá trị gửi lên KHÔNG lấy từ
  // state string này (xem payload PUT) — tránh mọi sai lệch do parse/format.
  const [ceoThreshold, setCeoThreshold] = useState(
    source?.ceoApprovalThreshold != null ? String(source.ceoApprovalThreshold) : '',
  )
  // [F-7 S155] Lý do sửa → BE ghi vào changelog của CHÍNH những phiếu đang chạy
  // trên quy trình này (ApprovalWorkflowV2AdminFeatures.cs:802-818). Optional.
  const [reason, setReason] = useState('')
  // [F-7 S155] Thông điệp lỗi từ BE (nhất là 409 giải thích vì sao KHÔNG sửa tại
  // chỗ được). Toast tự tắt sau vài giây và cắt dòng — 409 của lệnh này dài và là
  // hướng dẫn hành động ("hãy tạo phiên bản MỚI") ⇒ phải giữ trên màn để đọc kỹ.
  const [serverError, setServerError] = useState<string | null>(null)
  const [steps, setSteps] = useState<EditStep[]>(initialSteps)

  // Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
  // ALL xuống per Level slot (xem EditLevelEntry, render inline mỗi Level row).
  // Admin opt-in per-Approver-slot pattern proven 3× cumulative.

  const usersList = useQuery({
    queryKey: ['users-for-approver-v2'],
    queryFn: async () =>
      (await api.get<{ items: ApproverUser[] }>('/users', {
        params: { page: 1, pageSize: 500 },
      })).data.items,
  })

  const departmentsList = useQuery({
    queryKey: ['departments-list-v2'],
    queryFn: async () =>
      (await api.get<Paged<Department>>('/departments', { params: { page: 1, pageSize: 200 } })).data.items,
  })

  const save = useMutation({
    mutationFn: async () => {
      // Validate per Bước:
      //   - Phòng required
      //   - Cấp 1 phải có ≥1 NV (sequential gating đảm bảo nếu C1 empty thì C2/C3 cũng empty)
      //   - All approver thuộc đúng Phòng (defensive double-check)
      for (const s of steps) {
        if (!s.departmentId) {
          throw new Error(`Bước "${s.name}" chưa chọn Phòng.`)
        }
        if (entriesAtLevel(s, 1).length === 0) {
          throw new Error(`Bước "${s.name}" chưa có NV ở Cấp 1.`)
        }
        // Sequential gating đã enforce ở UI nhưng kiểm tra lại
        if (entriesAtLevel(s, 2).length === 0 && entriesAtLevel(s, 3).length > 0) {
          throw new Error(`Bước "${s.name}": Cấp 3 chỉ thao tác được khi Cấp 2 có NV.`)
        }
        for (const e of s.levelEntries) {
          if (!e.approverUserId) {
            throw new Error(`Bước "${s.name}": có dòng cấp chưa chọn NV.`)
          }
          // [F-7 S155] Ràng "NV phải thuộc Phòng đã chọn" CHỈ áp cho đường tạo mới.
          // Sửa tại chỗ được phép đổi Phòng của Bước (BE xếp hạng AN TOÀN và KHÔNG
          // kiểm quan hệ NV↔Phòng — :705-706 chỉ gán DepartmentId), trong khi người
          // đang duyệt thì KHÔNG được đụng (đổi người mà đã có chữ ký ⇒ 409). Giữ
          // ràng này ở chế độ sửa sẽ chặn oan đúng thao tác BE cho phép.
          if (!isEdit) {
            const u = usersList.data?.find(x => x.id === e.approverUserId)
            if (u && u.departmentId !== s.departmentId) {
              throw new Error(`Bước "${s.name}": NV "${u.fullName}" không thuộc Phòng đã chọn.`)
            }
          }
        }
      }

      // ============ [F-7 S155] ĐƯỜNG 1 — SỬA TẠI CHỖ (PUT, giữ Id + Version) ============
      if (editTarget) {
        // Chặn TRƯỚC ở FE những thay đổi BE sẽ trả 409, để người dùng đọc được câu
        // tiếng Việt nói rõ phải làm gì thay vì nhận lỗi sau khi bấm Lưu. UI đã khoá
        // các nút ±Bước/đổi thứ tự, đây là lớp chặn thứ hai (state vẫn có thể lệch
        // nếu dữ liệu nạp vào bất thường).
        if (steps.length !== editTarget.steps.length) {
          throw new Error(
            `Sửa tại chỗ không thêm/bớt được Bước (đang có ${editTarget.steps.length}, sau khi sửa còn ${steps.length}). Hãy dùng "Tạo phiên bản mới".`,
          )
        }
        for (const s of steps) {
          const src = editTarget.steps.find(x => x.id === s.id)
          if (!s.id || !src) {
            throw new Error('Sửa tại chỗ không tạo được Bước mới. Hãy dùng "Tạo phiên bản mới".')
          }
          if (src.levels.some(l => l.order < 1 || l.order > MAX_LEVELS_PER_STEP)) {
            throw new Error(
              `Bước "${src.name}" có cấp ngoài phạm vi 1–${MAX_LEVELS_PER_STEP} nên màn này không hiển thị đủ. Không sửa tại chỗ được — hãy dùng "Tạo phiên bản mới".`,
            )
          }
          // `src.levels[].order` từ DTO là `number`, còn `levelEntries[].order` là
          // `LevelOrder` (1|2|3) ⇒ ép cả hai Set về `number` để so sánh được.
          const srcOrders = new Set<number>(src.levels.map(l => l.order))
          const curOrders = new Set<number>(s.levelEntries.map(e => e.order))
          const lost = [...srcOrders].filter(o => !curOrders.has(o)).sort()
          if (lost.length > 0) {
            throw new Error(
              `Bước "${s.name}": Cấp ${lost.join(', ')} sẽ không còn ai duyệt — phiếu đang chờ ở cấp đó sẽ KẸT. Hãy giữ ít nhất 1 người ở mỗi Cấp, hoặc dùng "Tạo phiên bản mới".`,
            )
          }
          const added = [...curOrders].filter(o => !srcOrders.has(o)).sort()
          if (added.length > 0) {
            throw new Error(
              `Bước "${s.name}": sửa tại chỗ không thêm được Cấp mới (Cấp ${added.join(', ')}). Hãy dùng "Tạo phiên bản mới".`,
            )
          }
        }

        await api.put(`/approval-workflows-v2/${editTarget.id}`, {
          id: editTarget.id,
          name,
          description: description || null,
          // Lý do sửa → changelog phiếu đang chạy. Rỗng = không gửi.
          reason: reason.trim() === '' ? null : reason.trim(),
          // ECHO (null = BE bỏ qua, nhưng gửi đúng vẫn tốt hơn: lệch ⇒ 409 sớm).
          code: editTarget.code,
          applicableType,
          // 🔴 ECHO NGHIÊM (ApprovalWorkflowV2AdminFeatures.cs:529 so sánh BẰNG,
          // KHÔNG có "null = bỏ qua"): thiếu/sai ⇒ 409. Lấy THẲNG số từ DTO vừa
          // GET về — KHÔNG đi qua state string `ceoThreshold` để loại mọi rủi ro
          // parse/format ('' → null, dấu phân cách nghìn, làm tròn).
          ceoApprovalThreshold: editTarget.ceoApprovalThreshold,
          steps: steps.map(s => ({
            id: s.id,
            // Order gốc, KHÔNG phải index: BE bắt `reqStep.Order == dbStep.Order` (:554).
            order: s.order,
            name: s.name,
            departmentId: s.departmentId,
            levels: s.levelEntries.map(e => ({
              // null = dòng MỚI (thêm người vào Cấp có sẵn). Có Id = sửa dòng cũ.
              id: e.id ?? null,
              order: e.order,
              name: `Cấp ${e.order}`,
              approverUserId: e.approverUserId,
              // 9 cờ qua ĐÚNG 1 hàm dùng chung với POST — xem levelFlagsPayload().
              ...levelFlagsPayload(e),
            })),
          })),
        })
        return
      }

      // ============ ĐƯỜNG 2 — TẠO PHIÊN BẢN MỚI (POST, sinh Id mới) ============
      await api.post('/approval-workflows-v2', {
        applicableType,
        // [W2c S182] Panel nhóm: mã lấy từ prop khoá, KHÔNG lấy từ state ô nhập.
        // Ô nhập đang disabled nên state không đổi được qua UI — nhưng gửi thẳng
        // `lockedCode` là bất-biến theo CẤU TRÚC, không phụ thuộc kỷ luật của ô input.
        code: lockedCode ?? code,
        name,
        description: description || null,
        // S69 — ngưỡng gói CEO: '' → null (không áp ngưỡng), else parse số.
        // [W2f S182] KHKK KHÔNG dùng ngưỡng máy (người được uỷ quyền quyết lên CEO hay
        // không, qua ô "Duyệt là KẾT THÚC") ⇒ gửi thẳng null, không để giá trị lỡ tay
        // nằm lại trong state rồi âm thầm bật một nhánh rẽ mà quy trình KHKK không chạy.
        ceoApprovalThreshold: isKhkk
          ? null
          : ceoThreshold.trim() === '' ? null : Number(ceoThreshold.replace(/[^\d]/g, '')),
        steps: steps.map((s, i) => ({
          order: i + 1,
          name: s.name,
          departmentId: s.departmentId,
          // Mỗi entry → 1 Level row. Multiple rows cùng Order = same Cấp với
          // N approvers (BE iterate group by Order).
          // Mig 29 (S21 t5) — 5 Allow* options per slot Approver.
          // Mig 30 (S22+5) — +AllowApproverEditBudget.
          // Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 storage swap per-slot.
          levels: s.levelEntries.map(e => ({
            order: e.order,
            name: `Cấp ${e.order}`,
            approverUserId: e.approverUserId,
            // [F6 S155 → F-7 S155] 🔴 DÂY DỄ QUÊN NHẤT: payload liệt kê tay từng
            // field (KHÔNG spread `...e`) ⇒ thiếu 1 cờ thì TS vẫn build sạch, ô tick
            // vẫn hiện, chỉ BE không bao giờ nhận. Từ khi có call-site thứ hai (PUT),
            // 9 cờ gom vào levelFlagsPayload() dùng CHUNG — thêm cờ mới chỉ sửa 1 nơi.
            ...levelFlagsPayload(e),
          })),
        })),
      })
    },
    onSuccess: () => {
      setServerError(null)
      toast.success(
        isEdit
          ? `Đã lưu thay đổi trên ${editTarget!.code} v${editTarget!.version} — GIỮ NGUYÊN phiên bản. Các phiếu đang chạy trên quy trình này áp dụng NGAY.`
          : 'Đã lưu quy trình mới. Version cũ đã archive. Lưu ý: phiếu ĐANG chạy vẫn theo bản cũ.',
      )
      onSaved()
    },
    onError: err => {
      // 🔴 KHÔNG nuốt thông điệp BE. 409 của lệnh sửa-tại-chỗ giải thích chính xác
      // thay đổi nào phạm luật + bảo phải làm gì ("tạo phiên bản MỚI") ⇒ giữ lại
      // trên màn (banner) chứ không chỉ toast thoáng qua.
      const msg = getErrorMessage(err)
      setServerError(msg)
      toast.error(msg)
    },
  })

  function submit(e: FormEvent) {
    e.preventDefault()
    setServerError(null)
    if (steps.length === 0) {
      toast.error('Phải có ít nhất 1 bước')
      return
    }
    save.mutate()
  }

  function moveStep(idx: number, dir: -1 | 1) {
    const newIdx = idx + dir
    if (newIdx < 0 || newIdx >= steps.length) return
    const next = [...steps]
    ;[next[idx], next[newIdx]] = [next[newIdx], next[idx]]
    setSteps(next)
  }

  return (
    <Dialog
      open
      onClose={onClose}
      title={
        isEdit
          ? `Sửa tại chỗ — ${editTarget!.code} v${String(editTarget!.version).padStart(2, '0')} (giữ nguyên phiên bản)`
          : `Tạo quy trình mới — ${applicableTypeLabel}`
      }
      size="lg"
      footer={
        <>
          <Button variant="outline" onClick={onClose}>Hủy</Button>
          <Button onClick={submit} disabled={save.isPending} form="aw-v2-form">
            {save.isPending
              ? 'Đang lưu…'
              : isEdit ? 'Lưu thay đổi (giữ nguyên phiên bản)' : 'Lưu + kích hoạt'}
          </Button>
        </>
      }
    >
      <form id="aw-v2-form" onSubmit={submit} className="space-y-4">
        {/* [F-7 S155] Banner phân biệt HAI đường — người dùng phải hiểu hệ quả
            khác nhau TRƯỚC khi sửa, không phải sau khi bấm Lưu. */}
        {isEdit ? (
          <div className="rounded-lg border border-emerald-300 bg-emerald-50 px-3 py-2 text-[11.5px] leading-relaxed text-emerald-900">
            <div className="font-semibold">Đang SỬA TẠI CHỖ phiên bản {editTarget!.code} v{String(editTarget!.version).padStart(2, '0')} — không tạo phiên bản mới.</div>
            <div className="mt-1">
              Lưu xong, thay đổi áp dụng <span className="font-semibold">NGAY</span> cho cả những phiếu <span className="font-semibold">đang chạy</span> trên quy trình này
              (kể cả phiếu đang chờ duyệt) — đây là cách duy nhất để cờ quyền vừa tick tới được phiếu đã gửi đi.
            </div>
            <div className="mt-1">
              Sửa được: thêm/bớt người trong Cấp <span className="font-semibold">có sẵn</span> · đổi người duyệt · các ô quyền duyệt (gồm quyền XÓA phiếu) · tên hiển thị · Phòng của Bước.
              <br />
              Muốn thêm/bớt Bước hoặc Cấp, đổi thứ tự, đổi Mã / Ngưỡng CEO / cờ &quot;Duyệt là KẾT THÚC&quot; / &quot;Duyệt thẳng Cấp cuối&quot; ⇒ phải bấm <span className="font-semibold">&quot;Tạo phiên bản mới&quot;</span>.
            </div>
          </div>
        ) : (
          <div className="rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-[11.5px] leading-relaxed text-amber-900">
            <div className="font-semibold">Đang TẠO PHIÊN BẢN MỚI — số phiên bản sẽ tăng, bản hiện tại chuyển sang lưu trữ.</div>
            <div className="mt-1">
              Phiếu <span className="font-semibold">đang chạy vẫn theo bản cũ</span> (phiếu gắn chặt vào phiên bản lúc tạo). Chỉ phiếu tạo SAU khi lưu mới theo bản này.
              Nếu chỉ muốn thêm người / đổi quyền cho phiếu đang treo, hãy đóng lại và bấm <span className="font-semibold">&quot;Sửa tại chỗ&quot;</span>.
            </div>
          </div>
        )}

        {/* [F-7 S155] Thông điệp từ máy chủ — nhất là 409 "không sửa tại chỗ được vì…".
            Giữ trên màn (không chỉ toast) để đọc kỹ và làm theo hướng dẫn. */}
        {serverError && (
          <div className="flex items-start gap-2 rounded-lg border border-rose-300 bg-rose-50 px-3 py-2 text-[11.5px] leading-relaxed text-rose-900">
            <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
            <div className="whitespace-pre-wrap">{serverError}</div>
          </div>
        )}

        <div className="grid grid-cols-2 gap-3">
          <div className="space-y-1.5">
            <Label>Mã quy trình *</Label>
            <Input
              value={code}
              onChange={e => setCode(e.target.value)}
              required
              className="font-mono"
              // Mã là khoá của chuỗi phiên bản ⇒ sửa tại chỗ KHÔNG đổi được (BE :521 → 409).
              // [W2c S182] Panel nhóm KHKK cũng khoá: đổi mã ở đây = đẻ quy trình mồ côi
              // (phiếu của nhóm vẫn đi tìm đúng `KHKK-N{n}`).
              disabled={isEdit || lockedCode !== null}
            />
            <div className="text-[11px] text-slate-400">
              {isEdit
                ? 'Không đổi được khi sửa tại chỗ — mã là khoá của chuỗi phiên bản.'
                : lockedCode !== null
                  ? 'Mã khoá theo nhóm — version tự tăng.'
                  : 'Vd QT-DN-V2-001. Version auto-tăng mỗi lần lưu.'}
            </div>
          </div>
          <div className="space-y-1.5">
            <Label>Tên hiển thị *</Label>
            <Input value={name} onChange={e => setName(e.target.value)} required />
          </div>
          <div className="col-span-2 space-y-1.5">
            <Label>Mô tả</Label>
            <Textarea rows={2} value={description} onChange={e => setDescription(e.target.value)} />
          </div>
          {/* S69 — Ngưỡng giá trị gói CEO (nullable). Empty = không áp ngưỡng. */}
          <div className="col-span-2 space-y-1.5">
            <Label>Ngưỡng giá trị gói CEO (đ)</Label>
            <Input
              inputMode="numeric"
              value={ceoThreshold === '' ? '' : Number(ceoThreshold.replace(/[^\d]/g, '')).toLocaleString('vi-VN')}
              onChange={e => setCeoThreshold(e.target.value.replace(/[^\d]/g, ''))}
              placeholder="Để trống = luôn theo đủ quy trình"
              // 🔴 Sửa tại chỗ KHÔNG đổi được ngưỡng: phiếu đang chờ sẽ đổi nhánh
              // cần/không-cần CEO giữa chừng (BE :529 → 409). Khoá ô để người dùng
              // không gõ vào rồi mới nhận lỗi. Giá trị gửi lên lấy thẳng từ bản đang lưu.
              // [W2f S182] KHKK khoá VÔ ĐIỀU KIỆN (kể cả tạo mới): quy trình KHKK không
              // rẽ nhánh theo số tiền — người được uỷ quyền quyết. Tiền lệ cùng cách làm
              // là ô "Duyệt thẳng Cấp cuối" bên dưới.
              disabled={isEdit || isKhkk}
            />
            <div className="text-[11px] leading-relaxed text-slate-400">
              {isKhkk
                ? 'KHKK không dùng ngưỡng máy — NĐUQ quyết. Ai được kết thúc quy trình thì tick ô "Duyệt là KẾT THÚC" ở đúng NV đó bên dưới.'
                : isEdit
                  ? 'Không đổi được khi sửa tại chỗ — đổi ngưỡng làm phiếu đang chờ nhảy nhánh cần/không-cần CEO giữa chừng. Muốn đổi: tạo phiên bản mới.'
                  : 'Gói < ngưỡng: CCM duyệt là xong, không cần CEO. ≥ ngưỡng: phải lên CEO. Để trống = luôn theo đủ quy trình.'}
            </div>
          </div>

          {/* [F-7 S155] Lý do sửa — BE ghi vào changelog của chính những phiếu đang
              chạy trên quy trình này, để người đọc lịch sử phiếu hiểu vì sao quy
              trình đổi giữa chừng (ApprovalWorkflowV2AdminFeatures.cs:802-818). */}
          {isEdit && (
            <div className="col-span-2 space-y-1.5">
              <Label>Lý do sửa (ghi vào lịch sử phiếu đang chạy)</Label>
              <Textarea
                rows={2}
                maxLength={500}
                value={reason}
                onChange={e => setReason(e.target.value)}
                placeholder="Vd: Bổ sung quyền xóa phiếu cho Trưởng phòng Mua hàng theo yêu cầu ngày 27/07."
              />
            </div>
          )}
        </div>

        {/* Mig 29 (S21 t5) — 5 Allow* F1+F3 per slot Approver.
            Mig 30 (S22+5) — +AllowApproverEditBudget per slot.
            Mig 31 (S23 t1) — F2 storage swap Users→Level: per-Approver-slot.
            ALL Allow* options now configured PER NV trong slot Approver dưới đây. */}
        <div className="rounded-lg border border-violet-200 bg-violet-50/30 px-3 py-2 text-[11px] leading-relaxed text-violet-800">
          ⓘ Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới
          (Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối).
          {/* [W2d S182 — D3=C] Nói thẳng lý do các ô bị mờ, thay vì để admin tự đoán
              là "mất quyền" hay "lỗi". Ô mờ vẫn hiện ĐÚNG giá trị đang lưu trong DB. */}
          {isKhkk && (
            <div className="mt-1 text-violet-900">
              Riêng Kế hoạch ký kết HĐ: một số ô bị khoá vì luồng duyệt KHKK chưa dùng tới
              (ô vẫn hiển thị đúng giá trị đang lưu, không bị đổi). Hai ô đang có tác dụng
              thật là <span className="font-semibold">&quot;Trả về Drafter&quot;</span> (KHKK luôn trả về
              người soạn) và <span className="font-semibold">&quot;Duyệt là KẾT THÚC&quot;</span> — tick cho NV
              được uỷ quyền quyết dừng hay trình tiếp CEO.
            </div>
          )}
        </div>

        <div className="space-y-2 rounded-lg border border-slate-200 p-3">
          <div className="flex items-center justify-between">
            <Label>
              Các bước duyệt — mỗi bước = 1 Phòng × <span className="font-bold text-violet-700">tối đa {MAX_LEVELS_PER_STEP} cấp NV</span> ({steps.length} bước)
            </Label>
            <Button
              type="button"
              size="sm"
              variant="outline"
              onClick={() => setSteps([...steps, makeEmptyStep(steps.length + 1)])}
            >
              <Plus className="h-3.5 w-3.5" />
              Thêm bước
            </Button>
          </div>

          {steps.map((s, idx) => (
            <div key={idx} className="rounded-md border border-slate-200 bg-slate-50/40 p-3">
              <div className="flex items-start gap-2">
                <div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand-600 text-[11px] font-bold text-white">
                  {idx + 1}
                </div>
                <div className="grid flex-1 grid-cols-3 gap-2">
                  <div>
                    <Label className="text-[11px]">Tên bước</Label>
                    <Input
                      value={s.name}
                      onChange={e => setSteps(steps.map((x, i) => (i === idx ? { ...x, name: e.target.value } : x)))}
                      placeholder="Phòng A"
                    />
                  </div>
                  <div className="col-span-2">
                    <Label className="text-[11px]">Phòng * (chọn để filter NV duyệt)</Label>
                    <Select
                      value={s.departmentId ?? ''}
                      onChange={e => {
                        // Đổi Phòng → clear hết approvers vì NV cũ có thể không thuộc Phòng mới.
                        const newDeptId = e.target.value || null
                        setSteps(steps.map((x, i) => (i === idx
                          ? { ...x, departmentId: newDeptId, levelEntries: [] }
                          : x)))
                      }}
                      required
                    >
                      <option value="">— Chọn Phòng —</option>
                      {departmentsList.data?.map(d => (
                        <option key={d.id} value={d.id}>{d.name}</option>
                      ))}
                    </Select>
                  </div>
                </div>
                <div className="flex flex-col gap-0.5">
                  <button
                    type="button"
                    onClick={() => moveStep(idx, -1)}
                    disabled={idx === 0}
                    className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
                    title="Lên"
                  >
                    <ChevronUp className="h-3.5 w-3.5" />
                  </button>
                  <button
                    type="button"
                    onClick={() => moveStep(idx, 1)}
                    disabled={idx === steps.length - 1}
                    className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
                    title="Xuống"
                  >
                    <ChevronDown className="h-3.5 w-3.5" />
                  </button>
                  <button
                    type="button"
                    onClick={() => setSteps(steps.filter((_, i) => i !== idx))}
                    className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-red-50 hover:text-red-600"
                    title="Xóa bước"
                  >
                    <Trash2 className="h-3.5 w-3.5" />
                  </button>
                </div>
              </div>

              {/* Levels — 3 section cố định C1/C2/C3. Mỗi cấp có N NV.
                  Sequential gating: Cấp k chỉ active khi Cấp k-1 có ≥1 NV. */}
              <div className="mt-2 ml-9 space-y-2 border-l-2 border-violet-200 pl-3">
                {!s.departmentId && (
                  <div className="rounded bg-amber-50 px-2 py-1 text-[11px] font-medium text-amber-700">
                    ⚠ Chọn Phòng để bắt đầu cấu hình cấp duyệt.
                  </div>
                )}
                {s.departmentId && usersForDept(usersList.data, s.departmentId).length === 0 && (
                  <div className="rounded border border-dashed border-amber-300 bg-amber-50 px-2 py-1.5 text-[11px] italic text-amber-700">
                    ⚠ Phòng này chưa có nhân viên. Vào /system/users gán NV vào Phòng trước, sau đó quay lại.
                  </div>
                )}

                {LEVEL_ORDERS.map(order => {
                  const entries = entriesAtLevel(s, order)
                  const enabled = isLevelEnabled(s, order)
                  const filteredUsers = usersForDept(usersList.data, s.departmentId)
                  // NV còn khả dụng (chưa được dùng ở cấp này — cùng cấp không trùng NV)
                  const usedInThisLevel = new Set(entries.map(e => e.approverUserId))
                  const availableUsers = filteredUsers.filter(u => !usedInThisLevel.has(u.id))

                  // Disable Add: Phòng chưa chọn / cấp trước chưa có NV / hết NV available
                  const addDisabled = !enabled || availableUsers.length === 0

                  return (
                    <div
                      key={order}
                      className={`rounded-md border p-2 ${
                        enabled
                          ? 'border-violet-200 bg-white'
                          : 'border-slate-200 bg-slate-50/50 opacity-60'
                      }`}
                    >
                      <div className="flex items-center justify-between">
                        <div className="flex items-center gap-2">
                          <span className="rounded-full bg-violet-100 px-2 py-0.5 font-mono text-[10px] font-bold text-violet-700">
                            Cấp {order}
                          </span>
                          <span className="text-[11px] text-slate-500">
                            {entries.length === 0
                              ? (enabled ? 'Chưa có NV' : `Hoàn tất Cấp ${order - 1} trước`)
                              : `${entries.length} NV duyệt`}
                          </span>
                        </div>
                        <button
                          type="button"
                          disabled={addDisabled}
                          onClick={() => {
                            if (availableUsers.length === 0) return
                            const firstUser = availableUsers[0]
                            setSteps(steps.map((x, i) =>
                              i === idx
                                ? { ...x, levelEntries: [...x.levelEntries, makeDefaultLevelEntry(order, firstUser.id)] }
                                : x,
                            ))
                          }}
                          className="rounded bg-violet-50 px-2 py-1 text-[11px] font-medium text-violet-700 hover:bg-violet-100 disabled:cursor-not-allowed disabled:opacity-40"
                          title={
                            !enabled
                              ? `Cấp ${order - 1} phải có ≥1 NV trước`
                              : availableUsers.length === 0
                                ? 'Hết NV khả dụng (đã thêm hết hoặc Phòng không còn NV)'
                                : 'Thêm NV duyệt vào cấp này'
                          }
                        >
                          + Thêm NV
                        </button>
                      </div>

                      {entries.length > 0 && (
                        <div className="mt-1.5 space-y-1">
                          {entries.map((entry, ei) => {
                            // Tính index trong levelEntries gốc để update
                            const globalIdx = s.levelEntries.findIndex(
                              x => x === entry,
                            )
                            // NV available cho dropdown: filtered + chính NV đang chọn (giữ option hiện tại)
                            const dropdownUsers = filteredUsers.filter(
                              u => u.id === entry.approverUserId || !usedInThisLevel.has(u.id),
                            )
                            return (
                              <div key={ei} className="flex items-center gap-1.5">
                                <span className="text-[10px] text-slate-400">#{ei + 1}</span>
                                <Select
                                  value={entry.approverUserId}
                                  onChange={e => {
                                    const newId = e.target.value
                                    setSteps(steps.map((x, i) =>
                                      i === idx
                                        ? {
                                            ...x,
                                            levelEntries: x.levelEntries.map((y, j) =>
                                              j === globalIdx ? { ...y, approverUserId: newId } : y,
                                            ),
                                          }
                                        : x,
                                    ))
                                  }}
                                  className="h-7 flex-1 text-xs"
                                >
                                  {dropdownUsers.map(u => (
                                    <option key={u.id} value={u.id}>
                                      {u.fullName} ({u.email})
                                    </option>
                                  ))}
                                </Select>
                                <button
                                  type="button"
                                  onClick={() => {
                                    // Chặn xóa NV cuối Cấp 1 nếu Cấp 2/3 còn entries (gating)
                                    if (entries.length === 1) {
                                      const nextLevelHasEntries =
                                        order < MAX_LEVELS_PER_STEP &&
                                        entriesAtLevel(s, (order + 1) as LevelOrder).length > 0
                                      if (nextLevelHasEntries) {
                                        toast.error(
                                          `Hãy xóa hết NV ở Cấp ${order + 1} trước khi rỗng Cấp ${order}.`,
                                        )
                                        return
                                      }
                                    }
                                    setSteps(steps.map((x, i) =>
                                      i === idx
                                        ? { ...x, levelEntries: x.levelEntries.filter((_, j) => j !== globalIdx) }
                                        : x,
                                    ))
                                  }}
                                  className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-red-50 hover:text-red-600"
                                  title="Xóa NV khỏi cấp này"
                                >
                                  <Trash2 className="h-3 w-3" />
                                </button>
                              </div>
                            )
                          })}
                          {/* Mig 29 (S21 t5) — 5 Allow* checkbox inline cho mỗi
                              NV entry. Mặc định AllowReturnToDrafter=true (S17
                              backward compat). Admin tick mở mode khác per slot. */}
                          {entries.map((entry, ei) => {
                            const globalIdx = s.levelEntries.findIndex(x => x === entry)
                            const updateField = (field: keyof EditLevelEntry, value: boolean) => {
                              setSteps(steps.map((x, i) =>
                                i === idx
                                  ? {
                                      ...x,
                                      levelEntries: x.levelEntries.map((y, j) =>
                                        j === globalIdx ? { ...y, [field]: value } : y,
                                      ),
                                    }
                                  : x,
                              ))
                            }
                            return (
                              <div
                                key={`opts-${ei}`}
                                className="ml-4 mt-1 rounded border border-amber-100 bg-amber-50/30 px-2 py-1.5"
                              >
                                <div className="mb-1 text-[10px] font-medium uppercase text-amber-700">
                                  Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}
                                </div>
                                {/* [W2d S182 — D3=C] Với KHKK (type 10, gồm 8 panel nhóm) các ô
                                    dưới đây bị KHOÁ vì luồng KHKK chưa chạy tới chúng.
                                    🔴 HAI điều KHÔNG được làm, vì đã có tiền lệ hỏng:
                                      (1) KHÔNG ép `checked={false}`. Ô phải nói đúng thứ ĐANG NẰM
                                          TRONG DB — bản seed có cờ nào bật thì phải thấy nó bật,
                                          nếu không admin sẽ tưởng cờ đã tắt trong khi BE vẫn đọc.
                                      (2) KHÔNG động vào payload (`levelFlagsPayload`): nó tiếp tục
                                          echo NGUYÊN giá trị đang lưu. Khoá là chuyện MÀN HÌNH,
                                          không phải chuyện dữ liệu — khoá bằng cách ghi đè dữ liệu
                                          thì mở lại sau này không còn gì để mở.
                                    Ô nào KHKK có chạy thì để nguyên: "Trả về Drafter" (hành vi thật
                                    là LUÔN trả về người soạn) và "Duyệt là KẾT THÚC" (#8, sống). */}
                                <div className="grid grid-cols-2 gap-1">
                                  <label
                                    className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowReturnOneLevel}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowReturnOneLevel', e.target.checked)}
                                    />
                                    <span>Trả về 1 Cấp trước</span>
                                  </label>
                                  <label
                                    className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowReturnOneStep}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowReturnOneStep', e.target.checked)}
                                    />
                                    <span>Trả về 1 Bước trước</span>
                                  </label>
                                  <label
                                    className={`flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowReturnToAssignee}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowReturnToAssignee', e.target.checked)}
                                    />
                                    <span>Trả về Người chỉ định</span>
                                  </label>
                                  {/* [W2d S182] Ô này KHÁC 6 ô kia: KHKK có chạy nhánh trả-về-người-soạn,
                                      và chạy KHÔNG ĐIỀU KIỆN — cờ chưa được phân nhánh ở service nên bỏ
                                      tick cũng không tắt được. Vì vậy khoá nhưng KHÔNG làm mờ kiểu
                                      "chưa có": mờ ở đây sẽ đọc thành "tính năng chưa chạy", ngược hẳn
                                      sự thật. Câu nhắc viết riêng, không dùng câu chung. */}
                                  <label
                                    className="flex items-center gap-1 text-[11px] text-slate-700"
                                    title={isKhkk ? khkkDrafterHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowReturnToDrafter}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowReturnToDrafter', e.target.checked)}
                                    />
                                    <span>Trả về Drafter (mặc định)</span>
                                  </label>
                                  {/* [W2d S182 — vá MAJOR-1] Câu này là thứ DUY NHẤT phân biệt ô trên
                                      với 6 ô "chưa áp dụng" cùng khối, nên nó phải NHÌN THẤY ĐƯỢC chứ
                                      không nằm sau một cú rê chuột. `col-start-2` để dòng chữ rơi đúng
                                      dưới ô của nó (cột phải), không bị đọc nhầm sang ô bên trái. */}
                                  {isKhkk && (
                                    <div className="col-start-2 -mt-0.5 pl-4 text-[10px] leading-snug text-slate-500">
                                      {khkkDrafterHint}
                                    </div>
                                  )}
                                  <label
                                    className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowApproverEditDetails}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowApproverEditDetails', e.target.checked)}
                                    />
                                    <span>Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá) lúc đang duyệt</span>
                                  </label>
                                  <label
                                    className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowApproverEditBudget}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowApproverEditBudget', e.target.checked)}
                                    />
                                    <span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
                                  </label>
                                  {/* Mig 31 (S23 t1) — F2 AllowApproverSkipToFinal admin opt-in per-slot.
                                      Approver tick = skip thẳng Cấp cuối khi đang ChoDuyet.
                                      [K3 S165 — gate-K3 F-2] KHKK (type-10) CHỈ mở ô "KẾT THÚC" (dưới):
                                      service KHKK CHƯA port nhánh SkipToFinal + BE CREATE vẫn ép-false cờ
                                      này cho type-10 ⇒ ô này DISABLE cho KHKK kẻo tick bị nuốt im lặng
                                      (class "UI nói một đằng DB một nẻo" — đúng thứ W1 S161 cảnh báo). */}
                                  {/* [W2d S182] GIỮ NGUYÊN hành vi ô này (kể cả `checked` ép false —
                                      khác 6 ô trên, đây là quyết định cũ của K3 S165 và BE Create
                                      ĐANG THẬT SỰ ép-false cờ này cho type 10, nên màn hình đang
                                      khớp DB). Chỉ đổi 3 biểu thức trùng lặp sang cờ dùng chung
                                      `isKhkk` — giá trị y hệt, để 8 chỗ khoá không lệch nhau. */}
                                  <label
                                    className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-slate-700'}`}
                                    title={isKhkk ? 'Kế hoạch ký kết chưa hỗ trợ duyệt vượt cấp — service chưa port nhánh này (K3)' : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={isKhkk ? false : entry.allowApproverSkipToFinal}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowApproverSkipToFinal', e.target.checked)}
                                    />
                                    <span>Cho phép duyệt thẳng Cấp cuối khi đang duyệt</span>
                                  </label>
                                  {/* [Mig 58 CCM item 4 — anh Kiệt FDC] Duyệt là KẾT THÚC: NV slot
                                      này duyệt xong → DaDuyet luôn, KHÔNG trình tiếp CEO (vd Cấp anh Chương).
                                      [K3 S164] Với Kế hoạch ký kết HĐ đây CHÍNH LÀ ô owner cần: tick cho
                                      Cấp TP.CCM (anh Chương) ⇒ anh ấy quyết kết thúc hay trình tiếp CEO. */}
                                  <label className="col-span-2 flex items-center gap-1 text-[11px] text-emerald-700">
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowApproverFinalize}
                                      onChange={e => updateField('allowApproverFinalize', e.target.checked)}
                                    />
                                    <span>Duyệt là KẾT THÚC quy trình (không trình tiếp CEO)</span>
                                  </label>
                                  {/* [F6 S155 — quyền xóa phiếu đang duyệt] PER-NGƯỜI, KHÔNG
                                      per-Cấp: 1 dòng NV = 1 ô tick riêng. Tick cho NV nào thì
                                      CHỈ NV đó xóa được, và chỉ khi phiếu đang Chờ duyệt ĐÚNG
                                      lượt của họ. Cấp có 3 NV mà tick 1 ⇒ vẫn chỉ 1 người xóa. */}
                                  <label
                                    className={`col-span-2 flex items-center gap-1 text-[11px] ${isKhkk ? 'text-slate-400' : 'text-rose-700'}`}
                                    title={isKhkk ? khkkNotYetHint : undefined}
                                  >
                                    <input
                                      type="checkbox"
                                      className="h-3 w-3"
                                      checked={entry.allowApproverDelete}
                                      disabled={isKhkk}
                                      onChange={e => updateField('allowApproverDelete', e.target.checked)}
                                    />
                                    <span>Cho phép NV này XÓA phiếu khi đến lượt duyệt của mình</span>
                                  </label>
                                </div>
                              </div>
                            )
                          })}
                        </div>
                      )}
                    </div>
                  )
                })}
              </div>
            </div>
          ))}
        </div>

        <div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">
          <GitBranch className="mt-0.5 h-3.5 w-3.5 shrink-0" />
          <div>
            Quy tắc: mỗi Bước có 1 Phòng + tối đa {MAX_LEVELS_PER_STEP} Cấp. Quy trình chạy theo số cấp thật sự có
            (1 / 2 / 3 cấp đều OK). Trong cùng 1 Cấp có thể có nhiều NV — chỉ cần 1 NV duyệt là cấp đó pass
            (OR-of-N). Tuần tự: Cấp 1 → Cấp 2 → Cấp 3 → Bước kế. Hết tất cả Bước = Đã duyệt.
          </div>
        </div>
      </form>
    </Dialog>
  )
}
