Files
solution-erp/fe-user/src/components/WorkflowSummaryCard.tsx
pqhuy1987 cae4d84830
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s
[CLAUDE] Domain+Infra+App+FE: dynamic workflow policy per ContractType
Đọc QT-TP-NCC.docx: quy trình 9 bước chỉ áp dụng cho Thầu phụ/NCC/Tổ đội.
Dịch vụ/Mua bán/Nguyên tắc bypass CCM. Thay hardcoded dict bằng policy
registry.

Domain — WorkflowPolicy.cs:
- Record WorkflowPolicy { Name, Description, Transitions, PhaseSla,
  ActivePhases } — pure data, testable.
- WorkflowPolicies.Standard: 9-phase full (Thầu phụ/Giao khoán/NCC)
- WorkflowPolicies.SkipCcm: 7-phase (Dịch vụ/Mua bán/Nguyên tắc)
- WorkflowPolicyRegistry.For(type) map ContractType → policy
- WorkflowPolicyRegistry.ForContract(c) override nếu BypassProcurement
  AndCCM=true (instance-level escape hatch)

Infrastructure — ContractWorkflowService:
- Xóa hardcoded Transitions/PhaseSla dicts → load từ policy.ForContract
- TransitionAsync: validate qua policy.Transitions thay vì dict local
- Error message include policy.Name để debug dễ hơn
- GetPhaseSla trả SLA từ Standard policy (fallback — SLA hiện tại giống
  nhau giữa 2 policy)

Application — ContractDetailDto:
- Field mới `Workflow: WorkflowSummaryDto { PolicyName, Description,
  ActivePhases, NextPhases }` — FE dùng để render nút chuyển phase
  dynamic + timeline card.
- BuildWorkflowSummary helper trong ContractFeatures.

FE (both apps):
- Type WorkflowSummary + ContractDetail.workflow
- ContractDetailPage xóa hardcoded NEXT_PHASES — dùng
  c.workflow.nextPhases từ BE (single source of truth)
- WorkflowSummaryCard: timeline của ActivePhases với check/current/
  future states + policy name/description ở header
- Card hiển thị trong sidebar, phía trên "Lịch sử duyệt"

Docs:
- gotchas.md #21 marked RESOLVED (NEXT_PHASES sync không còn cần)

Foundation: sau này admin có thể edit policy qua UI khi chuyển sang DB-
backed policy — nhưng API contract (WorkflowSummaryDto) đã stable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:31 +07:00

64 lines
2.5 KiB
TypeScript

import { Check, Circle, GitBranch } from 'lucide-react'
import { PhaseBadge } from '@/components/PhaseBadge'
import { ContractPhaseLabel } from '@/types/contracts'
import type { WorkflowSummary } from '@/types/contracts'
import { cn } from '@/lib/cn'
// Shows the active WorkflowPolicy (Standard / SkipCcm / …) as a timeline of
// phases with the current one highlighted. Drafter + approvers see clearly
// "where we are" and "what comes next" — no more mental mapping of enum ints.
export function WorkflowSummaryCard({
workflow,
currentPhase,
}: {
workflow: WorkflowSummary
currentPhase: number
}) {
const currentIdx = workflow.activePhases.indexOf(currentPhase)
return (
<section className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 className="mb-1 flex items-center gap-2 text-sm font-semibold text-slate-700">
<GitBranch className="h-4 w-4" />
Quy trình: {workflow.policyName}
</h2>
<p className="mb-4 text-xs leading-relaxed text-slate-500">{workflow.policyDescription}</p>
<ol className="space-y-2">
{workflow.activePhases.map((phase, idx) => {
const isDone = currentIdx >= 0 && idx < currentIdx
const isCurrent = phase === currentPhase
const isFuture = currentIdx >= 0 && idx > currentIdx
return (
<li key={phase} className="flex items-center gap-3">
<div
className={cn(
'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold',
isDone && 'bg-emerald-100 text-emerald-700',
isCurrent && 'bg-brand-600 text-white ring-4 ring-brand-100',
isFuture && 'bg-slate-100 text-slate-400',
)}
>
{isDone ? <Check className="h-3 w-3" /> : isCurrent ? <Circle className="h-2 w-2 fill-current" /> : idx + 1}
</div>
<div className="flex-1">
<div
className={cn(
'text-sm',
isCurrent && 'font-semibold text-slate-900',
isFuture && 'text-slate-500',
isDone && 'text-slate-600',
)}
>
{ContractPhaseLabel[phase] ?? `Phase ${phase}`}
</div>
</div>
{isCurrent && <PhaseBadge phase={phase} className="text-[10px]" />}
</li>
)
})}
</ol>
</section>
)
}