[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
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:
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
|
||||
@ -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,7 +97,7 @@ 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
|
||||
|
||||
@ -136,29 +136,24 @@ public class TransitionBudgetCommandHandler(
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
// ===== Smart reject + resume (Phase 9 — Migration 16) =====
|
||||
// ===== Reject → TraLai (Session 17 spec mới) =====
|
||||
// Bỏ smart-reject jump-back. Trả lại = Phase riêng (TraLai).
|
||||
// Drafter từ TraLai gửi lại như Nháp — Policy `(TraLai, ChoCCM)` đã wire.
|
||||
// Field RejectedFromPhase giữ DB column nhưng KHÔNG set value mới (data cũ vẫn đọc).
|
||||
var fromPhase = entity.Phase;
|
||||
var targetPhase = request.TargetPhase;
|
||||
var isResumingAfterReject = request.Decision == ApprovalDecision.Approve
|
||||
&& fromPhase == BudgetPhase.DangSoanThao
|
||||
&& entity.RejectedFromPhase != null;
|
||||
|
||||
if (request.Decision == ApprovalDecision.Reject)
|
||||
if (request.Decision == ApprovalDecision.Reject && targetPhase != BudgetPhase.TuChoi)
|
||||
{
|
||||
entity.RejectedFromPhase = fromPhase;
|
||||
targetPhase = BudgetPhase.DangSoanThao;
|
||||
}
|
||||
else if (isResumingAfterReject)
|
||||
{
|
||||
targetPhase = entity.RejectedFromPhase!.Value;
|
||||
entity.RejectedFromPhase = null;
|
||||
// Trả lại — override target → TraLai
|
||||
targetPhase = BudgetPhase.TraLai;
|
||||
}
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
|
||||
|
||||
// Policy guard — bypass khi resume sau reject.
|
||||
if (!isAdmin && !isResumingAfterReject
|
||||
// Policy guard
|
||||
if (!isAdmin
|
||||
&& !policy.IsTransitionAllowed(fromPhase, targetPhase, currentUser.Roles))
|
||||
throw new ForbiddenException(
|
||||
$"Role không đủ quyền chuyển {fromPhase} → {targetPhase}.");
|
||||
@ -168,8 +163,8 @@ public class TransitionBudgetCommandHandler(
|
||||
// nhưng giữ consistent UX 3 module.
|
||||
if (request.Decision == ApprovalDecision.Approve
|
||||
&& targetPhase != BudgetPhase.DangSoanThao
|
||||
&& targetPhase != BudgetPhase.TraLai
|
||||
&& targetPhase != BudgetPhase.TuChoi
|
||||
&& !isResumingAfterReject
|
||||
&& !isAdmin
|
||||
&& currentUser.UserId is Guid actorUid)
|
||||
{
|
||||
|
||||
@ -74,7 +74,7 @@ public class GetWorkflowAdminOverviewQueryHandler(
|
||||
|
||||
private static readonly Dictionary<ContractPhase, string> PhaseLabels = new()
|
||||
{
|
||||
[ContractPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[ContractPhase.DangSoanThao] = "Nháp",
|
||||
[ContractPhase.DangGopY] = "Đang góp ý",
|
||||
[ContractPhase.DangDamPhan] = "Đang đàm phán",
|
||||
[ContractPhase.DangInKy] = "Đang in ký",
|
||||
@ -82,6 +82,9 @@ public class GetWorkflowAdminOverviewQueryHandler(
|
||||
[ContractPhase.DangTrinhKy] = "Đang trình ký",
|
||||
[ContractPhase.DangDongDau] = "Đang đóng dấu",
|
||||
[ContractPhase.DaPhatHanh] = "Đã phát hành",
|
||||
[ContractPhase.ChoDuyet] = "Đã gửi duyệt",
|
||||
[ContractPhase.TraLai] = "Trả lại",
|
||||
[ContractPhase.TuChoi] = "Từ chối",
|
||||
};
|
||||
|
||||
public async Task<WorkflowAdminOverviewDto> Handle(GetWorkflowAdminOverviewQuery request, CancellationToken ct)
|
||||
|
||||
@ -69,13 +69,16 @@ public class GetPeWorkflowAdminOverviewQueryHandler(
|
||||
|
||||
private static readonly Dictionary<PurchaseEvaluationPhase, string> PhaseLabels = new()
|
||||
{
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = "Nháp",
|
||||
[PurchaseEvaluationPhase.ChoPurchasing] = "Chờ Purchasing",
|
||||
[PurchaseEvaluationPhase.ChoDuAn] = "Chờ Dự án",
|
||||
[PurchaseEvaluationPhase.ChoCCM] = "Chờ CCM",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetPA] = "Chờ CEO duyệt PA",
|
||||
[PurchaseEvaluationPhase.ChoCEODuyetNCC] = "Chờ CEO duyệt NCC",
|
||||
[PurchaseEvaluationPhase.ChoDuyet] = "Đã gửi duyệt",
|
||||
[PurchaseEvaluationPhase.DaDuyet] = "Đã duyệt",
|
||||
[PurchaseEvaluationPhase.TraLai] = "Trả lại",
|
||||
[PurchaseEvaluationPhase.TuChoi] = "Từ chối",
|
||||
};
|
||||
|
||||
public async Task<PeWorkflowAdminOverviewDto> Handle(GetPeWorkflowAdminOverviewQuery request, CancellationToken ct)
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// State machine ngân sách — đơn giản 3 bước duyệt + 2 terminal.
|
||||
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet
|
||||
// Bất kỳ phase duyệt → DangSoanThao (reject)
|
||||
// DangSoanThao → TuChoi
|
||||
// State machine ngân sách — Session 17 spec mới (5 trạng thái mirror PE/HĐ):
|
||||
// DangSoanThao (Nháp) → ChoCCM (Drafter trình)
|
||||
// TraLai (Trả lại) → ChoCCM (Drafter sửa+gửi lại, chạy từ đầu)
|
||||
// ChoCCM/ChoCEO → next phase OR TraLai OR TuChoi
|
||||
// ChoCEO → DaDuyet (terminal)
|
||||
// DangSoanThao/TraLai → TuChoi (Drafter huỷ)
|
||||
public enum BudgetPhase
|
||||
{
|
||||
DangSoanThao = 1,
|
||||
ChoCCM = 2,
|
||||
ChoCEO = 3,
|
||||
DaDuyet = 4,
|
||||
TraLai = 98, // Phase riêng (không revert DangSoanThao)
|
||||
TuChoi = 99,
|
||||
}
|
||||
|
||||
@ -31,12 +31,15 @@ public static class BudgetPolicies
|
||||
private static readonly Dictionary<BudgetPhase, TimeSpan?> DefaultSla = new()
|
||||
{
|
||||
[BudgetPhase.DangSoanThao] = TimeSpan.FromDays(5),
|
||||
[BudgetPhase.TraLai] = TimeSpan.FromDays(5),
|
||||
[BudgetPhase.ChoCCM] = TimeSpan.FromDays(3),
|
||||
[BudgetPhase.ChoCEO] = TimeSpan.FromDays(2),
|
||||
[BudgetPhase.DaDuyet] = null,
|
||||
[BudgetPhase.TuChoi] = null,
|
||||
};
|
||||
|
||||
// Session 17 spec: Reject = về TraLai (Phase riêng). Drafter từ TraLai
|
||||
// gửi lại = entry point thứ 2 (mirror DangSoanThao → ChoCCM).
|
||||
public static readonly BudgetPolicy Default = new(
|
||||
Name: "Default",
|
||||
Description: "Quy trình ngân sách 3-step (Drafter → CCM → CEO).",
|
||||
@ -44,17 +47,22 @@ public static class BudgetPolicies
|
||||
{
|
||||
[(BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(BudgetPhase.DangSoanThao, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(BudgetPhase.TraLai, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(BudgetPhase.TraLai, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.TraLai)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.TuChoi)] = [AppRoles.CostControl],
|
||||
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
BudgetPhase.DangSoanThao,
|
||||
BudgetPhase.TraLai,
|
||||
BudgetPhase.ChoCCM,
|
||||
BudgetPhase.ChoCEO,
|
||||
BudgetPhase.DaDuyet,
|
||||
|
||||
@ -1,25 +1,28 @@
|
||||
namespace SolutionErp.Domain.Contracts;
|
||||
|
||||
// State machine HĐ — Session 16 drastic refactor (Mig 21):
|
||||
// DangSoanThao → ChoDuyet (Drafter trình, init CurrentWorkflowStepIndex=0)
|
||||
// ChoDuyet → ChoDuyet (advance step pointer per approve)
|
||||
// ChoDuyet → DaPhatHanh (last step done — terminal)
|
||||
// ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex, Drafter sửa)
|
||||
// ChoDuyet → TuChoi (Từ chối — terminal khoá)
|
||||
// State machine HĐ — Session 17 spec mới (5 trạng thái):
|
||||
// DangSoanThao (Nháp) ──Drafter trình──► ChoDuyet
|
||||
// TraLai (Trả lại) ──Drafter sửa+gửi lại──► ChoDuyet (chạy lại từ Cấp 1 Bước 1)
|
||||
// ChoDuyet (Đã gửi duyệt) ──advance step pointer──► ChoDuyet
|
||||
// ──last step done──────────► DaPhatHanh (terminal + gen mã HĐ)
|
||||
// ──Approver Trả lại────────► TraLai
|
||||
// ──Approver Từ chối────────► TuChoi (terminal)
|
||||
//
|
||||
// LEGACY values (DangChon, DangGopY, DangDamPhan, DangInKy, DangKiemTraCCM,
|
||||
// DangTrinhKy, DangDongDau) deprecated post-Mig 21 — giữ enum cho data cũ.
|
||||
// TraLai=98: Session 17 thêm mới — Trả lại là Phase RIÊNG (mirror PE).
|
||||
public enum ContractPhase
|
||||
{
|
||||
DangChon = 1, // [LEGACY]
|
||||
DangSoanThao = 2,
|
||||
DangSoanThao = 2, // Nháp
|
||||
DangGopY = 3, // [LEGACY]
|
||||
DangDamPhan = 4, // [LEGACY]
|
||||
DangInKy = 5, // [LEGACY]
|
||||
DangKiemTraCCM = 6, // [LEGACY]
|
||||
DangTrinhKy = 7, // [LEGACY]
|
||||
DangDongDau = 8, // [LEGACY]
|
||||
DaPhatHanh = 9, // terminal thành công (= DaDuyet cho HĐ)
|
||||
ChoDuyet = 10, // [Mig 21] generic intermediate, dùng CurrentWorkflowStepIndex tracking
|
||||
TuChoi = 99, // terminal khoá
|
||||
DaPhatHanh = 9, // Đã duyệt (cho HĐ — gen mã + phát hành) — terminal thành công
|
||||
ChoDuyet = 10, // Đã gửi duyệt — generic intermediate, CurrentWorkflowStepIndex tracking
|
||||
TraLai = 98, // Trả lại — Phase riêng, Drafter sửa rồi gửi lại chạy từ đầu
|
||||
TuChoi = 99, // Từ chối — terminal khoá
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ public static class WorkflowPolicies
|
||||
private static readonly Dictionary<ContractPhase, TimeSpan?> DefaultSla = new()
|
||||
{
|
||||
[ContractPhase.DangSoanThao] = TimeSpan.FromDays(7),
|
||||
[ContractPhase.TraLai] = TimeSpan.FromDays(7),
|
||||
[ContractPhase.DangGopY] = TimeSpan.FromDays(7),
|
||||
[ContractPhase.DangDamPhan] = TimeSpan.FromDays(7),
|
||||
[ContractPhase.DangInKy] = TimeSpan.FromDays(1),
|
||||
@ -65,6 +66,8 @@ public static class WorkflowPolicies
|
||||
|
||||
// ===== STANDARD: 9-phase formal workflow =====
|
||||
// Per QT-TP-NCC.docx: Thầu phụ / NCC / Tổ đội — full CCM review required.
|
||||
// Session 17: Reject = về TraLai (Phase riêng). Drafter từ TraLai gửi lại
|
||||
// = entry point thứ 2 (mirror DangSoanThao → DangGopY).
|
||||
public static readonly WorkflowPolicy Standard = new(
|
||||
Name: "Standard",
|
||||
Description: "Quy trình đầy đủ 8 phase — CCM kiểm tra + BOD duyệt. Áp dụng HĐ Thầu phụ / NCC / Giao khoán.",
|
||||
@ -72,26 +75,28 @@ public static class WorkflowPolicies
|
||||
{
|
||||
[(ContractPhase.DangSoanThao, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.DangSoanThao, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.TraLai, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.TraLai, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
[(ContractPhase.DangGopY, ContractPhase.DangDamPhan)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.DangGopY, ContractPhase.DangSoanThao)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
|
||||
[(ContractPhase.DangGopY, ContractPhase.TraLai)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
|
||||
|
||||
[(ContractPhase.DangDamPhan, ContractPhase.DangInKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
|
||||
[(ContractPhase.DangInKy, ContractPhase.DangKiemTraCCM)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
|
||||
[(ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy)] = [AppRoles.CostControl],
|
||||
[(ContractPhase.DangKiemTraCCM, ContractPhase.DangSoanThao)] = [AppRoles.CostControl],
|
||||
[(ContractPhase.DangKiemTraCCM, ContractPhase.TraLai)] = [AppRoles.CostControl],
|
||||
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.DangDongDau)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
|
||||
[(ContractPhase.DangDongDau, ContractPhase.DaPhatHanh)] = [AppRoles.HrAdmin],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
ContractPhase.DangSoanThao, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
|
||||
ContractPhase.DangSoanThao, ContractPhase.TraLai, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
|
||||
ContractPhase.DangInKy, ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy,
|
||||
ContractPhase.DangDongDau, ContractPhase.DaPhatHanh, ContractPhase.TuChoi,
|
||||
]);
|
||||
@ -106,9 +111,11 @@ public static class WorkflowPolicies
|
||||
{
|
||||
[(ContractPhase.DangSoanThao, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.DangSoanThao, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.TraLai, ContractPhase.DangGopY)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.TraLai, ContractPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
[(ContractPhase.DangGopY, ContractPhase.DangDamPhan)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(ContractPhase.DangGopY, ContractPhase.DangSoanThao)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
|
||||
[(ContractPhase.DangGopY, ContractPhase.TraLai)] = [AppRoles.ProjectManager, AppRoles.Procurement, AppRoles.CostControl, AppRoles.Finance, AppRoles.Accounting, AppRoles.Equipment],
|
||||
|
||||
[(ContractPhase.DangDamPhan, ContractPhase.DangInKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
|
||||
@ -116,14 +123,14 @@ public static class WorkflowPolicies
|
||||
[(ContractPhase.DangInKy, ContractPhase.DangTrinhKy)] = [AppRoles.Drafter, AppRoles.DeptManager, AppRoles.ProjectManager],
|
||||
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.DangDongDau)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(ContractPhase.DangTrinhKy, ContractPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
|
||||
[(ContractPhase.DangDongDau, ContractPhase.DaPhatHanh)] = [AppRoles.HrAdmin],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
ContractPhase.DangSoanThao, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
|
||||
ContractPhase.DangSoanThao, ContractPhase.TraLai, ContractPhase.DangGopY, ContractPhase.DangDamPhan,
|
||||
ContractPhase.DangInKy, ContractPhase.DangTrinhKy,
|
||||
ContractPhase.DangDongDau, ContractPhase.DaPhatHanh, ContractPhase.TuChoi,
|
||||
]);
|
||||
@ -213,22 +220,35 @@ public static class WorkflowPolicyRegistry
|
||||
transitions[(prev.Value, s.Phase)] = roles;
|
||||
if (userIds.Length > 0) userTransitions[(prev.Value, s.Phase)] = userIds;
|
||||
|
||||
// Reject path back to Drafter (common pattern QT docx)
|
||||
// Mirror: TraLai → s.Phase (Drafter resubmit từ Trả lại = entry point thứ 2)
|
||||
if (prev.Value == ContractPhase.DangSoanThao)
|
||||
{
|
||||
transitions.TryAdd((ContractPhase.TraLai, s.Phase), roles);
|
||||
if (userIds.Length > 0)
|
||||
userTransitions.TryAdd((ContractPhase.TraLai, s.Phase), userIds);
|
||||
}
|
||||
|
||||
// Reject path → TraLai (Phase riêng, không revert DangSoanThao)
|
||||
if (prev.Value != ContractPhase.DangSoanThao && s.Phase != ContractPhase.DangSoanThao)
|
||||
{
|
||||
transitions.TryAdd((s.Phase, ContractPhase.DangSoanThao), roles);
|
||||
transitions.TryAdd((s.Phase, ContractPhase.TraLai), roles);
|
||||
if (userIds.Length > 0)
|
||||
userTransitions.TryAdd((s.Phase, ContractPhase.DangSoanThao), userIds);
|
||||
userTransitions.TryAdd((s.Phase, ContractPhase.TraLai), userIds);
|
||||
}
|
||||
}
|
||||
prev = s.Phase;
|
||||
}
|
||||
// First step có thể reject to TuChoi
|
||||
// First step có thể reject to TuChoi (cả Nháp + Trả lại)
|
||||
if (steps.Count > 0)
|
||||
{
|
||||
transitions.TryAdd((steps[0].Phase, ContractPhase.TuChoi),
|
||||
[AppRoles.Drafter, AppRoles.DeptManager]);
|
||||
transitions.TryAdd((ContractPhase.TraLai, ContractPhase.TuChoi),
|
||||
[AppRoles.Drafter, AppRoles.DeptManager]);
|
||||
}
|
||||
|
||||
if (!activePhases.Contains(ContractPhase.TuChoi)) activePhases.Add(ContractPhase.TuChoi);
|
||||
if (!activePhases.Contains(ContractPhase.TraLai)) activePhases.Add(ContractPhase.TraLai);
|
||||
|
||||
return new WorkflowPolicy(
|
||||
Name: $"{def.Code}-v{def.Version:D2}",
|
||||
|
||||
@ -1,25 +1,27 @@
|
||||
namespace SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
// State machine PE — Session 16 drastic refactor (Mig 21):
|
||||
// DangSoanThao → ChoDuyet (Drafter trình, init CurrentWorkflowStepIndex=0)
|
||||
// ChoDuyet → ChoDuyet (advance step pointer mỗi lần approve)
|
||||
// ChoDuyet → DaDuyet (last step done — terminal thành công)
|
||||
// ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex, Drafter sửa)
|
||||
// ChoDuyet → TuChoi (Từ chối — terminal khoá phiếu)
|
||||
// State machine PE — Session 17 spec mới (5 trạng thái):
|
||||
// DangSoanThao (Nháp) ──Drafter trình──► ChoDuyet
|
||||
// TraLai (Trả lại) ──Drafter sửa+gửi lại──► ChoDuyet (chạy lại từ Cấp 1 Bước 1)
|
||||
// ChoDuyet (Đã gửi duyệt) ──advance step pointer──► ChoDuyet
|
||||
// ──last step done──────────► DaDuyet (terminal)
|
||||
// ──Approver Trả lại────────► TraLai
|
||||
// ──Approver Từ chối────────► TuChoi (terminal)
|
||||
// DangSoanThao → TuChoi (Drafter huỷ trước trình)
|
||||
//
|
||||
// LEGACY values 2-6 + 98 deprecated post-Mig 21 (data cũ vẫn đọc OK,
|
||||
// new workflow definitions chỉ dùng DangSoanThao/ChoDuyet/DaDuyet/TuChoi).
|
||||
// LEGACY values 2-6 deprecated post-Mig 21 (data cũ vẫn đọc OK).
|
||||
// TraLai=98: Session 17 restore làm primary state — Trả lại là Phase RIÊNG,
|
||||
// không revert về DangSoanThao như Mig 21.
|
||||
public enum PurchaseEvaluationPhase
|
||||
{
|
||||
DangSoanThao = 1,
|
||||
ChoPurchasing = 2, // [LEGACY] deprecated
|
||||
ChoDuAn = 3, // [LEGACY] deprecated
|
||||
ChoCCM = 4, // [LEGACY] deprecated
|
||||
ChoCEODuyetPA = 5, // [LEGACY] deprecated
|
||||
ChoCEODuyetNCC = 6, // [LEGACY] deprecated
|
||||
DaDuyet = 7, // terminal thành công
|
||||
ChoDuyet = 10, // [Mig 21] generic intermediate, dùng CurrentWorkflowStepIndex tracking
|
||||
TraLai = 98, // [LEGACY] deprecated — Session 14 chốt thay bằng Trả lại = về DangSoanThao
|
||||
TuChoi = 99, // terminal từ chối — KHÔNG cho edit/thao tác
|
||||
DangSoanThao = 1, // Nháp
|
||||
ChoPurchasing = 2, // [LEGACY] deprecated
|
||||
ChoDuAn = 3, // [LEGACY] deprecated
|
||||
ChoCCM = 4, // [LEGACY] deprecated
|
||||
ChoCEODuyetPA = 5, // [LEGACY] deprecated
|
||||
ChoCEODuyetNCC = 6, // [LEGACY] deprecated
|
||||
DaDuyet = 7, // Đã duyệt — terminal thành công
|
||||
ChoDuyet = 10, // Đã gửi duyệt — generic intermediate, CurrentWorkflowStepIndex tracking
|
||||
TraLai = 98, // Trả lại — Phase riêng, Drafter sửa rồi gửi lại chạy từ đầu
|
||||
TuChoi = 99, // Từ chối — terminal khoá phiếu, KHÔNG cho edit/thao tác
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ public static class PurchaseEvaluationPolicies
|
||||
private static readonly Dictionary<PurchaseEvaluationPhase, TimeSpan?> DefaultSla = new()
|
||||
{
|
||||
[PurchaseEvaluationPhase.DangSoanThao] = TimeSpan.FromDays(3),
|
||||
[PurchaseEvaluationPhase.TraLai] = TimeSpan.FromDays(3),
|
||||
[PurchaseEvaluationPhase.ChoPurchasing] = TimeSpan.FromDays(2),
|
||||
[PurchaseEvaluationPhase.ChoDuAn] = TimeSpan.FromDays(2),
|
||||
[PurchaseEvaluationPhase.ChoCCM] = TimeSpan.FromDays(2),
|
||||
@ -48,33 +49,37 @@ public static class PurchaseEvaluationPolicies
|
||||
};
|
||||
|
||||
// A — DuyetNcc (3 step thực + Drafter soạn): Drafter → Purchasing → CCM → CEO
|
||||
// Session 17 spec: Reject = về TraLai (Phase riêng, không revert DangSoanThao).
|
||||
// Drafter từ TraLai gửi lại = entry point thứ 2 (mirror DangSoanThao → ChoPurchasing).
|
||||
public static readonly PurchaseEvaluationPolicy NccOnly = new(
|
||||
Name: "NccOnly",
|
||||
Description: "Duyệt NCC — 3 step (Purchasing → CCM → CEO). Không cần duyệt phương án.",
|
||||
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
|
||||
{
|
||||
// Drafter trình từ Nháp HOẶC gửi lại từ Trả lại — cùng entry point.
|
||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại Drafter (DangSoanThao) / Từ chối hoàn toàn (TuChoi).
|
||||
// Trả lại = smart reject pattern Mig 16 (set RejectedFromPhase + về DangSoanThao + Drafter sửa).
|
||||
// Từ chối = phiếu khoá hoàn toàn (Phase=TuChoi → 17 handler Mig 16 lock edit).
|
||||
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai Phase riêng) / Từ chối (TuChoi terminal).
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.CostControl],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
PurchaseEvaluationPhase.DangSoanThao,
|
||||
PurchaseEvaluationPhase.TraLai,
|
||||
PurchaseEvaluationPhase.ChoPurchasing,
|
||||
PurchaseEvaluationPhase.ChoCCM,
|
||||
PurchaseEvaluationPhase.ChoCEODuyetNCC,
|
||||
@ -90,32 +95,35 @@ public static class PurchaseEvaluationPolicies
|
||||
{
|
||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại / Từ chối (xem comment NccOnly).
|
||||
// Phase trung gian: 3 hành động — Duyệt forward / Trả lại (TraLai) / Từ chối (TuChoi).
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Procurement],
|
||||
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Procurement],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
|
||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.ProjectManager],
|
||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TraLai)] = [AppRoles.ProjectManager],
|
||||
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.ProjectManager],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai)] = [AppRoles.CostControl],
|
||||
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.CostControl],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
PurchaseEvaluationPhase.DangSoanThao,
|
||||
PurchaseEvaluationPhase.TraLai,
|
||||
PurchaseEvaluationPhase.ChoPurchasing,
|
||||
PurchaseEvaluationPhase.ChoDuAn,
|
||||
PurchaseEvaluationPhase.ChoCCM,
|
||||
@ -183,30 +191,45 @@ public static class PurchaseEvaluationPolicyRegistry
|
||||
transitions[(prev.Value, s.Phase)] = roles;
|
||||
if (userIds.Length > 0) userTransitions[(prev.Value, s.Phase)] = userIds;
|
||||
|
||||
// 3 hành động phase trung gian — Duyệt forward + Trả lại Drafter + Từ chối hoàn toàn
|
||||
// Mirror: TraLai → s.Phase cho cả Drafter (resubmit từ Trả lại = entry point thứ 2)
|
||||
if (prev.Value == PurchaseEvaluationPhase.DangSoanThao)
|
||||
{
|
||||
transitions.TryAdd((PurchaseEvaluationPhase.TraLai, s.Phase), roles);
|
||||
if (userIds.Length > 0)
|
||||
userTransitions.TryAdd((PurchaseEvaluationPhase.TraLai, s.Phase), userIds);
|
||||
}
|
||||
|
||||
// 3 hành động phase trung gian — Duyệt forward + Trả lại (TraLai) + Từ chối (TuChoi)
|
||||
if (prev.Value != PurchaseEvaluationPhase.DangSoanThao && s.Phase != PurchaseEvaluationPhase.DangSoanThao)
|
||||
{
|
||||
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), roles);
|
||||
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), roles);
|
||||
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), roles);
|
||||
if (userIds.Length > 0)
|
||||
{
|
||||
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), userIds);
|
||||
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TraLai), userIds);
|
||||
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.TuChoi), userIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
prev = s.Phase;
|
||||
}
|
||||
// First step (DangSoanThao) — Drafter có thể TuChoi (huỷ phiếu)
|
||||
// First step (DangSoanThao) — Drafter có thể TuChoi (huỷ phiếu).
|
||||
// Tương tự cho TraLai — Drafter có thể TuChoi luôn từ Trả lại.
|
||||
if (steps.Count > 0)
|
||||
{
|
||||
transitions.TryAdd((steps[0].Phase, PurchaseEvaluationPhase.TuChoi),
|
||||
[AppRoles.Drafter, AppRoles.DeptManager]);
|
||||
transitions.TryAdd((PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.TuChoi),
|
||||
[AppRoles.Drafter, AppRoles.DeptManager]);
|
||||
}
|
||||
|
||||
// Terminal states always available
|
||||
// Terminal states always available + TraLai phase
|
||||
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))
|
||||
activePhases.Add(PurchaseEvaluationPhase.TuChoi);
|
||||
if (!activePhases.Contains(PurchaseEvaluationPhase.DaDuyet))
|
||||
activePhases.Add(PurchaseEvaluationPhase.DaDuyet);
|
||||
if (!activePhases.Contains(PurchaseEvaluationPhase.TraLai))
|
||||
activePhases.Add(PurchaseEvaluationPhase.TraLai);
|
||||
|
||||
return new PurchaseEvaluationPolicy(
|
||||
Name: $"{def.Code}-v{def.Version:D2}",
|
||||
|
||||
@ -12,7 +12,7 @@ public class ContractExcelExporter(IApplicationDbContext db, IDateTime dateTime)
|
||||
private static readonly Dictionary<ContractPhase, string> PhaseLabel = new()
|
||||
{
|
||||
[ContractPhase.DangChon] = "Đang chọn NCC",
|
||||
[ContractPhase.DangSoanThao] = "Đang soạn thảo",
|
||||
[ContractPhase.DangSoanThao] = "Nháp",
|
||||
[ContractPhase.DangGopY] = "Đang góp ý",
|
||||
[ContractPhase.DangDamPhan] = "Đang đàm phán",
|
||||
[ContractPhase.DangInKy] = "Đang in ký",
|
||||
@ -20,6 +20,8 @@ public class ContractExcelExporter(IApplicationDbContext db, IDateTime dateTime)
|
||||
[ContractPhase.DangTrinhKy] = "Đang trình ký",
|
||||
[ContractPhase.DangDongDau] = "Đang đóng dấu",
|
||||
[ContractPhase.DaPhatHanh] = "Đã phát hành",
|
||||
[ContractPhase.ChoDuyet] = "Đã gửi duyệt",
|
||||
[ContractPhase.TraLai] = "Trả lại",
|
||||
[ContractPhase.TuChoi] = "Từ chối",
|
||||
};
|
||||
|
||||
|
||||
@ -11,10 +11,15 @@ using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// Contract Workflow Service — Session 16 drastic refactor (Mig 21):
|
||||
// Flat workflow model. Mỗi step = 1 (Phòng × Cấp + Approvers). Service iterate
|
||||
// steps OrderBy Order, advance Contract.CurrentWorkflowStepIndex per approve.
|
||||
// Phase enum simplified: DangSoanThao → ChoDuyet → DaPhatHanh / TuChoi.
|
||||
// Contract Workflow Service — Session 17 spec mới (state machine 5 trạng thái):
|
||||
// Nháp (DangSoanThao) ──trình──► Đã gửi duyệt (ChoDuyet) ─approve cấp cuối─► Đã phát hành (DaPhatHanh, terminal + gen mã)
|
||||
// ├─ Trả lại ────────► Trả lại (TraLai)
|
||||
// └─ Từ chối ────────► Từ chối (TuChoi, terminal)
|
||||
// Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu, KHÔNG jump-back)
|
||||
//
|
||||
// Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại = Phase RIÊNG
|
||||
// (TraLai=98), không revert về DangSoanThao. Drafter từ TraLai gửi lại như
|
||||
// case Nháp — workflow chạy lại từ Cấp 1 Bước 1.
|
||||
public class ContractWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IContractCodeGenerator codeGenerator,
|
||||
@ -48,9 +53,9 @@ public class ContractWorkflowService(
|
||||
}
|
||||
else
|
||||
{
|
||||
contract.RejectedFromPhase = fromPhase;
|
||||
contract.RejectedAtStepIndex = contract.CurrentWorkflowStepIndex;
|
||||
contract.Phase = ContractPhase.DangSoanThao;
|
||||
// Trả lại — Phase=TraLai RIÊNG (không revert về DangSoanThao).
|
||||
// Drafter sửa từ TraLai rồi gửi lại sẽ chạy lại từ Cấp 1 Bước 1.
|
||||
contract.Phase = ContractPhase.TraLai;
|
||||
contract.CurrentWorkflowStepIndex = null;
|
||||
}
|
||||
contract.SlaDeadline = null;
|
||||
@ -59,25 +64,9 @@ public class ContractWorkflowService(
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== RESUME AFTER REJECT =====
|
||||
var isResumingAfterReject = decision == ApprovalDecision.Approve
|
||||
&& fromPhase == ContractPhase.DangSoanThao
|
||||
&& contract.RejectedAtStepIndex != null;
|
||||
|
||||
if (isResumingAfterReject)
|
||||
{
|
||||
contract.Phase = ContractPhase.ChoDuyet;
|
||||
contract.CurrentWorkflowStepIndex = contract.RejectedAtStepIndex;
|
||||
contract.RejectedAtStepIndex = null;
|
||||
contract.RejectedFromPhase = null;
|
||||
contract.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||
await LogTransitionAsync(contract, fromPhase, ContractPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== DRAFTER TRÌNH =====
|
||||
if (fromPhase == ContractPhase.DangSoanThao
|
||||
// ===== DRAFTER TRÌNH/GỬI LẠI (Nháp HOẶC Trả lại → ChoDuyet) =====
|
||||
// Cả 2 entry point cùng logic: chạy lại từ đầu (CurrentWorkflowStepIndex=0).
|
||||
if ((fromPhase == ContractPhase.DangSoanThao || fromPhase == ContractPhase.TraLai)
|
||||
&& (targetPhase == ContractPhase.ChoDuyet || (!isAdmin && !isSystem)))
|
||||
{
|
||||
if (!isAdmin && !isSystem
|
||||
@ -213,7 +202,7 @@ public class ContractWorkflowService(
|
||||
NotificationType.ContractPublished),
|
||||
ContractPhase.TuChoi => ($"HĐ {contract.TenHopDong ?? "của bạn"} bị từ chối",
|
||||
NotificationType.ContractRejected),
|
||||
ContractPhase.DangSoanThao when fromPhase == ContractPhase.ChoDuyet =>
|
||||
ContractPhase.TraLai when fromPhase == ContractPhase.ChoDuyet =>
|
||||
($"HĐ {contract.TenHopDong ?? "của bạn"} bị trả lại — vui lòng sửa và trình lại",
|
||||
NotificationType.ContractRejected),
|
||||
_ => ($"HĐ {contract.TenHopDong ?? contract.MaHopDong ?? ""} chuyển phase mới",
|
||||
|
||||
@ -12,11 +12,17 @@ using SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// PE Workflow Service — Session 16 drastic refactor (Mig 21):
|
||||
// Flat workflow model. Mỗi step = 1 (Phòng × Cấp + Approvers). Service iterate
|
||||
// steps OrderBy Order, advance PE.CurrentWorkflowStepIndex per approve.
|
||||
// Phase enum simplified: DangSoanThao → ChoDuyet (active workflow) → DaDuyet
|
||||
// (terminal) / TuChoi (khoá). Trả lại = về DangSoanThao + save RejectedAtStepIndex.
|
||||
// PE Workflow Service — Session 17 spec mới (state machine 5 trạng thái):
|
||||
// Nháp (DangSoanThao) ──trình──► Đã gửi duyệt (ChoDuyet) ─approve cấp cuối─► Đã duyệt (DaDuyet, terminal)
|
||||
// ├─ Trả lại ────────► Trả lại (TraLai)
|
||||
// └─ Từ chối ────────► Từ chối (TuChoi, terminal)
|
||||
// Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu, KHÔNG jump-back)
|
||||
//
|
||||
// Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại giờ là Phase
|
||||
// RIÊNG (TraLai=98), không revert về DangSoanThao. Drafter từ TraLai gửi lại
|
||||
// như case Nháp — workflow chạy lại từ Cấp 1 Bước 1. Field RejectedAtStepIndex
|
||||
// + RejectedFromPhase giữ DB column (nullable, không set value mới) cho data
|
||||
// cũ — sẽ cleanup migration sau.
|
||||
public class PurchaseEvaluationWorkflowService(
|
||||
IApplicationDbContext db,
|
||||
IDateTime dateTime,
|
||||
@ -49,10 +55,9 @@ public class PurchaseEvaluationWorkflowService(
|
||||
}
|
||||
else
|
||||
{
|
||||
// Trả lại — về DangSoanThao + save RejectedAtStepIndex (resume jump-back).
|
||||
evaluation.RejectedFromPhase = fromPhase;
|
||||
evaluation.RejectedAtStepIndex = evaluation.CurrentWorkflowStepIndex;
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DangSoanThao;
|
||||
// Trả lại — Phase=TraLai RIÊNG (không revert về DangSoanThao).
|
||||
// Drafter sửa từ TraLai rồi gửi lại sẽ chạy lại từ Cấp 1 Bước 1.
|
||||
evaluation.Phase = PurchaseEvaluationPhase.TraLai;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
}
|
||||
evaluation.SlaDeadline = null;
|
||||
@ -61,25 +66,10 @@ public class PurchaseEvaluationWorkflowService(
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== RESUME AFTER REJECT (Drafter trình lại) =====
|
||||
var isResumingAfterReject = decision == ApprovalDecision.Approve
|
||||
&& fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
&& evaluation.RejectedAtStepIndex != null;
|
||||
|
||||
if (isResumingAfterReject)
|
||||
{
|
||||
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = evaluation.RejectedAtStepIndex;
|
||||
evaluation.RejectedAtStepIndex = null;
|
||||
evaluation.RejectedFromPhase = null;
|
||||
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||
await LogTransitionAsync(evaluation, fromPhase, PurchaseEvaluationPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===== DRAFTER TRÌNH (DangSoanThao → ChoDuyet) =====
|
||||
if (fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
// ===== DRAFTER TRÌNH/GỬI LẠI (Nháp HOẶC Trả lại → ChoDuyet) =====
|
||||
// Cả 2 entry point cùng logic: chạy lại từ đầu (CurrentWorkflowStepIndex=0).
|
||||
if ((fromPhase == PurchaseEvaluationPhase.DangSoanThao
|
||||
|| fromPhase == PurchaseEvaluationPhase.TraLai)
|
||||
&& (targetPhase == PurchaseEvaluationPhase.ChoDuyet || !isAdmin && !isSystem))
|
||||
{
|
||||
// Drafter/DeptManager only (or Admin bypass).
|
||||
@ -230,7 +220,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
NotificationType.ContractPublished),
|
||||
PurchaseEvaluationPhase.TuChoi => ($"Phiếu {evaluation.TenGoiThau} bị từ chối",
|
||||
NotificationType.ContractRejected),
|
||||
PurchaseEvaluationPhase.DangSoanThao when fromPhase == PurchaseEvaluationPhase.ChoDuyet =>
|
||||
PurchaseEvaluationPhase.TraLai when fromPhase == PurchaseEvaluationPhase.ChoDuyet =>
|
||||
($"Phiếu {evaluation.TenGoiThau} bị trả lại — vui lòng sửa và trình lại",
|
||||
NotificationType.ContractRejected),
|
||||
_ => ($"Phiếu {evaluation.TenGoiThau} chuyển phase mới",
|
||||
|
||||
@ -45,11 +45,11 @@ public class BudgetPolicyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_CostControl_ChoCCM_To_DangSoanThao_Allowed()
|
||||
public void Default_CostControl_ChoCCM_To_TraLai_Allowed()
|
||||
{
|
||||
// Trả về Drafter
|
||||
// Session 17 spec: Trả lại = Phase riêng (TraLai), không revert DangSoanThao
|
||||
BudgetPolicies.Default
|
||||
.IsTransitionAllowed(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao,
|
||||
.IsTransitionAllowed(BudgetPhase.ChoCCM, BudgetPhase.TraLai,
|
||||
[AppRoles.CostControl])
|
||||
.Should().BeTrue();
|
||||
}
|
||||
@ -93,11 +93,12 @@ public class BudgetPolicyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_ActivePhases_Includes_All5States()
|
||||
public void Default_ActivePhases_Includes_All6States()
|
||||
{
|
||||
// Session 17: thêm TraLai = Phase riêng cho Trả lại
|
||||
BudgetPolicies.Default.ActivePhases.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM,
|
||||
BudgetPhase.DangSoanThao, BudgetPhase.TraLai, BudgetPhase.ChoCCM,
|
||||
BudgetPhase.ChoCEO, BudgetPhase.DaDuyet, BudgetPhase.TuChoi,
|
||||
});
|
||||
}
|
||||
@ -111,11 +112,21 @@ public class BudgetPolicyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_NextPhasesFrom_ChoCEO_Includes_DaDuyet_And_DangSoanThao()
|
||||
public void Default_NextPhasesFrom_TraLai_Includes_ChoCCM_And_TuChoi()
|
||||
{
|
||||
// Drafter từ TraLai gửi lại = entry point thứ 2 (mirror DangSoanThao)
|
||||
var next = BudgetPolicies.Default.NextPhasesFrom(BudgetPhase.TraLai);
|
||||
next.Should().Contain(BudgetPhase.ChoCCM);
|
||||
next.Should().Contain(BudgetPhase.TuChoi);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_NextPhasesFrom_ChoCEO_Includes_DaDuyet_And_TraLai()
|
||||
{
|
||||
var next = BudgetPolicies.Default.NextPhasesFrom(BudgetPhase.ChoCEO);
|
||||
next.Should().Contain(BudgetPhase.DaDuyet);
|
||||
next.Should().Contain(BudgetPhase.DangSoanThao);
|
||||
next.Should().Contain(BudgetPhase.TraLai);
|
||||
next.Should().Contain(BudgetPhase.TuChoi);
|
||||
}
|
||||
|
||||
// SLA — chống regression khi đổi phase deadline accidentally
|
||||
|
||||
@ -90,12 +90,23 @@ public class WorkflowPolicyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Standard_RejectFromCCM_GoesBackToDraft()
|
||||
public void Standard_RejectFromCCM_GoesTo_TraLai()
|
||||
{
|
||||
// Session 17 spec: Trả lại = Phase riêng (TraLai), không revert DangSoanThao
|
||||
WorkflowPolicies.Standard
|
||||
.IsTransitionAllowed(ContractPhase.DangKiemTraCCM, ContractPhase.DangSoanThao,
|
||||
.IsTransitionAllowed(ContractPhase.DangKiemTraCCM, ContractPhase.TraLai,
|
||||
[AppRoles.CostControl])
|
||||
.Should().BeTrue("CCM có quyền reject về Drafter");
|
||||
.Should().BeTrue("CCM có quyền reject về TraLai");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Standard_TraLai_To_DangGopY_Allowed_For_Drafter()
|
||||
{
|
||||
// Drafter từ TraLai gửi lại = entry point thứ 2 (mirror DangSoanThao → DangGopY)
|
||||
WorkflowPolicies.Standard
|
||||
.IsTransitionAllowed(ContractPhase.TraLai, ContractPhase.DangGopY,
|
||||
[AppRoles.Drafter])
|
||||
.Should().BeTrue("Drafter resubmit từ Trả lại");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@ -131,17 +131,33 @@ public class PurchaseEvaluationPolicyTests
|
||||
[Theory]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccWithPlan))]
|
||||
public void BothPolicies_RejectFromCCM_GoesBackTo_DangSoanThao(string policyName)
|
||||
public void BothPolicies_RejectFromCCM_GoesTo_TraLai(string policyName)
|
||||
{
|
||||
// Session 17 spec: Trả lại = Phase riêng (TraLai), không revert DangSoanThao
|
||||
var policy = policyName == nameof(PurchaseEvaluationPolicies.NccOnly)
|
||||
? PurchaseEvaluationPolicies.NccOnly
|
||||
: PurchaseEvaluationPolicies.NccWithPlan;
|
||||
|
||||
policy.IsTransitionAllowed(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao,
|
||||
policy.IsTransitionAllowed(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.TraLai,
|
||||
[AppRoles.CostControl])
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccWithPlan))]
|
||||
public void BothPolicies_TraLai_To_ChoPurchasing_AllowedForDrafter(string policyName)
|
||||
{
|
||||
// Drafter từ TraLai gửi lại = entry point thứ 2 (mirror DangSoanThao)
|
||||
var policy = policyName == nameof(PurchaseEvaluationPolicies.NccOnly)
|
||||
? PurchaseEvaluationPolicies.NccOnly
|
||||
: PurchaseEvaluationPolicies.NccWithPlan;
|
||||
|
||||
policy.IsTransitionAllowed(PurchaseEvaluationPhase.TraLai, PurchaseEvaluationPhase.ChoPurchasing,
|
||||
[AppRoles.Drafter])
|
||||
.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccOnly))]
|
||||
[InlineData(nameof(PurchaseEvaluationPolicies.NccWithPlan))]
|
||||
|
||||
Reference in New Issue
Block a user