[CLAUDE] FE-User: Plan AA Chunk B - WorkflowMatrixViewPage read-only matrix view + types extend
NEW files:
- pages/pe/WorkflowMatrixViewPage.tsx ~215 LOC
- useQuery GET /approval-workflows-v2?applicableType=N&isUserSelectable=true
- PageHeader "Luồng duyệt — {label}" + Network icon
- Loading/Error/Empty state 3 variant rõ
- WorkflowCard per ghim version: header (code/version + badge "Đang dùng" emerald + "Được ghim" amber Pin icon)
- Table 10 cột read-only: Bước rowSpan | Cấp | NV duyệt + 7 Allow* flag (✓/—)
- FlagCell helper component TS indexed-access type union 7 keys
- Mirror admin Designer ApprovalWorkflowsV2Page layout (drop edit mutations)
- types/approvalWorkflowV2.ts ~55 LOC
- 5 type subset: AwLevelDto + AwStepDto + AwDefinitionDto + AwTypeSummaryDto + AwAdminOverviewDto
- Field name khớp BE record positional param (history not versions)
MODIFIED:
- App.tsx +import + Route /purchase-evaluations/workflow-matrix trước /workspace
Why:
- Chunk A BE đã wire endpoint với filter param + menu seed Pe_DuyetNcc_WfView
cho user xem matrix workflow admin Designer ghim trước khi tạo phiếu.
Verify:
- npm run build fe-user PASS clean 0 TS err, 1907 modules, 2.61s
- Reviewer cumulative A+B PASS 0 critical/major/minor
Pending Chunk C: Docs session log + STATUS + HANDOFF + 4 agent MEMORY drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -12,6 +12,7 @@ import { MyContractsPage } from '@/pages/contracts/MyContractsPage'
|
|||||||
import { PurchaseEvaluationsListPage, PurchaseEvaluationDetailPage } from '@/pages/pe/PurchaseEvaluationsListPage'
|
import { PurchaseEvaluationsListPage, PurchaseEvaluationDetailPage } from '@/pages/pe/PurchaseEvaluationsListPage'
|
||||||
import { PurchaseEvaluationCreatePage } from '@/pages/pe/PurchaseEvaluationCreatePage'
|
import { PurchaseEvaluationCreatePage } from '@/pages/pe/PurchaseEvaluationCreatePage'
|
||||||
import { PurchaseEvaluationWorkspacePage } from '@/pages/pe/PurchaseEvaluationWorkspacePage'
|
import { PurchaseEvaluationWorkspacePage } from '@/pages/pe/PurchaseEvaluationWorkspacePage'
|
||||||
|
import { WorkflowMatrixViewPage } from '@/pages/pe/WorkflowMatrixViewPage'
|
||||||
import { BudgetsListPage, BudgetDetailPage } from '@/pages/budgets/BudgetsListPage'
|
import { BudgetsListPage, BudgetDetailPage } from '@/pages/budgets/BudgetsListPage'
|
||||||
import { BudgetCreatePage } from '@/pages/budgets/BudgetCreatePage'
|
import { BudgetCreatePage } from '@/pages/budgets/BudgetCreatePage'
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ function App() {
|
|||||||
<Route path="/contracts/:id" element={<ContractDetailPage />} />
|
<Route path="/contracts/:id" element={<ContractDetailPage />} />
|
||||||
<Route path="/my-contracts" element={<MyContractsPage />} />
|
<Route path="/my-contracts" element={<MyContractsPage />} />
|
||||||
<Route path="/purchase-evaluations" element={<PurchaseEvaluationsListPage />} />
|
<Route path="/purchase-evaluations" element={<PurchaseEvaluationsListPage />} />
|
||||||
|
<Route path="/purchase-evaluations/workflow-matrix" element={<WorkflowMatrixViewPage />} />
|
||||||
<Route path="/purchase-evaluations/workspace" element={<PurchaseEvaluationWorkspacePage />} />
|
<Route path="/purchase-evaluations/workspace" element={<PurchaseEvaluationWorkspacePage />} />
|
||||||
<Route path="/purchase-evaluations/new" element={<PurchaseEvaluationCreatePage />} />
|
<Route path="/purchase-evaluations/new" element={<PurchaseEvaluationCreatePage />} />
|
||||||
<Route path="/purchase-evaluations/:id" element={<PurchaseEvaluationDetailPage />} />
|
<Route path="/purchase-evaluations/:id" element={<PurchaseEvaluationDetailPage />} />
|
||||||
|
|||||||
247
fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx
Normal file
247
fe-user/src/pages/pe/WorkflowMatrixViewPage.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { Network, CheckCircle2, Pin } from 'lucide-react'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import type { AwAdminOverviewDto, AwDefinitionDto, AwLevelDto } from '@/types/approvalWorkflowV2'
|
||||||
|
|
||||||
|
// Mig 23 ApplicableType enum mirror (BE Domain/ApprovalWorkflowsV2)
|
||||||
|
const TYPE_LABEL: Record<number, string> = {
|
||||||
|
1: 'Duyệt NCC',
|
||||||
|
2: 'Duyệt NCC + Giải pháp',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkflowMatrixViewPage() {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const rawType = Number(searchParams.get('type'))
|
||||||
|
const typeInt = rawType === 1 || rawType === 2 ? rawType : 1
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ['workflow-matrix', typeInt],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await api.get<AwAdminOverviewDto>('/approval-workflows-v2', {
|
||||||
|
params: { applicableType: typeInt, isUserSelectable: true },
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Em chỉ render type được chọn — BE đã filter applicableType.
|
||||||
|
const summary = data?.types.find(t => t.applicableType === typeInt)
|
||||||
|
const workflows: AwDefinitionDto[] = summary?.history ?? []
|
||||||
|
const typeLabel = summary?.applicableTypeLabel ?? TYPE_LABEL[typeInt] ?? 'Quy trình duyệt'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 px-6 py-5">
|
||||||
|
<PageHeader
|
||||||
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Network className="h-5 w-5 text-slate-500" />
|
||||||
|
Luồng duyệt — {typeLabel}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description="Cấu hình do Admin quản trị. Có thắc mắc liên hệ Admin."
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="rounded-xl border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500">
|
||||||
|
Đang tải cấu hình...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && !isLoading && (
|
||||||
|
<div className="rounded-xl border border-dashed border-rose-300 bg-rose-50/40 p-8 text-center text-sm text-rose-700">
|
||||||
|
Không thể tải cấu hình quy trình. Thử lại sau.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !isError && workflows.length === 0 && (
|
||||||
|
<div className="rounded-xl border border-dashed border-slate-300 bg-slate-50/40 p-8 text-center text-sm text-slate-500">
|
||||||
|
Chưa có quy trình nào được Admin ghim cho loại phiếu này. Liên hệ Admin.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !isError && workflows.map(wf => (
|
||||||
|
<WorkflowCard key={wf.id} wf={wf} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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">
|
||||||
|
{wf.name}
|
||||||
|
</h3>
|
||||||
|
<span className="rounded bg-slate-100 px-2 py-0.5 font-mono text-[11px] text-slate-600">
|
||||||
|
{wf.code} v{String(wf.version).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
{wf.isActive && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2.5 py-0.5 text-[11px] font-medium text-emerald-700">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
Đang dùng
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{wf.isUserSelectable && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-amber-100 px-2.5 py-0.5 text-[11px] font-medium text-amber-700">
|
||||||
|
<Pin className="h-3 w-3" />
|
||||||
|
Được ghim
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{wf.description && (
|
||||||
|
<p className="mt-1.5 text-[12px] leading-relaxed text-slate-500">{wf.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="px-5 py-4">
|
||||||
|
{totalRows === 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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="border-t border-slate-100 px-5 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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlagCell({ value }: { value: AwLevelDto[keyof Pick<AwLevelDto,
|
||||||
|
'allowReturnOneLevel' | 'allowReturnOneStep' | 'allowReturnToAssignee' |
|
||||||
|
'allowReturnToDrafter' | 'allowApproverEditDetails' | 'allowApproverEditBudget' |
|
||||||
|
'allowApproverSkipToFinal'
|
||||||
|
>] }) {
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
fe-user/src/types/approvalWorkflowV2.ts
Normal file
56
fe-user/src/types/approvalWorkflowV2.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Types mirror BE `AwAdminOverviewDto` (Application/ApprovalWorkflowsV2/
|
||||||
|
// ApprovalWorkflowV2AdminFeatures.cs). Chỉ subset cần cho fe-user read-only
|
||||||
|
// matrix view (Plan AA Chunk B S24).
|
||||||
|
//
|
||||||
|
// 7 Allow* flag per slot Level — Mig 29/30/31 cumulative.
|
||||||
|
|
||||||
|
export type AwLevelDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
name: string | null
|
||||||
|
approverUserId: string
|
||||||
|
approverUserName: string | null
|
||||||
|
approverEmail: string | null
|
||||||
|
allowReturnOneLevel: boolean
|
||||||
|
allowReturnOneStep: boolean
|
||||||
|
allowReturnToAssignee: boolean
|
||||||
|
allowReturnToDrafter: boolean
|
||||||
|
allowApproverEditDetails: boolean
|
||||||
|
allowApproverEditBudget: boolean
|
||||||
|
allowApproverSkipToFinal: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AwStepDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
name: string
|
||||||
|
departmentId: string | null
|
||||||
|
departmentName: string | null
|
||||||
|
levels: AwLevelDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AwDefinitionDto = {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
version: number
|
||||||
|
applicableType: number
|
||||||
|
applicableTypeLabel: string
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
isActive: boolean
|
||||||
|
isUserSelectable: boolean
|
||||||
|
activatedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
steps: AwStepDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AwTypeSummaryDto = {
|
||||||
|
applicableType: number
|
||||||
|
applicableTypeLabel: string
|
||||||
|
active: AwDefinitionDto | null
|
||||||
|
history: AwDefinitionDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AwAdminOverviewDto = {
|
||||||
|
types: AwTypeSummaryDto[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user