[CLAUDE] Contract: W2 BE-DTO nền GĐ3 — ContractSourceDto reverse-join KHKK + CurrentWorkflowStepIndex + WorkflowSteps cây thô khuôn KHKK; types ×2 app khớp tên JSON BE (vá reviewer F-1/F-2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -170,6 +170,67 @@ export type ContractDetail = {
|
||||
approvalWorkflowId: string | null
|
||||
currentApprovalLevelOrder: number | null
|
||||
levelOpinions: ContractLevelOpinion[] | null
|
||||
|
||||
// ========= [W2 S187 · YC-023 GĐ3] Nguồn gốc KHKK + cây quy trình V2 (3 field) ==========
|
||||
// 🔴 Khai `?` KÈM `| null` CÓ CHỦ ĐÍCH — 2 nghĩa KHÁC NHAU, đừng gộp:
|
||||
// • `undefined` (vắng key) = payload đến từ API/bundle CHƯA có W2, hoặc cache TanStack cũ;
|
||||
// • `null` = BE đã trả lời "HĐ này KHÔNG có nguồn KHKK / chưa pin quy trình".
|
||||
// BE append 3 field này ở CUỐI `ContractDetailDto` với default null (hôm nay field cuối là
|
||||
// `LevelOpinions` — `ContractDtos.cs:64`) ⇒ wire cũ không vỡ. Nơi đọc PHẢI null-check,
|
||||
// TUYỆT ĐỐI không suy "vắng field ⇒ HĐ tạo tay".
|
||||
/** HĐ kế thừa từ phiếu KHKK đã duyệt (bridge K7). HĐ tạo tay ⇒ null, FE không render card. */
|
||||
source?: ContractSource | null
|
||||
/** INDEX 0-based vào mảng `workflowSteps` (KHÔNG phải `order`) — mirror khuôn KHKK. */
|
||||
currentWorkflowStepIndex?: number | null
|
||||
/** Cây quy trình THÔ (xem `ContractWorkflowStep`). */
|
||||
workflowSteps?: ContractWorkflowStep[] | null
|
||||
}
|
||||
|
||||
// [W2 S187 · YC-023] Khối "Nguồn gốc KHKK" — BE reverse-join `ContractSigningPlanLines
|
||||
// WHERE ContractId = id` → phiếu KHKK + hạng mục + Σ tiền (spec `§2 W2(a)`). Link nằm ở
|
||||
// `ContractSigningPlanLine.ContractId`, KHÔNG có con-trỏ ngược trên `Contract` ⇒ HĐ 0 dòng
|
||||
// nối trả null.
|
||||
// ✅ FLAG-A ĐÓNG @S188 (reviewer F-1): BE đặt `Source` (`ContractDtos.cs:87`) đúng AC spec ⇒ FE
|
||||
// rename khớp JSON BE từng tên: `contractSource`→`source` · `tenHangMuc`→`tenHangMucs` ·
|
||||
// `totalApprovedAmount`→`approvedAmountTotal` (JSON bind theo TÊN; System.Text.Json camelCase).
|
||||
export type ContractSource = {
|
||||
/** Mã phiếu KHKK, vd `KHKK/2026/001`. null khi phiếu cha đã xoá mềm (F-2 — BE trả null qua global filter). */
|
||||
maKeHoach: string | null
|
||||
planId: string
|
||||
/** Nhóm duyệt N1–N8 của phiếu (Mig 71). null = phiếu không mang nhóm. */
|
||||
approvalGroup?: number | null
|
||||
/** Tên các hạng mục của phiếu đã nối sang HĐ này (có thể rỗng). */
|
||||
tenHangMucs: string[]
|
||||
/** Σ `ApprovedAmount` các dòng nối — số đã đóng băng tại choke-point finalize của phiếu. */
|
||||
approvedAmountTotal?: number | null
|
||||
/** Phiếu Duyệt NCC gốc — chuỗi PE → KHKK → HĐ. */
|
||||
purchaseEvaluationId?: string | null
|
||||
}
|
||||
|
||||
// [W2 S187] Cây quy trình THÔ do BE trả — mirror khuôn KHKK (`types/khkk.ts:275-296`, BE
|
||||
// `ContractSigningPlanFeatures.cs:131-152`).
|
||||
// 🔴 KHÁC PE: BE **KHÔNG** precompute `status` Done/Current/Pending. FE tự suy từ CẶP con-trỏ
|
||||
// `currentWorkflowStepIndex` (INDEX 0-based vào mảng steps) + `currentApprovalLevelOrder`
|
||||
// (GIÁ TRỊ `level.order`, KHÔNG phải index).
|
||||
export type ContractWorkflowLevel = {
|
||||
id: string
|
||||
order: number
|
||||
name: string | null
|
||||
approverUserId: string
|
||||
approverFullName: string | null
|
||||
/** Cờ per-Cấp "được KẾT THÚC quy trình tại cấp này" — spec `§2 W2(b)` gọi là "cờ per-level". */
|
||||
allowApproverFinalize: boolean
|
||||
}
|
||||
|
||||
export type ContractWorkflowStep = {
|
||||
id: string
|
||||
order: number
|
||||
name: string
|
||||
levels: ContractWorkflowLevel[]
|
||||
// 2 field dưới có trong khuôn KHKK nhưng spec W2 KHÔNG nêu tên ⇒ khai `?` để nơi đọc buộc
|
||||
// null-check, thay vì hứa một field payload có thể không mang (bài học `khkk.ts:403-407`).
|
||||
departmentId?: string | null
|
||||
departmentName?: string | null
|
||||
}
|
||||
|
||||
// [Plan B S29 2026-05-22 Chunk E3] Mirror BE ContractLevelOpinionDto 12 fields.
|
||||
|
||||
@ -174,6 +174,67 @@ export type ContractDetail = {
|
||||
approvalWorkflowId: string | null
|
||||
currentApprovalLevelOrder: number | null
|
||||
levelOpinions: ContractLevelOpinion[] | null
|
||||
|
||||
// ========= [W2 S187 · YC-023 GĐ3] Nguồn gốc KHKK + cây quy trình V2 (3 field) ==========
|
||||
// 🔴 Khai `?` KÈM `| null` CÓ CHỦ ĐÍCH — 2 nghĩa KHÁC NHAU, đừng gộp:
|
||||
// • `undefined` (vắng key) = payload đến từ API/bundle CHƯA có W2, hoặc cache TanStack cũ;
|
||||
// • `null` = BE đã trả lời "HĐ này KHÔNG có nguồn KHKK / chưa pin quy trình".
|
||||
// BE append 3 field này ở CUỐI `ContractDetailDto` với default null (hôm nay field cuối là
|
||||
// `LevelOpinions` — `ContractDtos.cs:64`) ⇒ wire cũ không vỡ. Nơi đọc PHẢI null-check,
|
||||
// TUYỆT ĐỐI không suy "vắng field ⇒ HĐ tạo tay".
|
||||
/** HĐ kế thừa từ phiếu KHKK đã duyệt (bridge K7). HĐ tạo tay ⇒ null, FE không render card. */
|
||||
source?: ContractSource | null
|
||||
/** INDEX 0-based vào mảng `workflowSteps` (KHÔNG phải `order`) — mirror khuôn KHKK. */
|
||||
currentWorkflowStepIndex?: number | null
|
||||
/** Cây quy trình THÔ (xem `ContractWorkflowStep`). */
|
||||
workflowSteps?: ContractWorkflowStep[] | null
|
||||
}
|
||||
|
||||
// [W2 S187 · YC-023] Khối "Nguồn gốc KHKK" — BE reverse-join `ContractSigningPlanLines
|
||||
// WHERE ContractId = id` → phiếu KHKK + hạng mục + Σ tiền (spec `§2 W2(a)`). Link nằm ở
|
||||
// `ContractSigningPlanLine.ContractId`, KHÔNG có con-trỏ ngược trên `Contract` ⇒ HĐ 0 dòng
|
||||
// nối trả null.
|
||||
// ✅ FLAG-A ĐÓNG @S188 (reviewer F-1): BE đặt `Source` (`ContractDtos.cs:87`) đúng AC spec ⇒ FE
|
||||
// rename khớp JSON BE từng tên: `contractSource`→`source` · `tenHangMuc`→`tenHangMucs` ·
|
||||
// `totalApprovedAmount`→`approvedAmountTotal` (JSON bind theo TÊN; System.Text.Json camelCase).
|
||||
export type ContractSource = {
|
||||
/** Mã phiếu KHKK, vd `KHKK/2026/001`. null khi phiếu cha đã xoá mềm (F-2 — BE trả null qua global filter). */
|
||||
maKeHoach: string | null
|
||||
planId: string
|
||||
/** Nhóm duyệt N1–N8 của phiếu (Mig 71). null = phiếu không mang nhóm. */
|
||||
approvalGroup?: number | null
|
||||
/** Tên các hạng mục của phiếu đã nối sang HĐ này (có thể rỗng). */
|
||||
tenHangMucs: string[]
|
||||
/** Σ `ApprovedAmount` các dòng nối — số đã đóng băng tại choke-point finalize của phiếu. */
|
||||
approvedAmountTotal?: number | null
|
||||
/** Phiếu Duyệt NCC gốc — chuỗi PE → KHKK → HĐ. */
|
||||
purchaseEvaluationId?: string | null
|
||||
}
|
||||
|
||||
// [W2 S187] Cây quy trình THÔ do BE trả — mirror khuôn KHKK (`types/khkk.ts:275-296`, BE
|
||||
// `ContractSigningPlanFeatures.cs:131-152`).
|
||||
// 🔴 KHÁC PE: BE **KHÔNG** precompute `status` Done/Current/Pending. FE tự suy từ CẶP con-trỏ
|
||||
// `currentWorkflowStepIndex` (INDEX 0-based vào mảng steps) + `currentApprovalLevelOrder`
|
||||
// (GIÁ TRỊ `level.order`, KHÔNG phải index).
|
||||
export type ContractWorkflowLevel = {
|
||||
id: string
|
||||
order: number
|
||||
name: string | null
|
||||
approverUserId: string
|
||||
approverFullName: string | null
|
||||
/** Cờ per-Cấp "được KẾT THÚC quy trình tại cấp này" — spec `§2 W2(b)` gọi là "cờ per-level". */
|
||||
allowApproverFinalize: boolean
|
||||
}
|
||||
|
||||
export type ContractWorkflowStep = {
|
||||
id: string
|
||||
order: number
|
||||
name: string
|
||||
levels: ContractWorkflowLevel[]
|
||||
// 2 field dưới có trong khuôn KHKK nhưng spec W2 KHÔNG nêu tên ⇒ khai `?` để nơi đọc buộc
|
||||
// null-check, thay vì hứa một field payload có thể không mang (bài học `khkk.ts:403-407`).
|
||||
departmentId?: string | null
|
||||
departmentName?: string | null
|
||||
}
|
||||
|
||||
// [Plan B S29 2026-05-22 Chunk E3] Mirror BE ContractLevelOpinionDto 12 fields.
|
||||
|
||||
@ -709,6 +709,88 @@ public class GetContractQueryHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// ---- [W2 S187 — YC-023 GĐ3] (a) NGUỒN GỐC KHKK: reverse-join qua DÒNG kế hoạch.
|
||||
// `Contract` không mang con-trỏ ngược sang GĐ2; con-trỏ nằm ở `Line.ContractId`
|
||||
// (`ContractSigningPlanLine.cs:46`), write-site DUY NHẤT = bridge K7
|
||||
// (`CreateContractFromSigningPlanFeatures.cs:238` — gán cho TỪNG dòng được chọn).
|
||||
// 0 dòng ⇒ `null` ⇒ FE không render card (HĐ tạo tay / HĐ cũ trước K7).
|
||||
// 🔴 Cả `ContractSigningPlanLines` lẫn `ContractSigningPlans` CÓ global query filter
|
||||
// `!IsDeleted` (`ContractSigningPlanLineConfiguration.cs:52` + `...PlanConfiguration.cs:55`)
|
||||
// ⇒ KHÔNG cần `.Where(!IsDeleted)` thủ công như họ HRM.
|
||||
ContractSourceDto? sourceDto = null;
|
||||
var planLines = await db.ContractSigningPlanLines.AsNoTracking()
|
||||
.Where(l => l.ContractId == c.Id)
|
||||
.OrderBy(l => l.CreatedAt).ThenBy(l => l.Id)
|
||||
.Select(l => new { l.ContractSigningPlanId, l.TenHangMuc, l.ApprovedAmount })
|
||||
.ToListAsync(ct);
|
||||
if (planLines.Count > 0)
|
||||
{
|
||||
// "N dòng nối cùng 1 phiếu" là bất biến do rào (2) của bridge
|
||||
// (`CreateContractFromSigningPlanFeatures.cs:101-109`). Vẫn chốt phiếu theo dòng
|
||||
// ĐẦU (thứ tự tất định) rồi lọc lại: nếu có ai INSERT SQL thô (họ #84) làm lệch,
|
||||
// card ra số của ĐÚNG 1 phiếu thay vì trộn tiền của 2 phiếu.
|
||||
var planId = planLines[0].ContractSigningPlanId;
|
||||
var linesOfPlan = planLines.Where(l => l.ContractSigningPlanId == planId).ToList();
|
||||
var plan = await db.ContractSigningPlans.AsNoTracking()
|
||||
.Where(p => p.Id == planId)
|
||||
.Select(p => new { p.MaKeHoach, p.ApprovalGroup, p.PurchaseEvaluationId })
|
||||
.FirstOrDefaultAsync(ct);
|
||||
sourceDto = new ContractSourceDto(
|
||||
PlanId: planId,
|
||||
MaKeHoach: plan?.MaKeHoach,
|
||||
ApprovalGroup: plan?.ApprovalGroup,
|
||||
PurchaseEvaluationId: plan?.PurchaseEvaluationId,
|
||||
TenHangMucs: linesOfPlan
|
||||
.Select(l => string.IsNullOrWhiteSpace(l.TenHangMuc) ? "(chưa gán hạng mục)" : l.TenHangMuc!)
|
||||
.ToList(),
|
||||
// Σ in-memory, KHÔNG `Sum()` dịch xuống SQL: provider SQLite của test-fixture
|
||||
// không cộng được `decimal` (lưu TEXT) — cộng ở đây cho cả 2 provider ra CÙNG số.
|
||||
ApprovedAmountTotal: linesOfPlan.Sum(l => l.ApprovedAmount ?? 0));
|
||||
}
|
||||
|
||||
// ---- [W2 S187] (b) CÂY THÔ workflow V2 (Bước → Cấp → người) — khuôn KHKK
|
||||
// `ContractSigningPlanFeatures.cs:1014-1057`. Chỉ nạp khi HĐ ĐÃ pin V2; HĐ V1 legacy
|
||||
// ⇒ null (FE giữ nhánh render cũ). KHÔNG precompute status — panel tự suy từ cặp
|
||||
// con-trỏ (`CurrentWorkflowStepIndex` INDEX 0-based + `CurrentApprovalLevelOrder`).
|
||||
List<ContractWorkflowStepDto>? workflowStepsDto = null;
|
||||
if (c.ApprovalWorkflowId is Guid awIdTree)
|
||||
{
|
||||
var wf = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Include(w => w.Steps.OrderBy(s => s.Order))
|
||||
.ThenInclude(s => s.Levels.OrderBy(l => l.Order))
|
||||
.FirstOrDefaultAsync(w => w.Id == awIdTree, ct);
|
||||
if (wf is not null)
|
||||
{
|
||||
var approverIds = wf.Steps.SelectMany(s => s.Levels)
|
||||
.Select(l => l.ApproverUserId).Distinct().ToList();
|
||||
var approverNames = await userManager.Users.AsNoTracking()
|
||||
.Where(u => approverIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
|
||||
|
||||
// `Step.DepartmentId` loose-Guid (không nav) ⇒ gom 1 lượt, KHÔNG N+1 trong vòng lặp.
|
||||
var stepDeptIds = wf.Steps.Where(s => s.DepartmentId.HasValue)
|
||||
.Select(s => s.DepartmentId!.Value).Distinct().ToList();
|
||||
var stepDeptNames = stepDeptIds.Count == 0
|
||||
? new Dictionary<Guid, string>()
|
||||
: await db.Departments.AsNoTracking()
|
||||
.Where(d => stepDeptIds.Contains(d.Id))
|
||||
.Select(d => new { d.Id, d.Name })
|
||||
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||
|
||||
workflowStepsDto = wf.Steps.OrderBy(s => s.Order)
|
||||
.Select(s => new ContractWorkflowStepDto(
|
||||
s.Id, s.Order, s.Name, s.DepartmentId,
|
||||
s.Levels.OrderBy(l => l.Order)
|
||||
.Select(l => new ContractWorkflowLevelDto(
|
||||
l.Id, l.Order, l.Name, l.ApproverUserId,
|
||||
approverNames.TryGetValue(l.ApproverUserId, out var afn2) ? afn2 : null,
|
||||
l.AllowApproverFinalize))
|
||||
.ToList(),
|
||||
s.DepartmentId is Guid sdId && stepDeptNames.TryGetValue(sdId, out var sdn) ? sdn : null))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve user names
|
||||
var userIds = new HashSet<Guid>();
|
||||
if (c.DrafterUserId is Guid did) userIds.Add(did);
|
||||
@ -751,7 +833,12 @@ public class GetContractQueryHandler(
|
||||
// [Plan B S29 2026-05-22 Chunk E2] V2 fields
|
||||
ApprovalWorkflowId: c.ApprovalWorkflowId,
|
||||
CurrentApprovalLevelOrder: c.CurrentApprovalLevelOrder,
|
||||
LevelOpinions: levelOpinionsDto);
|
||||
LevelOpinions: levelOpinionsDto,
|
||||
// [W2 S187] Nền hiển thị GĐ3 — nguồn gốc KHKK + đủ cặp con-trỏ + cây thô.
|
||||
// `CurrentWorkflowStepIndex` đọc THẲNG cột đã có (`Contract.cs:40`) ⇒ 0-migration.
|
||||
Source: sourceDto,
|
||||
CurrentWorkflowStepIndex: c.CurrentWorkflowStepIndex,
|
||||
WorkflowSteps: workflowStepsDto);
|
||||
}
|
||||
|
||||
// FE uses this to render next-phase buttons dynamically — no more hardcoded
|
||||
|
||||
@ -61,7 +61,81 @@ public record ContractDetailDto(
|
||||
// FE fetch ApprovalFlow shape via /api/approval-workflows-v2/{ApprovalWorkflowId}.
|
||||
Guid? ApprovalWorkflowId = null,
|
||||
int? CurrentApprovalLevelOrder = null,
|
||||
List<ContractLevelOpinionDto>? LevelOpinions = null);
|
||||
List<ContractLevelOpinionDto>? LevelOpinions = null,
|
||||
// ===================== [W2 S187 — YC-023 GĐ3] append-CUỐI =========================
|
||||
// 🔴 Positional record: 3 field dưới BẮT BUỘC có default `null` và đứng CUỐI —
|
||||
// call-site duy nhất (`ContractFeatures.cs:721`) truyền theo vị trí, thêm vào giữa
|
||||
// là vỡ mọi arg sau nó.
|
||||
//
|
||||
// • `Source` — NGUỒN GỐC KHKK (GĐ2) của HĐ này. Đường đọc là REVERSE-JOIN
|
||||
// `ContractSigningPlanLines WHERE ContractId = {id}`: `Contract` KHÔNG mang con-trỏ
|
||||
// ngược nào sang kế hoạch (`Contract.cs` 0 field `SigningPlan*`) — con-trỏ nằm ở
|
||||
// phía dòng kế hoạch (`ContractSigningPlanLine.cs:46`, write-site DUY NHẤT là bridge
|
||||
// K7 `CreateContractFromSigningPlanFeatures.cs:238`). HĐ tạo tay / HĐ cũ trước K7
|
||||
// ⇒ 0 dòng nối ⇒ `null` ⇒ FE KHÔNG render card (null-path là ca THƯỜNG, không phải lỗi).
|
||||
// • `CurrentWorkflowStepIndex` — con-trỏ Bước 0-based, đọc THẲNG cột đã có sẵn
|
||||
// (`Contract.cs:40`, Mig 21) ⇒ wave này 0-migration. Ghép với
|
||||
// `CurrentApprovalLevelOrder` (đã có `:63`) thành ĐỦ CẶP con-trỏ mà panel duyệt cần.
|
||||
// • `WorkflowSteps` — CÂY THÔ (Bước → Cấp → người) của quy trình V2 đã pin.
|
||||
// 🔴 KHUÔN KHKK, KHÔNG PHẢI KHUÔN PE: BE trả cây thô + cặp con-trỏ, panel FE TỰ SUY
|
||||
// Done/Current/Pending — cố ý KHÔNG precompute `status` như cặp DTO PE
|
||||
// (`PurchaseEvaluationCurrentApprovalDto`/`...ApprovalFlowDto`), vì panel được chọn
|
||||
// mirror là `KhkkWorkflowPanel` và nó tiêu thụ ĐÚNG shape này
|
||||
// (`ContractSigningPlanFeatures.cs:194` + `:205`).
|
||||
// `null` khi HĐ chưa pin `ApprovalWorkflowId` (V1 legacy) — FE giữ nhánh cũ.
|
||||
// ⚠️ Đường CŨ vẫn sống: FE Section 5 từng fetch cây qua
|
||||
// `/api/approval-workflows-v2/{ApprovalWorkflowId}` (xem chú thích `:61`). Field này
|
||||
// là đường INLINE 1-request để panel không phải gọi thêm — KHÔNG đẻ đường thứ 3.
|
||||
ContractSourceDto? Source = null,
|
||||
int? CurrentWorkflowStepIndex = null,
|
||||
List<ContractWorkflowStepDto>? WorkflowSteps = null);
|
||||
|
||||
// [W2 S187] Nguồn gốc GĐ2 (Kế hoạch ký kết — KHKK) của một HĐ. Dựng từ N dòng kế hoạch
|
||||
// cùng trỏ về HĐ này (bridge K7 gán `Line.ContractId` cho TỪNG dòng được chọn —
|
||||
// `CreateContractFromSigningPlanFeatures.cs:238`; rào (3) `:111-115` bảo đảm N dòng đó
|
||||
// cùng 1 NCC và rào (2) `:101-109` bảo đảm cùng 1 phiếu).
|
||||
//
|
||||
// 3 field nullable (`MaKeHoach`/`ApprovalGroup`/`PurchaseEvaluationId`) = dữ liệu của
|
||||
// PHIẾU CHA. Chúng null KHI VÀ CHỈ KHI dòng còn sống mà phiếu cha không đọc được nữa
|
||||
// (xoá mềm) — ca gần như bất khả (xoá phiếu chỉ cho ở Nháp/Trả lại/Từ chối, mà phiếu đẻ
|
||||
// ra HĐ thì đã DaDuyet). Cố ý KHÔNG nuốt cả `Source` thành null ở ca đó: nuốt đi thì HĐ
|
||||
// mồ-côi trông Y HỆT HĐ tạo tay — hai thứ khác hẳn nhau.
|
||||
public record ContractSourceDto(
|
||||
Guid PlanId,
|
||||
string? MaKeHoach,
|
||||
int? ApprovalGroup,
|
||||
Guid? PurchaseEvaluationId,
|
||||
// Tên hạng mục của TỪNG dòng nối (denorm tại thời điểm gán — `ContractSigningPlanLine.cs:27`).
|
||||
// Dòng chưa gán hạng mục ⇒ "(chưa gán hạng mục)" (đúng chữ bridge dùng ở `:121`) thay vì
|
||||
// bị lọc bỏ: giữ `Count` == số dòng để FE không hiển thị ít chip hơn số tiền đã cộng.
|
||||
List<string> TenHangMucs,
|
||||
// Σ `ApprovedAmount` — giá CHỐT tại choke-point finalize. Bridge cấm dòng chưa có giá
|
||||
// chốt (`:125-127`) ⇒ trên thực tế 0 dòng nào null; `?? 0` chỉ là rào.
|
||||
decimal ApprovedAmountTotal);
|
||||
|
||||
// [W2 S187] Cây thô workflow V2 — mirror 1-1 `ContractSigningPlanWorkflowStepDto`
|
||||
// (`ContractSigningPlanFeatures.cs:142-152`). KHÔNG có field `Status`: trạng thái là thứ
|
||||
// panel tự suy từ cặp con-trỏ, BE chỉ trả cấu hình.
|
||||
public record ContractWorkflowStepDto(
|
||||
Guid Id,
|
||||
int Order,
|
||||
string Name,
|
||||
Guid? DepartmentId,
|
||||
List<ContractWorkflowLevelDto> Levels,
|
||||
// Tên phòng của Bước — `Step.DepartmentId` là loose-Guid (không nav) ⇒ join 1 lượt ở
|
||||
// handler, KHÔNG N+1. Default null để append thêm field sau này không vỡ call-site.
|
||||
string? DepartmentName = null);
|
||||
|
||||
public record ContractWorkflowLevelDto(
|
||||
Guid Id,
|
||||
int Order,
|
||||
string? Name,
|
||||
Guid ApproverUserId,
|
||||
string? ApproverFullName,
|
||||
// Cờ per-Cấp "Cấp này được KẾT THÚC quy trình" — trả kèm để panel mirror KHKK có đủ
|
||||
// dữ liệu; HĐ có dùng ô-tích này hay không là quyết của wave màn duyệt (W6), không
|
||||
// phải của DTO.
|
||||
bool AllowApproverFinalize = false);
|
||||
|
||||
// [Plan B S29 2026-05-22 Chunk E2] Ý kiến cấp duyệt V2 dynamic theo
|
||||
// ApprovalWorkflowLevel. Mirror PE PurchaseEvaluationLevelOpinionDto pattern.
|
||||
|
||||
Reference in New Issue
Block a user