[CLAUDE] Workflow: State machine 5 trạng thái — Trả lại = Phase riêng
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m17s

Session 17 spec: chốt 5 trạng thái phiếu PE/HĐ/Budget theo state diagram:
  Nháp ─trình──► Đã gửi duyệt ─approve cấp cuối──► Đã duyệt (terminal)
                              ├─ Trả lại ────────► Trả lại
                              └─ Từ chối ────────► Từ chối (terminal)
  Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu)

Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại = Phase
RIÊNG (TraLai=98), không revert về DangSoanThao + không jump-back step.
Drafter từ TraLai gửi lại như case Nháp — workflow chạy lại từ Cấp 1
Bước 1 (Option A diagram chốt với user).

BE Domain:
- ContractPhase + TraLai = 98
- BudgetPhase + TraLai = 98
- PurchaseEvaluationPhase: TraLai=98 đổi từ [LEGACY deprecated] thành
  primary state. Comment update enum docs cho cả 3.

BE Policy (PE/HĐ/Budget):
- Reject transitions trỏ về TraLai (thay DangSoanThao)
- Mirror entry transitions: TraLai → next phase (cho Drafter resubmit)
- ActivePhases thêm TraLai
- FromDefinition mirror: TraLai → step.Phase + reject → TraLai
- DefaultSla cho TraLai = same as DangSoanThao

BE Service (PE + Contract):
- Reject branch: target=TuChoi giữ; else set Phase=TraLai, clear
  CurrentWorkflowStepIndex=null
- Bỏ ResumeAfterReject branch + RejectedAtStepIndex/RejectedFromPhase
  assignment (DB column giữ deprecated cho data cũ)
- Drafter trình branch: từ DangSoanThao HOẶC TraLai → ChoDuyet, init
  CurrentWorkflowStepIndex=0 (cùng entry point, chạy lại từ đầu)
- Notification: TraLai when fromPhase=ChoDuyet → "bị trả lại"
- Budget Handler: simplify reject → TraLai, bỏ smart-reject + isResuming

BE Tests update:
- WorkflowPolicyTests: Standard_RejectFromCCM → TraLai (rename + assert)
  + Standard_TraLai_To_DangGopY_Allowed_For_Drafter (new)
- PurchaseEvaluationPolicyTests: BothPolicies_RejectFromCCM → TraLai
  + BothPolicies_TraLai_To_ChoPurchasing_AllowedForDrafter (new theory)
- BudgetPolicyTests: Default_CostControl_ChoCCM_To_TraLai (rename)
  + ActivePhases All6States (was All5) + NextPhasesFrom_TraLai (new)
  + NextPhasesFrom_ChoCEO_Includes_DaDuyet_And_TraLai (rename)
- 77 → 81 test pass (+4 tests TraLai entry point)

FE rename "Bản nháp" → "Nháp" (cả 2 app + types):
- types/purchaseEvaluation.ts: PurchaseEvaluationPhaseLabel 1=Nháp,
  10=Đã gửi duyệt. PeDisplayStatus.BanNhap → Nhap (key + value).
  PhaseLabel/Color cho TraLai update active.
- types/contracts.ts: +ChoDuyet=10, +TraLai=98 const + label/color.
  Phase 2 'Đang soạn thảo' → 'Nháp'.
- types/budget.ts: +TraLai=98 const + label/color. Phase 1 → 'Nháp'.
- PeListPanel + PurchaseEvaluationsListPage filter dropdown: Nhap +
  TraLai map đúng phase value.

BE label maps update consistent:
- ContractExcelExporter PhaseLabel: DangSoanThao → "Nháp" + add ChoDuyet/
  TraLai entries.
- PeWorkflowAdminFeatures + WorkflowAdminFeatures PhaseLabels: same.

Verify: dotnet test 81 pass · npm build × 2 app pass · BE 0 error.

Field RejectedAtStepIndex/RejectedFromPhase giữ DB column (nullable,
không set value mới). Cleanup migration sau.
This commit is contained in:
pqhuy1987
2026-05-08 14:12:38 +07:00
parent f3bea3c616
commit ff21120c8c
25 changed files with 286 additions and 187 deletions

View File

