[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:
@ -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á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 & 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>
|
||||
|
||||
Reference in New Issue
Block a user