[CLAUDE] FE-Admin+Domain+Infra+App: Workflows tab → sidebar menu items
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m37s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m37s
User request: 7 tab trong /system/workflows thành menu items riêng. Domain: - MenuKeys.WorkflowTypeLeaf(code) helper — `Wf_<TypeCode>` pattern Infrastructure (DbInitializer): - Seed 7 leaves dưới Workflows group (order 95..101), label matches ContractType (HĐ Thầu phụ / Giao khoán / NCC / Dịch vụ / Mua bán / Nguyên tắc NCC / Nguyên tắc Dịch vụ). Idempotent. Application (GetMyMenuTreeQuery): - Generalized inherit-perm logic: descendants of Contracts AND Workflows inherit parent CanRead flag. Single Workflows.Read grant → all 7 Wf_* leaves visible; no per-leaf permission rows needed. FE Layout (admin): - resolvePath: Wf_<Code> → /system/workflows/<code>. Ct_* still hidden on admin side. FE App.tsx: - New route /system/workflows/:typeCode? FE WorkflowsPage: - Removed horizontal tab bar; type selection now comes từ URL param. - Landing view (no param): 3-col grid card per type với active version badge — so admin có visual overview khi click top-level Workflows group without selecting a type. - TYPE_CODE_TO_INT map drives URL→int conversion. Result: click `Quy trình HĐ > HĐ Mua bán` trong sidebar → opens /system/workflows/MuaBan directly với designer scoped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
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, Info, History } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
@ -63,16 +64,31 @@ function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
||||
|
||||
// ===== Page =====
|
||||
|
||||
// Map URL type code → int. Mirror Wf_<Code> menu key.
|
||||
const TYPE_CODE_TO_INT: Record<string, number> = {
|
||||
ThauPhu: 1,
|
||||
GiaoKhoan: 2,
|
||||
NhaCungCap: 3,
|
||||
DichVu: 4,
|
||||
MuaBan: 5,
|
||||
NguyenTacNcc: 6,
|
||||
NguyenTacDv: 7,
|
||||
}
|
||||
|
||||
export function WorkflowsPage() {
|
||||
const qc = useQueryClient()
|
||||
const { typeCode } = useParams<{ typeCode?: string }>()
|
||||
const overview = useQuery({
|
||||
queryKey: ['workflow-overview'],
|
||||
queryFn: async () => (await api.get<{ types: TypeSummaryDto[] }>('/workflows')).data,
|
||||
})
|
||||
|
||||
const [activeType, setActiveType] = useState<number | null>(null)
|
||||
const tab = activeType ?? overview.data?.types[0]?.contractType ?? 1
|
||||
const currentType = overview.data?.types.find(t => t.contractType === tab)
|
||||
// URL drives which type to show. `/system/workflows` (no param) → show
|
||||
// landing hint to pick from sidebar; `/system/workflows/<code>` → open that.
|
||||
const selectedTypeInt = typeCode ? TYPE_CODE_TO_INT[typeCode] : null
|
||||
const currentType = selectedTypeInt
|
||||
? overview.data?.types.find(t => t.contractType === selectedTypeInt)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@ -80,38 +96,41 @@ export function WorkflowsPage() {
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5" />
|
||||
Quy trình duyệt hợp đồng
|
||||
{currentType ? `Quy trình: ${currentType.contractTypeLabel}` : 'Quy trình duyệt hợp đồng'}
|
||||
</span>
|
||||
}
|
||||
description="Mỗi loại HĐ có quy trình riêng, hỗ trợ versioning. Tạo version mới → HĐ tương lai chạy theo. HĐ cũ vẫn giữ quy trình cũ."
|
||||
description={
|
||||
currentType
|
||||
? 'Tạo version mới → HĐ tương lai dùng. HĐ đã tạo giữ version cũ (pinned lúc tạo).'
|
||||
: 'Chọn loại HĐ từ menu bên trái để xem + chỉnh quy trình duyệt.'
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
{overview.data && (
|
||||
<div className="mb-5 flex gap-1 overflow-x-auto border-b border-slate-200">
|
||||
{overview.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}
|
||||
|
||||
{/* Landing: no type picked yet */}
|
||||
{overview.data && !currentType && (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{overview.data.types.map(t => (
|
||||
<button
|
||||
key={t.contractType}
|
||||
onClick={() => setActiveType(t.contractType)}
|
||||
className={`shrink-0 border-b-2 px-4 py-2 text-sm font-medium transition ${
|
||||
tab === t.contractType
|
||||
? 'border-brand-600 text-brand-700'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-800'
|
||||
}`}
|
||||
>
|
||||
{t.contractTypeLabel}
|
||||
{t.active && (
|
||||
<span className="ml-2 rounded bg-brand-50 px-1.5 py-0.5 font-mono text-[10px] text-brand-700">
|
||||
v{String(t.active.version).padStart(2, '0')}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<div key={t.contractType} 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.contractTypeLabel}</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.history.length} version${t.history.length > 1 ? 's' : ''}`
|
||||
: 'Chưa có quy trình'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{overview.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}
|
||||
|
||||
{currentType && <TypePanel type={currentType} onSaved={() => qc.invalidateQueries({ queryKey: ['workflow-overview'] })} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user