@ -244,7 +244,7 @@ export function PeListPanel({
// hết, user vẫn thấy được phiếu Đã gửi duyệt cùng với tất cả khác. Trade-off
// chấp nhận tới khi BE thêm multi-phase param.
function statusToPhaseValue(status: PeDisplayStatus): string {
if (status === PeDisplayStatus.BanNhap) return String(PurchaseEvaluationPhase.DangSoanThao)
if (status === PeDisplayStatus.Nhap) return String(PurchaseEvaluationPhase.DangSoanThao)
if (status === PeDisplayStatus.DaDuyet) return String(PurchaseEvaluationPhase.DaDuyet)
if (status === PeDisplayStatus.TuChoi) return String(PurchaseEvaluationPhase.TuChoi)
return '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE add support)

View File

@ -122,10 +122,12 @@ export function PurchaseEvaluationsListPage() {
<Select value={phase} onChange={e => setParam('phase', e.target.value)}>
<option value="">Tất cả trạng thái</option>
{Object.values(PeDisplayStatus).map(s => {
const phaseValue = s === PeDisplayStatus.BanNhap
const phaseValue = s === PeDisplayStatus.Nhap
? String(PurchaseEvaluationPhase.DangSoanThao)
: s === PeDisplayStatus.DaDuyet
? String(PurchaseEvaluationPhase.DaDuyet)
: s === PeDisplayStatus.TraLai
? String(PurchaseEvaluationPhase.TraLai)
: s === PeDisplayStatus.TuChoi
? String(PurchaseEvaluationPhase.TuChoi)
: '' // DaGuiDuyet — multi-phase, không filter exact (TODO BE)

View File

@ -7,15 +7,17 @@ export const BudgetPhase = {
ChoCCM: 2,
ChoCEO: 3,
DaDuyet: 4,
TraLai: 98,
TuChoi: 99,
} as const
export type BudgetPhase = typeof BudgetPhase[keyof typeof BudgetPhase]
export const BudgetPhaseLabel: Record<number, string> = {
1: 'Đang soạn thảo',
1: 'Nháp',
2: 'Chờ CCM',
3: 'Chờ CEO',
4: 'Đã duyệt',
98: 'Trả lại',
99: 'Từ chối',
}
@ -24,6 +26,7 @@ export const BudgetPhaseColor: Record<number, string> = {
2: 'bg-indigo-100 text-indigo-700',
3: 'bg-pink-100 text-pink-700',
4: 'bg-emerald-100 text-emerald-700',
98: 'bg-yellow-100 text-yellow-800',
99: 'bg-red-100 text-red-700',
}

View File

@ -8,6 +8,8 @@ export const ContractPhase = {
DangTrinhKy: 7,
DangDongDau: 8,
DaPhatHanh: 9,
ChoDuyet: 10,
TraLai: 98,
TuChoi: 99,
} as const
@ -15,7 +17,7 @@ export type ContractPhase = typeof ContractPhase[keyof typeof ContractPhase]
export const ContractPhaseLabel: Record<number, string> = {
1: 'Đang chọn NCC',
2: 'Đang soạn thảo',
2: 'Nháp',
3: 'Đang góp ý',
4: 'Đang đàm phán',
5: 'Đang in ký',
@ -23,12 +25,14 @@ export const ContractPhaseLabel: Record<number, string> = {
7: 'Đang trình ký',
8: 'Đang đóng dấu',
9: 'Đã phát hành',
10: 'Đã gửi duyệt',
98: 'Trả lại',
99: 'Từ chối',
}
export const ContractPhaseColor: Record<number, string> = {
1: 'bg-slate-100 text-slate-700',
2: 'bg-blue-100 text-blue-700',
2: 'bg-slate-100 text-slate-700',
3: 'bg-amber-100 text-amber-700',
4: 'bg-orange-100 text-orange-700',
5: 'bg-purple-100 text-purple-700',
@ -36,6 +40,8 @@ export const ContractPhaseColor: Record<number, string> = {
7: 'bg-fuchsia-100 text-fuchsia-700',
8: 'bg-pink-100 text-pink-700',
9: 'bg-emerald-100 text-emerald-700',
10: 'bg-amber-100 text-amber-700',
98: 'bg-yellow-100 text-yellow-800',
99: 'bg-red-100 text-red-700',
}

View File

@ -27,20 +27,20 @@ export const PurchaseEvaluationPhase = {
ChoCEODuyetNCC: 6, // [LEGACY]
DaDuyet: 7,
ChoDuyet: 10, // [Mig 21] generic intermediate
TraLai: 98, // [LEGACY]
TraLai: 98, // Phase riêng — Drafter sửa+gửi lại chạy từ đầu
TuChoi: 99,
} as const
export type PurchaseEvaluationPhase = typeof PurchaseEvaluationPhase[keyof typeof PurchaseEvaluationPhase]
export const PurchaseEvaluationPhaseLabel: Record<number, string> = {
1: 'Đang soạn thảo',
1: 'Nháp',
2: 'Chờ Purchasing',
3: 'Chờ Dự án',
4: 'Chờ CCM',
5: 'Chờ CEO duyệt PA',
6: 'Chờ CEO duyệt NCC',
7: 'Đã duyệt',
10: ang duyệt',
10: ã gửi duyệt',
98: 'Trả lại',
99: 'Từ chối',
}
@ -65,15 +65,14 @@ export function isEditablePhase(phase: number): boolean {
|| phase === PurchaseEvaluationPhase.TraLai
}
// Display status meta — gom các phase chi tiết thành 4 nhóm hiển thị end-user
// friendly. Workflow timeline + workflow service vẫn dùng phase chi tiết.
// User 2026-05-07 chỉnh:
// - Bản nháp = DangSoanThao (chỉ hiện ở Thao tác workspace, ko Duyệt menu)
// - Đã gửi duyệt = bất kỳ phase trung gian (ChoPurchasing/ChoDuAn/ChoCCM/...)
// - Đã duyệt = DaDuyet
// - Từ chối = TuChoi
// Display status meta — 5 trạng thái spec Session 17:
// Nháp = DangSoanThao (chưa vào quy trình duyệt)
// Đã gửi duyệt = ChoDuyet/legacy intermediate (đang chạy quy trình)
// Trả lại = TraLai (có history đã đi qua quy trình, sửa+gửi lại chạy từ đầu)
// Đã duyệt = DaDuyet (terminal OK — input cho phiếu khác / in trình ký)
// Từ chối = TuChoi (terminal lock — không thao tác gì được nữa)
export const PeDisplayStatus = {
BanNhap: 'BanNhap',
Nhap: 'Nhap',
DaGuiDuyet: 'DaGuiDuyet',
TraLai: 'TraLai',
DaDuyet: 'DaDuyet',
@ -82,7 +81,7 @@ export const PeDisplayStatus = {
export type PeDisplayStatus = typeof PeDisplayStatus[keyof typeof PeDisplayStatus]
export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
BanNhap: 'Bản nháp',
Nhap: 'Nháp',
DaGuiDuyet: 'Đã gửi duyệt',
TraLai: 'Trả lại',
DaDuyet: 'Đã duyệt',
@ -90,7 +89,7 @@ export const PeDisplayStatusLabel: Record<PeDisplayStatus, string> = {
}
export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
BanNhap: 'bg-slate-100 text-slate-700',
Nhap: 'bg-slate-100 text-slate-700',
DaGuiDuyet: 'bg-amber-100 text-amber-700',
TraLai: 'bg-yellow-100 text-yellow-800',
DaDuyet: 'bg-emerald-100 text-emerald-700',
@ -98,11 +97,11 @@ export const PeDisplayStatusColor: Record<PeDisplayStatus, string> = {
}
export function getPeDisplayStatus(phase: number): PeDisplayStatus {
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.BanNhap
if (phase === PurchaseEvaluationPhase.DangSoanThao) return PeDisplayStatus.Nhap
if (phase === PurchaseEvaluationPhase.DaDuyet) return PeDisplayStatus.DaDuyet
if (phase === PurchaseEvaluationPhase.TraLai) return PeDisplayStatus.TraLai
if (phase === PurchaseEvaluationPhase.TuChoi) return PeDisplayStatus.TuChoi
// Mig 21 ChoDuyet=10 + legacy intermediate 2-6 → all map "Đã gửi duyệt"
// ChoDuyet=10 + legacy intermediate 2-6 → all map "Đã gửi duyệt"
return PeDisplayStatus.DaGuiDuyet
}