[CLAUDE] FE-User: Plan AA redesign - WorkflowMatrixViewPage panel-per-NV layout + color coding 2 layer (Step/Cấp)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m20s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m20s
UAT feedback 2026-05-15 sau Run #211 deploy: bro request hiển thị rõ ràng giống admin Designer (panel per NV + 7 label tiếng Việt) + màu sắc khác nhau giữa Cấp duyệt + giữa Phòng ban để phân biệt. Redesign WorkflowMatrixViewPage.tsx ~250 LOC (drop table 11 cột symbol khó hiểu): NEW layout per Step (Phòng): - Step container có unique color (cycle 5 màu: blue/purple/emerald/amber/pink) - Step header bar với tone đậm: "Bước N — Phòng X" - Group levels theo level.order → 1 Cấp group = N NV panel song song (OR-of-N) - Cấp badge có unique color (cycle 5 màu: violet/sky/teal/orange/rose) - "1 NV duyệt" hoặc "N NV (OR-of-N — chỉ cần 1 NV duyệt là qua Cấp)" hint - NV permission panel mirror admin Designer line 853-949: - Header "QUYỀN DUYỆT {NV name} {email}" amber-700 uppercase - 7 checkbox label tiếng Việt rõ (read-only disabled accent-emerald): 1. Trả về 1 Cấp trước 2. Trả về 1 Bước trước 3. Trả về Người chỉ định 4. Trả về Drafter (mặc định) 5. Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá) lúc đang duyệt 6. Cho phép chỉnh sửa Section ngân sách lúc đang duyệt 7. Cho phép duyệt thẳng Cấp cuối khi đang duyệt - Grid 2-col cho 4 return mode + col-span-2 cho 3 Allow* label dài - Inactive label slate-400, active slate-800 font-medium Color palette (Tailwind JIT — full class strings array): - STEP_PALETTE: 5 màu cycle theo sIdx % 5 - LEVEL_PALETTE: 5 màu cycle theo (level.order - 1) % 5 Drop FlagCell table cell helper. Replace với StepBlock + NvPermissionPanel + FlagRow components. Verify: - npm run build fe-user PASS clean 0 TS err, 423ms, 1907 modules - Bundle 1282.91 KB (+0.32 KB from baseline — minor add new components) Em main solo CSS/UX redesign decision (criteria #2 Implementer REFUSE — UX flow decision needed cho color palette + layout structure). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,11 +1,15 @@
|
||||
// Plan AA Chunk B (S24, 2026-05-15) — User read-only matrix view của workflow
|
||||
// V2 đã admin Designer ghim (`IsUserSelectable=true`, Mig 25). Hiển thị tất cả
|
||||
// version ghim cho ApplicableType (1=DuyetNcc, 2=DuyetNccPhuongAn) dưới dạng
|
||||
// table 10 cột — Bước/Cấp/NV duyệt + 7 Allow* flag.
|
||||
// version ghim cho ApplicableType (1=DuyetNcc, 2=DuyetNccPhuongAn).
|
||||
//
|
||||
// [Plan AA redesign S24 t1] Bro UAT request: drop table 11 cột compact symbol →
|
||||
// switch sang panel-per-NV layout mirror admin Designer read-only. 7 label rõ
|
||||
// tiếng Việt + color coding 2 layer (Step/Phòng + Cấp). Group levels theo
|
||||
// `level.order` (OR-of-N approvers cùng Cấp render N panel song song).
|
||||
//
|
||||
// URL: /purchase-evaluations/workflow-matrix?type=1|2
|
||||
// Mirror layout admin `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx`
|
||||
// nhưng drop mutation (Clone/Ghim/Xoá) + render table thay vì ol/li.
|
||||
// line 853-949 (read-only — drop input onChange).
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Network, CheckCircle2, Pin } from 'lucide-react'
|
||||
@ -19,6 +23,26 @@ const TYPE_LABEL: Record<number, string> = {
|
||||
2: 'Duyệt NCC + Giải pháp',
|
||||
}
|
||||
|
||||
// Color palette per Step (Phòng) — cycle 5 màu để phân biệt Bước. Tailwind JIT
|
||||
// yêu cầu full class strings (không dynamic interpolation).
|
||||
const STEP_PALETTE = [
|
||||
{ bg: 'bg-blue-50/40', border: 'border-blue-200', headerBg: 'bg-blue-100', headerText: 'text-blue-800' },
|
||||
{ bg: 'bg-purple-50/40', border: 'border-purple-200', headerBg: 'bg-purple-100', headerText: 'text-purple-800' },
|
||||
{ bg: 'bg-emerald-50/40', border: 'border-emerald-200', headerBg: 'bg-emerald-100', headerText: 'text-emerald-800' },
|
||||
{ bg: 'bg-amber-50/40', border: 'border-amber-200', headerBg: 'bg-amber-100', headerText: 'text-amber-800' },
|
||||
{ bg: 'bg-pink-50/40', border: 'border-pink-200', headerBg: 'bg-pink-100', headerText: 'text-pink-800' },
|
||||
] as const
|
||||
|
||||
// Color palette per Cấp (Level order) — cycle 5 màu để phân biệt Cấp trong cùng
|
||||
// Bước. Badge ring + tone đậm.
|
||||
const LEVEL_PALETTE = [
|
||||
'bg-violet-100 text-violet-800 ring-violet-300',
|
||||
'bg-sky-100 text-sky-800 ring-sky-300',
|
||||
'bg-teal-100 text-teal-800 ring-teal-300',
|
||||
'bg-orange-100 text-orange-800 ring-orange-300',
|
||||
'bg-rose-100 text-rose-800 ring-rose-300',
|
||||
] as const
|
||||
|
||||
export function WorkflowMatrixViewPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const rawType = Number(searchParams.get('type'))
|
||||
@ -79,12 +103,11 @@ export function WorkflowMatrixViewPage() {
|
||||
}
|
||||
|
||||
function WorkflowCard({ wf }: { wf: AwDefinitionDto }) {
|
||||
// Tính tổng số row table = tổng levels qua all steps.
|
||||
const totalRows = wf.steps.reduce((sum, s) => sum + s.levels.length, 0)
|
||||
const totalLevels = wf.steps.reduce((sum, s) => sum + s.levels.length, 0)
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<header className="flex items-start justify-between gap-3 border-b border-slate-100 px-5 py-3">
|
||||
<header className="flex items-start justify-between gap-3 border-b border-slate-100 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-[15px] font-semibold text-slate-900">
|
||||
@ -112,118 +135,19 @@ function WorkflowCard({ wf }: { wf: AwDefinitionDto }) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="px-5 py-4">
|
||||
{totalRows === 0 ? (
|
||||
<div className="space-y-3 px-3 py-3">
|
||||
{totalLevels === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-slate-200 bg-slate-50/40 px-3 py-4 text-center text-[12px] italic text-slate-400">
|
||||
Quy trình chưa cấu hình bước duyệt nào.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 text-[12px] font-semibold uppercase tracking-wide text-slate-600">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left">Bước (Phòng)</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left">Cấp</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left">NV duyệt</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Trả về 1 Cấp trước"
|
||||
>
|
||||
{'↶'} 1 Cấp
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Trả về 1 Bước trước"
|
||||
>
|
||||
{'↶'} 1 Bước
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Trả về Người chỉ định"
|
||||
>
|
||||
{'↶'} Chỉ định
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Trả về Drafter (mặc định)"
|
||||
>
|
||||
{'↶'} Drafter
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá)"
|
||||
>
|
||||
{'✎'} Section 2
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Cho phép chỉnh sửa Section ngân sách"
|
||||
>
|
||||
{'✎'} Ngân sách
|
||||
</th>
|
||||
<th
|
||||
className="border border-slate-200 px-3 py-2 text-center"
|
||||
title="Cho phép duyệt thẳng Cấp cuối"
|
||||
>
|
||||
{'⏩'} Cấp cuối
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{wf.steps.map((step, sIdx) => (
|
||||
step.levels.length === 0 ? (
|
||||
<tr key={step.id}>
|
||||
<td className="border border-slate-200 bg-slate-50/50 px-3 py-2 align-top font-medium text-slate-700">
|
||||
Bước {sIdx + 1} — {step.departmentName ?? '(Không gắn phòng)'}
|
||||
</td>
|
||||
<td colSpan={9} className="border border-slate-200 px-3 py-2 text-[12px] italic text-slate-400">
|
||||
Chưa có cấp duyệt
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
step.levels.map((level, lIdx) => (
|
||||
<tr key={level.id}>
|
||||
{lIdx === 0 && (
|
||||
<td
|
||||
rowSpan={step.levels.length}
|
||||
className="border border-slate-200 bg-slate-50/50 px-3 py-2 align-top font-medium text-slate-700"
|
||||
>
|
||||
Bước {sIdx + 1} — {step.departmentName ?? '(Không gắn phòng)'}
|
||||
</td>
|
||||
)}
|
||||
<td className="border border-slate-200 px-3 py-2 align-top">
|
||||
<span className="inline-flex items-center rounded-full bg-violet-100 px-2 py-0.5 font-mono text-[10px] font-bold text-violet-700">
|
||||
Cấp {level.order}
|
||||
</span>
|
||||
</td>
|
||||
<td className="border border-slate-200 px-3 py-2 align-top">
|
||||
<span className="font-medium text-slate-800">
|
||||
{level.approverUserName ?? level.approverUserId}
|
||||
</span>
|
||||
{level.approverEmail && (
|
||||
<span className="block text-[11px] text-slate-400">
|
||||
{level.approverEmail}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<FlagCell value={level.allowReturnOneLevel} />
|
||||
<FlagCell value={level.allowReturnOneStep} />
|
||||
<FlagCell value={level.allowReturnToAssignee} />
|
||||
<FlagCell value={level.allowReturnToDrafter} />
|
||||
<FlagCell value={level.allowApproverEditDetails} />
|
||||
<FlagCell value={level.allowApproverEditBudget} />
|
||||
<FlagCell value={level.allowApproverSkipToFinal} />
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
wf.steps.map((step, sIdx) => (
|
||||
<StepBlock key={step.id} step={step} sIdx={sIdx} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-slate-100 px-5 py-2.5">
|
||||
<footer className="border-t border-slate-100 px-4 py-2.5">
|
||||
<small className="text-[11px] text-slate-400">
|
||||
Cấu hình do Admin quản trị. Có thắc mắc liên hệ Admin.
|
||||
</small>
|
||||
@ -232,18 +156,104 @@ function WorkflowCard({ wf }: { wf: AwDefinitionDto }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FlagCell({ value }: { value: AwLevelDto[keyof Pick<AwLevelDto,
|
||||
'allowReturnOneLevel' | 'allowReturnOneStep' | 'allowReturnToAssignee' |
|
||||
'allowReturnToDrafter' | 'allowApproverEditDetails' | 'allowApproverEditBudget' |
|
||||
'allowApproverSkipToFinal'
|
||||
>] }) {
|
||||
function StepBlock({ step, sIdx }: { step: AwDefinitionDto['steps'][number]; sIdx: number }) {
|
||||
// Step (Phòng) → unique color cycle 5 màu để phân biệt Bước.
|
||||
const stepColor = STEP_PALETTE[sIdx % STEP_PALETTE.length]
|
||||
|
||||
// Group levels theo level.order — OR-of-N approvers cùng Cấp = N row Level
|
||||
// cùng Order (Mig 29 schema). User view: 1 Cấp group → N NV panel song song.
|
||||
const byOrder = new Map<number, AwLevelDto[]>()
|
||||
for (const lvl of step.levels) {
|
||||
const arr = byOrder.get(lvl.order) ?? []
|
||||
arr.push(lvl)
|
||||
byOrder.set(lvl.order, arr)
|
||||
}
|
||||
const sortedOrders = [...byOrder.keys()].sort((a, b) => a - b)
|
||||
|
||||
return (
|
||||
<td className="border border-slate-200 px-3 py-2 text-center align-top">
|
||||
{value ? (
|
||||
<span className="font-bold text-emerald-600">{'✓'}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">{'—'}</span>
|
||||
)}
|
||||
</td>
|
||||
<div className={`overflow-hidden rounded-lg border ${stepColor.border} ${stepColor.bg}`}>
|
||||
<div className={`flex items-center gap-2 border-b ${stepColor.border} ${stepColor.headerBg} px-3 py-2`}>
|
||||
<span className={`text-[13px] font-semibold ${stepColor.headerText}`}>
|
||||
Bước {sIdx + 1} — {step.departmentName ?? '(Không gắn phòng)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 p-3">
|
||||
{sortedOrders.length === 0 ? (
|
||||
<div className="text-[12px] italic text-slate-400">Chưa có cấp duyệt</div>
|
||||
) : (
|
||||
sortedOrders.map(order => {
|
||||
const levelsOfOrder = byOrder.get(order)!
|
||||
// Cấp (Level order) → unique color cycle 5 màu trong Step.
|
||||
const levelColor = LEVEL_PALETTE[(order - 1) % LEVEL_PALETTE.length]
|
||||
return (
|
||||
<div key={order} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-[11px] font-bold ring-1 ${levelColor}`}>
|
||||
Cấp {order}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
{levelsOfOrder.length === 1
|
||||
? '1 NV duyệt'
|
||||
: `${levelsOfOrder.length} NV (OR-of-${levelsOfOrder.length} — chỉ cần 1 NV duyệt là qua Cấp)`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{levelsOfOrder.map(level => (
|
||||
<NvPermissionPanel key={level.id} level={level} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NvPermissionPanel({ level }: { level: AwLevelDto }) {
|
||||
return (
|
||||
<div className="rounded-md border border-amber-200 bg-white/90 px-3 py-2 shadow-sm">
|
||||
<div className="mb-2 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 border-b border-amber-100 pb-1.5">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide text-amber-700">
|
||||
Quyền duyệt {level.approverUserName ?? level.approverUserId}
|
||||
</span>
|
||||
{level.approverEmail && (
|
||||
<span className="text-[11px] text-slate-400">
|
||||
{level.approverEmail}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-3 gap-y-1.5 sm:grid-cols-2">
|
||||
<FlagRow active={level.allowReturnOneLevel} label="Trả về 1 Cấp trước" />
|
||||
<FlagRow active={level.allowReturnOneStep} label="Trả về 1 Bước trước" />
|
||||
<FlagRow active={level.allowReturnToAssignee} label="Trả về Người chỉ định" />
|
||||
<FlagRow active={level.allowReturnToDrafter} label="Trả về Drafter (mặc định)" />
|
||||
<FlagRow active={level.allowApproverEditDetails} label="Cho phép chỉnh sửa Section 2 (Hạng mục/NCC/Báo giá) lúc đang duyệt" colSpan2 />
|
||||
<FlagRow active={level.allowApproverEditBudget} label="Cho phép chỉnh sửa Section ngân sách lúc đang duyệt" colSpan2 />
|
||||
<FlagRow active={level.allowApproverSkipToFinal} label="Cho phép duyệt thẳng Cấp cuối khi đang duyệt" colSpan2 />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// FlagRow read-only checkbox mirror admin Designer line 885-948. `disabled`
|
||||
// giữ tick visible nhưng disable interaction. colSpan2 cho 3 Allow* label dài
|
||||
// (Edit Section 2 / Edit Budget / Skip Final) chiếm full 2 col.
|
||||
function FlagRow({ active, label, colSpan2 }: { active: boolean; label: string; colSpan2?: boolean }) {
|
||||
return (
|
||||
<label className={`flex items-start gap-1.5 text-[12px] ${colSpan2 ? 'sm:col-span-2' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5 h-3.5 w-3.5 shrink-0 accent-emerald-600"
|
||||
checked={active}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
<span className={active ? 'font-medium text-slate-800' : 'text-slate-400'}>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user