[CLAUDE] FE-Admin: S21 t5 Chunk B — Designer move 5 checkbox xuống per-Level slot

ApprovalWorkflowsV2Page.tsx refactor Designer modal theo Mig 29 per-NV:

Types update:
- `LevelDto` +5 Allow* (mirror BE AwLevelDto)
- `DefinitionDto` REMOVE 6 workflow-level Allow* (no longer used)
- `EditLevelEntry` +5 Allow* (form state per slot entry)
- `makeDefaultLevelEntry(order, userId)` helper — 4 false + AllowReturnToDrafter
  true (S17 backward compat)
- `copyFromDefinition` propagate 5 Allow* từ existing Levels

Form state:
- REMOVE 6 useState workflow-level (allowReturnOneLevel...allowApproverEditDetails)
- POST body remove 6 workflow-level field
- POST body levels[].* propagate 5 Allow* per slot

UI refactor:
- REMOVE entire section "Cấu hình nâng cao" workflow-level (amber bg 6 checkbox)
- REPLACE với info banner violet ngắn "ⓘ Cấu hình quyền duyệt riêng cho từng NV
  ở mỗi Cấp dưới đây. F2 cấu hình ở User Management."
- Mỗi Level entry (NV row) ADD inline panel amber-50/30 5 checkbox grid-cols-2:
  - Trả về 1 Cấp trước
  - Trả về 1 Bước trước
  - Trả về Người chỉ định
  - Trả về Drafter (mặc định checked)
  - Cho phép chỉnh sửa Section 2 (col-span-2, full row)
- Header "Quyền duyệt NV #N" [10px] uppercase amber-700
- `updateField()` helper inline update per entry index

F2 (AllowDrafterSkipToFinal) cần UX riêng ở User Management page (per-Drafter
user global). Defer Chunk B Plus hoặc commit sau khi user UAT request.

Verify:
- npm run build fe-admin pass 498ms cached
- 0 TS6 err, warning chunk size pre-existing

Pending Chunk C: FE eOffice (PeWorkflowPanel + PeDetailTabs) read
`evaluation.currentLevelOptions` + `evaluation.drafterAllowSkipToFinal` thay vì
`workflowOptions`. Mirror 2 app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 20:07:10 +07:00
parent 036694638e
commit 63234b2cce

View File

@ -41,6 +41,12 @@ type LevelDto = {
approverUserId: string
approverUserName: string | null
approverEmail: string | null
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
}
type StepDto = {
id: string
@ -60,13 +66,9 @@ type DefinitionDto = {
description: string | null
isActive: boolean
isUserSelectable: boolean // Mig 25 — admin toggle cho user pick
// Mig 28 (S21 t4) — 6 advanced options per workflow version
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean // default true backward compat S17
allowDrafterSkipToFinal: boolean
allowApproverEditDetails: boolean
// Mig 29 (S21 t5) — 6 Allow* options MOVED:
// - 5 flag F1+F3 xuống per slot Level (xem LevelDto)
// - 1 flag F2 AllowDrafterSkipToFinal xuống per User (User Management)
activatedAt: string | null
createdAt: string
steps: StepDto[]
@ -79,7 +81,17 @@ type TypeSummaryDto = {
}
type LevelOrder = 1 | 2 | 3
type EditLevelEntry = { order: LevelOrder; approverUserId: string }
type EditLevelEntry = {
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).
allowReturnOneLevel: boolean
allowReturnOneStep: boolean
allowReturnToAssignee: boolean
allowReturnToDrafter: boolean
allowApproverEditDetails: boolean
}
type EditStep = { name: string; departmentId: string | null; levelEntries: EditLevelEntry[] }
type ApproverUser = { id: string; fullName: string; email: string; departmentId: string | null }
@ -110,16 +122,39 @@ function makeEmptyStep(stepNo: number, deptId: string | null = null): EditStep {
}
// 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 => ({
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 })),
.map(l => ({
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,
})),
}))
}
// 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,
}
}
// 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 []
@ -452,14 +487,9 @@ function Designer({
const [description, setDescription] = useState(cloneFrom?.description ?? '')
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
// Mig 28 (S21 t4) — 6 advanced options. Default clone từ cloneFrom (giữ
// config version trước) hoặc backward compat S17 (chỉ Drafter mode).
const [allowReturnOneLevel, setAllowReturnOneLevel] = useState(cloneFrom?.allowReturnOneLevel ?? false)
const [allowReturnOneStep, setAllowReturnOneStep] = useState(cloneFrom?.allowReturnOneStep ?? false)
const [allowReturnToAssignee, setAllowReturnToAssignee] = useState(cloneFrom?.allowReturnToAssignee ?? false)
const [allowReturnToDrafter, setAllowReturnToDrafter] = useState(cloneFrom?.allowReturnToDrafter ?? true)
const [allowDrafterSkipToFinal, setAllowDrafterSkipToFinal] = useState(cloneFrom?.allowDrafterSkipToFinal ?? false)
const [allowApproverEditDetails, setAllowApproverEditDetails] = useState(cloneFrom?.allowApproverEditDetails ?? false)
// Mig 29 (S21 t5) — 6 Allow* options MOVED:
// - 5 flag F1+F3 xuống per Level slot (xem EditLevelEntry, render mỗi Level row)
// - 1 flag F2 AllowDrafterSkipToFinal xuống per User (User Management page)
const usersList = useQuery({
queryKey: ['users-for-approver-v2'],
@ -513,19 +543,18 @@ function Designer({
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.
levels: s.levelEntries.map(e => ({
order: e.order,
name: `Cấp ${e.order}`,
approverUserId: e.approverUserId,
allowReturnOneLevel: e.allowReturnOneLevel,
allowReturnOneStep: e.allowReturnOneStep,
allowReturnToAssignee: e.allowReturnToAssignee,
allowReturnToDrafter: e.allowReturnToDrafter,
allowApproverEditDetails: e.allowApproverEditDetails,
})),
})),
// Mig 28 (S21 t4) — 6 advanced options
allowReturnOneLevel,
allowReturnOneStep,
allowReturnToAssignee,
allowReturnToDrafter,
allowDrafterSkipToFinal,
allowApproverEditDetails,
})
},
onSuccess: () => {
@ -584,116 +613,14 @@ function Designer({
</div>
</div>
{/* Mig 28 (S21 t4) — Section Cấu hình nâng cao (F1+F2+F3 advanced options).
6 checkbox per workflow: 4 mode Trả lại + 1 Skip CEO + 1 Approver edit. */}
<div className="space-y-2 rounded-lg border border-amber-200 bg-amber-50/30 p-3">
<Label className="text-amber-900">
Cấu hình nâng cao quyền duyệt mở rộng
</Label>
<p className="text-[11px] leading-relaxed text-slate-600">
Bật/tắt mode duyệt mở rộng cho workflow này. Mặc đnh chỉ "Trả về Người soạn thảo" enabled
(tương thích quy trình ). Các mode khác opt-in đ audit nghiêm.
</p>
<div className="mt-2 space-y-3">
<div>
<div className="mb-1 text-[11px] font-semibold uppercase text-slate-500">
Mode Trả lại (Approver chọn khi nhấn Trả lại)
</div>
<div className="grid grid-cols-2 gap-1.5">
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowReturnOneLevel}
onChange={e => setAllowReturnOneLevel(e.target.checked)}
/>
<span>
<span className="font-medium">Trả về 1 Cấp trước</span>
<span className="block text-[10px] text-slate-500">Lùi 1 Cấp trong cùng Bước, peer review chain</span>
</span>
</label>
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowReturnOneStep}
onChange={e => setAllowReturnOneStep(e.target.checked)}
/>
<span>
<span className="font-medium">Trả về 1 Bước trước</span>
<span className="block text-[10px] text-slate-500">Lùi sang Bước trước, Cấp cuối nhận lại</span>
</span>
</label>
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowReturnToAssignee}
onChange={e => setAllowReturnToAssignee(e.target.checked)}
/>
<span>
<span className="font-medium">Trả về Người chỉ đnh</span>
<span className="block text-[10px] text-slate-500">Pick runtime từ list NV đã duyệt</span>
</span>
</label>
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowReturnToDrafter}
onChange={e => setAllowReturnToDrafter(e.target.checked)}
/>
<span>
<span className="font-medium">Trả về Người soạn thảo</span>
<span className="block text-[10px] text-slate-500">Phase=TraLai, Drafter sửa rồi gửi lại (mặc đnh)</span>
</span>
</label>
</div>
</div>
<div>
<div className="mb-1 text-[11px] font-semibold uppercase text-slate-500">
Drafter gửi duyệt (Workspace "Lưu &amp; Gửi Duyệt")
</div>
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowDrafterSkipToFinal}
onChange={e => setAllowDrafterSkipToFinal(e.target.checked)}
/>
<span>
<span className="font-medium">Cho phép Drafter gửi thẳng Cấp cuối</span>
<span className="block text-[10px] text-slate-500">
Skip mọi Bước/Cấp trung gian đi thẳng NV Cấp cuối (vd CEO).
Workspace hiện dropdown 2 option "Gửi tuần tự" vs "Gửi thẳng Cấp cuối".
</span>
</span>
</label>
</div>
<div>
<div className="mb-1 text-[11px] font-semibold uppercase text-slate-500">
Approver chỉnh sửa phiếu
</div>
<label className="flex items-start gap-2 rounded border border-slate-200 bg-white px-2 py-1.5 text-[12px] hover:bg-amber-50/40">
<input
type="checkbox"
className="mt-0.5 h-3.5 w-3.5"
checked={allowApproverEditDetails}
onChange={e => setAllowApproverEditDetails(e.target.checked)}
/>
<span>
<span className="font-medium">Cho phép Approver chỉnh sửa Section 2 (Hạng mục + NCC + Báo giá)</span>
<span className="block text-[10px] text-slate-500">
NV Cấp đang duyệt đưc edit chi tiết phiếu (không reset workflow,
giữ Cấp hiện tại). Mọi thay đi log vào Lịch sử chỉnh sửa.
</span>
</span>
</label>
</div>
</div>
{/* Mig 29 (S21 t5) — 6 Allow* options MOVED per-NV:
- 5 flag F1+F3 xuống mỗi Level row (xem level entry inline below).
- 1 flag F2 AllowDrafterSkipToFinal xuống Users page (System → Users).
Section "Cấu hình nâng cao" workflow-level cũ Mig 28 đã DROP. */}
<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 (Trả lại modes + Edit Section 2) đt RIÊNG cho từng NV mỗi
Cấp dưới đây. F2 "Gửi thẳng Cấp cuối" (Drafter) cấu hình
<span className="font-medium"> User Management</span> (mỗi NV global).
</div>
<div className="space-y-2 rounded-lg border border-slate-200 p-3">
@ -830,7 +757,7 @@ function Designer({
const firstUser = availableUsers[0]
setSteps(steps.map((x, i) =>
i === idx
? { ...x, levelEntries: [...x.levelEntries, { order, approverUserId: firstUser.id }] }
? { ...x, levelEntries: [...x.levelEntries, makeDefaultLevelEntry(order, firstUser.id)] }
: x,
))
}}
@ -913,6 +840,81 @@ function Designer({
</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 NV #{ei + 1}
</div>
<div className="grid grid-cols-2 gap-1">
<label className="flex items-center gap-1 text-[11px] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowReturnOneLevel}
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] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowReturnOneStep}
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] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowReturnToAssignee}
onChange={e => updateField('allowReturnToAssignee', e.target.checked)}
/>
<span>Trả về Người chỉ đnh</span>
</label>
<label className="flex items-center gap-1 text-[11px] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowReturnToDrafter}
onChange={e => updateField('allowReturnToDrafter', e.target.checked)}
/>
<span>Trả về Drafter (mặc đnh)</span>
</label>
<label className="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
<input
type="checkbox"
className="h-3 w-3"
checked={entry.allowApproverEditDetails}
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>
</div>
</div>
)
})}
</div>
)}
</div>