[CLAUDE] PurchaseEvaluation: User chọn quy trình duyệt V2 lúc tạo phiếu (Mig 23)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s

User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".

BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
  1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
  PE chạy logic cũ tới khi Session sau wire qua schema mới

BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
  param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
  (DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
  khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
  editable per Session 17 spec)

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
  - Replace field disabled "Loại quy trình" → Select bắt buộc
  - useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
    defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
  - Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
  - List cả version active + archived (UAT cần test compare)
  - Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
  - canSubmit require approvalWorkflowId set
  - POST payload include approvalWorkflowId

Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.

Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.
This commit is contained in:
pqhuy1987
2026-05-08 14:34:54 +07:00
parent d642fd361e
commit 0a40c65421
11 changed files with 4044 additions and 23 deletions

View File

@ -59,6 +59,8 @@ export function PeWorkspaceCreateView({
budgetManual: false, budgetManual: false,
budgetManualName: '', budgetManualName: '',
budgetManualAmount: 0, budgetManualAmount: 0,
// Mig 23 — Pin quy trình duyệt V2 (User tự chọn lúc tạo)
approvalWorkflowId: '',
}) })
// Payment terms: select preset OR "Khác" → text input // Payment terms: select preset OR "Khác" → text input
const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__ const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__
@ -69,6 +71,22 @@ export function PeWorkspaceCreateView({
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
}) })
// Mig 23 — fetch list quy trình duyệt V2 cho User chọn (filter theo
// ApplicableType khớp với defaultType: 1=DuyetNcc / 2=DuyetNccPhuongAn).
const approvalWorkflows = useQuery({
queryKey: ['approval-workflows-v2-active', defaultType],
queryFn: async () => {
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
'/approval-workflows-v2',
{ params: { applicableType: defaultType } },
)
// Trả về tất cả version (active + archived) cho User pick — UAT cần
// flexibility chọn cả version cũ test compare.
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
return typeBucket?.history ?? []
},
})
const eligibleBudgets = useQuery({ const eligibleBudgets = useQuery({
queryKey: ['eligible-budgets', form.projectId], queryKey: ['eligible-budgets', form.projectId],
queryFn: async () => { queryFn: async () => {
@ -93,6 +111,7 @@ export function PeWorkspaceCreateView({
diaDiem: form.diaDiem || null, diaDiem: form.diaDiem || null,
moTa: form.moTa || null, moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null, paymentTerms: form.paymentTerms || null,
approvalWorkflowId: form.approvalWorkflowId || null,
...budgetPayload, ...budgetPayload,
}) })
return res.data.id return res.data.id
@ -105,7 +124,7 @@ export function PeWorkspaceCreateView({
onError: e => toast.error(getErrorMessage(e)), onError: e => toast.error(getErrorMessage(e)),
}) })
const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.approvalWorkflowId && !create.isPending
return ( return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm"> <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
@ -126,15 +145,31 @@ export function PeWorkspaceCreateView({
{/* Section 1 — Thông tin gói thầu (editable) */} {/* Section 1 — Thông tin gói thầu (editable) */}
<Section title="1. Thông tin gói thầu"> <Section title="1. Thông tin gói thầu">
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
<div> <div className="md:col-span-2">
<Label className="text-[11px]">Loại quy trình (theo menu khóa)</Label> <Label className="text-[11px]">
<Input Quy trình duyệt * <span className="text-[10px] font-normal text-slate-400">(theo {PurchaseEvaluationTypeLabel[form.type]})</span>
value={PurchaseEvaluationTypeLabel[form.type]} </Label>
disabled <Select
className="bg-slate-100 font-medium" value={form.approvalWorkflowId}
/> onChange={e => setForm({ ...form, approvalWorkflowId: e.target.value })}
required
>
<option value=""> Chọn quy trình duyệt </option>
{approvalWorkflows.data?.map(w => (
<option key={w.id} value={w.id}>
{w.code} v{String(w.version).padStart(2, '0')} {w.name}
{w.isActive ? ' (đang áp dụng)' : ''}
</option>
))}
</Select>
{approvalWorkflows.data && approvalWorkflows.data.length === 0 && (
<p className="mt-1 text-[11px] text-amber-700">
Chưa quy trình duyệt cho loại {PurchaseEvaluationTypeLabel[form.type]}. Vào{' '}
<span className="font-mono">/system/approval-workflows-v2</span> đ tạo trước.
</p>
)}
</div> </div>
<div className="md:col-span-1"> <div className="md:col-span-2">
<Label className="text-[11px]">a. Tên gói thầu *</Label> <Label className="text-[11px]">a. Tên gói thầu *</Label>
<Input <Input
value={form.tenGoiThau} value={form.tenGoiThau}

View File

@ -310,6 +310,11 @@ export type PeDetailBundle = {
// Mig 17 — manual budget fallback khi không link Budget entity. Cả 2 cùng null OK. // Mig 17 — manual budget fallback khi không link Budget entity. Cả 2 cùng null OK.
budgetManualName: string | null budgetManualName: string | null
budgetManualAmount: number | null budgetManualAmount: number | null
// Mig 23 — Pin schema mới ApprovalWorkflowsV2 (User chọn lúc create).
approvalWorkflowId: string | null
approvalWorkflowCode: string | null
approvalWorkflowName: string | null
approvalWorkflowVersion: number | null
suppliers: PeSupplier[] suppliers: PeSupplier[]
details: PeDetailRow[] details: PeDetailRow[]
approvals: PeApproval[] approvals: PeApproval[]

View File

@ -59,6 +59,8 @@ export function PeWorkspaceCreateView({
budgetManual: false, budgetManual: false,
budgetManualName: '', budgetManualName: '',
budgetManualAmount: 0, budgetManualAmount: 0,
// Mig 23 — Pin quy trình duyệt V2 (User tự chọn lúc tạo)
approvalWorkflowId: '',
}) })
// Payment terms: select preset OR "Khác" → text input // Payment terms: select preset OR "Khác" → text input
const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__ const [paymentMode, setPaymentMode] = useState<string>('') // '' / preset / __custom__
@ -69,6 +71,19 @@ export function PeWorkspaceCreateView({
queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items, queryFn: async () => (await api.get<{ items: Project[] }>('/projects', { params: { pageSize: 1000 } })).data.items,
}) })
// Mig 23 — fetch list quy trình duyệt V2 (filter ApplicableType khớp defaultType).
const approvalWorkflows = useQuery({
queryKey: ['approval-workflows-v2-active', defaultType],
queryFn: async () => {
const res = await api.get<{ types: { applicableType: number; history: { id: string; code: string; version: number; name: string; isActive: boolean }[] }[] }>(
'/approval-workflows-v2',
{ params: { applicableType: defaultType } },
)
const typeBucket = res.data.types.find(t => t.applicableType === defaultType)
return typeBucket?.history ?? []
},
})
const eligibleBudgets = useQuery({ const eligibleBudgets = useQuery({
queryKey: ['eligible-budgets', form.projectId], queryKey: ['eligible-budgets', form.projectId],
queryFn: async () => { queryFn: async () => {
@ -93,6 +108,7 @@ export function PeWorkspaceCreateView({
diaDiem: form.diaDiem || null, diaDiem: form.diaDiem || null,
moTa: form.moTa || null, moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null, paymentTerms: form.paymentTerms || null,
approvalWorkflowId: form.approvalWorkflowId || null,
...budgetPayload, ...budgetPayload,
}) })
return res.data.id return res.data.id
@ -105,7 +121,7 @@ export function PeWorkspaceCreateView({
onError: e => toast.error(getErrorMessage(e)), onError: e => toast.error(getErrorMessage(e)),
}) })
const canSubmit = !!form.tenGoiThau && !!form.projectId && !create.isPending const canSubmit = !!form.tenGoiThau && !!form.projectId && !!form.approvalWorkflowId && !create.isPending
return ( return (
<div className="rounded-lg border border-slate-200 bg-white shadow-sm"> <div className="rounded-lg border border-slate-200 bg-white shadow-sm">
@ -126,15 +142,30 @@ export function PeWorkspaceCreateView({
{/* Section 1 — Thông tin gói thầu (editable) */} {/* Section 1 — Thông tin gói thầu (editable) */}
<Section title="1. Thông tin gói thầu"> <Section title="1. Thông tin gói thầu">
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
<div> <div className="md:col-span-2">
<Label className="text-[11px]">Loại quy trình (theo menu khóa)</Label> <Label className="text-[11px]">
<Input Quy trình duyệt * <span className="text-[10px] font-normal text-slate-400">(theo {PurchaseEvaluationTypeLabel[form.type]})</span>
value={PurchaseEvaluationTypeLabel[form.type]} </Label>
disabled <Select
className="bg-slate-100 font-medium" value={form.approvalWorkflowId}
/> onChange={e => setForm({ ...form, approvalWorkflowId: e.target.value })}
required
>
<option value=""> Chọn quy trình duyệt </option>
{approvalWorkflows.data?.map(w => (
<option key={w.id} value={w.id}>
{w.code} v{String(w.version).padStart(2, '0')} {w.name}
{w.isActive ? ' (đang áp dụng)' : ''}
</option>
))}
</Select>
{approvalWorkflows.data && approvalWorkflows.data.length === 0 && (
<p className="mt-1 text-[11px] text-amber-700">
Chưa quy trình duyệt cho loại {PurchaseEvaluationTypeLabel[form.type]}. Liên hệ admin tạo trước.
</p>
)}
</div> </div>
<div className="md:col-span-1"> <div className="md:col-span-2">
<Label className="text-[11px]">a. Tên gói thầu *</Label> <Label className="text-[11px]">a. Tên gói thầu *</Label>
<Input <Input
value={form.tenGoiThau} value={form.tenGoiThau}

View File

@ -309,6 +309,11 @@ export type PeDetailBundle = {
// Mig 17 — manual budget fallback khi không link Budget entity. Cả 2 cùng null OK. // Mig 17 — manual budget fallback khi không link Budget entity. Cả 2 cùng null OK.
budgetManualName: string | null budgetManualName: string | null
budgetManualAmount: number | null budgetManualAmount: number | null
// Mig 23 — Pin schema mới ApprovalWorkflowsV2 (User chọn lúc create).
approvalWorkflowId: string | null
approvalWorkflowCode: string | null
approvalWorkflowName: string | null
approvalWorkflowVersion: number | null
suppliers: PeSupplier[] suppliers: PeSupplier[]
details: PeDetailRow[] details: PeDetailRow[]
approvals: PeApproval[] approvals: PeApproval[]

View File

@ -129,6 +129,12 @@ public record PurchaseEvaluationDetailBundleDto(
BudgetSummaryDto? Budget, BudgetSummaryDto? Budget,
string? BudgetManualName, string? BudgetManualName,
decimal? BudgetManualAmount, decimal? BudgetManualAmount,
// Mig 23 — schema mới ApprovalWorkflowsV2 pin lúc create. Hiển thị Code +
// Name + Version để FE show "QT-DN-V2-001 - Quy trình Duyệt NCC (v01)".
Guid? ApprovalWorkflowId,
string? ApprovalWorkflowCode,
string? ApprovalWorkflowName,
int? ApprovalWorkflowVersion,
List<PurchaseEvaluationSupplierDto> Suppliers, List<PurchaseEvaluationSupplierDto> Suppliers,
List<PurchaseEvaluationDetailDto> Details, List<PurchaseEvaluationDetailDto> Details,
List<PurchaseEvaluationApprovalDto> Approvals, List<PurchaseEvaluationApprovalDto> Approvals,

View File

@ -25,7 +25,8 @@ public record CreatePurchaseEvaluationCommand(
string? PaymentTerms, string? PaymentTerms,
Guid? BudgetId, Guid? BudgetId,
string? BudgetManualName, string? BudgetManualName,
decimal? BudgetManualAmount) : IRequest<Guid>; decimal? BudgetManualAmount,
Guid? ApprovalWorkflowId = null) : IRequest<Guid>; // [Mig 23] User chọn quy trình duyệt V2 lúc tạo
public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<CreatePurchaseEvaluationCommand> public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<CreatePurchaseEvaluationCommand>
{ {
@ -57,6 +58,23 @@ public class CreatePurchaseEvaluationCommandHandler(
.Select(w => (Guid?)w.Id) .Select(w => (Guid?)w.Id)
.FirstOrDefaultAsync(ct); .FirstOrDefaultAsync(ct);
// Validate ApprovalWorkflowId V2 (Mig 23) — User chọn lúc create.
// Phải tồn tại + ApplicableType khớp với PE.Type.
if (request.ApprovalWorkflowId is Guid awId)
{
var aw = await db.ApprovalWorkflows.AsNoTracking()
.FirstOrDefaultAsync(w => w.Id == awId, ct)
?? throw new NotFoundException("ApprovalWorkflow", awId);
var expectedType = request.Type switch
{
PurchaseEvaluationType.DuyetNcc => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNcc,
PurchaseEvaluationType.DuyetNccPhuongAn => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
_ => throw new ConflictException($"PurchaseEvaluationType {request.Type} chưa map sang ApprovalWorkflowApplicableType."),
};
if (aw.ApplicableType != expectedType)
throw new ConflictException($"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với loại phiếu {request.Type}.");
}
// Validate Budget link (nếu có): cùng Project + Phase=DaDuyet (chỉ cho // Validate Budget link (nếu có): cùng Project + Phase=DaDuyet (chỉ cho
// pick ngân sách đã duyệt mới được dùng làm reference đối chiếu). // pick ngân sách đã duyệt mới được dùng làm reference đối chiếu).
if (request.BudgetId is Guid bid) if (request.BudgetId is Guid bid)
@ -81,6 +99,7 @@ public class CreatePurchaseEvaluationCommandHandler(
MoTa = request.MoTa, MoTa = request.MoTa,
DrafterUserId = currentUser.UserId, DrafterUserId = currentUser.UserId,
WorkflowDefinitionId = activeWfId, WorkflowDefinitionId = activeWfId,
ApprovalWorkflowId = request.ApprovalWorkflowId, // Mig 23 — schema mới V2
PaymentTerms = request.PaymentTerms, PaymentTerms = request.PaymentTerms,
BudgetId = request.BudgetId, BudgetId = request.BudgetId,
BudgetManualName = request.BudgetManualName, BudgetManualName = request.BudgetManualName,
@ -120,7 +139,8 @@ public record UpdatePurchaseEvaluationDraftCommand(
string? PaymentTerms, string? PaymentTerms,
Guid? BudgetId, Guid? BudgetId,
string? BudgetManualName, string? BudgetManualName,
decimal? BudgetManualAmount) : IRequest; decimal? BudgetManualAmount,
Guid? ApprovalWorkflowId = null) : IRequest; // [Mig 23] cho User đổi quy trình khi sửa Nháp
public class UpdatePurchaseEvaluationDraftCommandHandler( public class UpdatePurchaseEvaluationDraftCommandHandler(
IApplicationDbContext db, IApplicationDbContext db,
@ -131,8 +151,25 @@ public class UpdatePurchaseEvaluationDraftCommandHandler(
var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct) var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
?? throw new NotFoundException("PurchaseEvaluation", request.Id); ?? throw new NotFoundException("PurchaseEvaluation", request.Id);
if (entity.Phase != PurchaseEvaluationPhase.DangSoanThao) if (entity.Phase != PurchaseEvaluationPhase.DangSoanThao
throw new ConflictException("Chỉ sửa được phiếu khi ở phase Đang soạn thảo."); && entity.Phase != PurchaseEvaluationPhase.TraLai)
throw new ConflictException("Chỉ sửa được phiếu khi ở phase Nháp hoặc Trả lại.");
// Validate ApprovalWorkflowId V2 nếu thay đổi (Mig 23).
if (request.ApprovalWorkflowId is Guid awId && awId != entity.ApprovalWorkflowId)
{
var aw = await db.ApprovalWorkflows.AsNoTracking()
.FirstOrDefaultAsync(w => w.Id == awId, ct)
?? throw new NotFoundException("ApprovalWorkflow", awId);
var expectedType = entity.Type switch
{
PurchaseEvaluationType.DuyetNcc => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNcc,
PurchaseEvaluationType.DuyetNccPhuongAn => Domain.ApprovalWorkflowsV2.ApprovalWorkflowApplicableType.DuyetNccPhuongAn,
_ => throw new ConflictException($"PurchaseEvaluationType {entity.Type} chưa map sang ApprovalWorkflowApplicableType."),
};
if (aw.ApplicableType != expectedType)
throw new ConflictException($"Quy trình {aw.Code} áp dụng cho {aw.ApplicableType}, không khớp với loại phiếu {entity.Type}.");
}
// Validate Budget link nếu thay đổi. // Validate Budget link nếu thay đổi.
if (request.BudgetId is Guid bid && bid != entity.BudgetId) if (request.BudgetId is Guid bid && bid != entity.BudgetId)
@ -153,6 +190,7 @@ public class UpdatePurchaseEvaluationDraftCommandHandler(
entity.BudgetId = request.BudgetId; entity.BudgetId = request.BudgetId;
entity.BudgetManualName = request.BudgetManualName; entity.BudgetManualName = request.BudgetManualName;
entity.BudgetManualAmount = request.BudgetManualAmount; entity.BudgetManualAmount = request.BudgetManualAmount;
entity.ApprovalWorkflowId = request.ApprovalWorkflowId; // Mig 23 — User đổi quy trình
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
{ {
@ -409,6 +447,24 @@ public class GetPurchaseEvaluationQueryHandler(
policy = PurchaseEvaluationPolicyRegistry.ForEvaluation(e); policy = PurchaseEvaluationPolicyRegistry.ForEvaluation(e);
} }
// Mig 23 — load ApprovalWorkflow V2 info nếu pin (Code/Name/Version
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
string? awCode = null, awName = null;
int? awVersion = null;
if (e.ApprovalWorkflowId is Guid awId)
{
var aw = await db.ApprovalWorkflows.AsNoTracking()
.Where(w => w.Id == awId)
.Select(w => new { w.Code, w.Name, w.Version })
.FirstOrDefaultAsync(ct);
if (aw is not null)
{
awCode = aw.Code;
awName = aw.Name;
awVersion = aw.Version;
}
}
return new PurchaseEvaluationDetailBundleDto( return new PurchaseEvaluationDetailBundleDto(
e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa, e.Id, e.MaPhieu, e.Type, e.Phase, e.TenGoiThau, e.DiaDiem, e.MoTa,
e.ProjectId, project?.Name ?? "", e.ProjectId, project?.Name ?? "",
@ -419,6 +475,7 @@ public class GetPurchaseEvaluationQueryHandler(
e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt, e.PaymentTerms, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
e.BudgetId, budgetSummary, e.BudgetId, budgetSummary,
e.BudgetManualName, e.BudgetManualAmount, e.BudgetManualName, e.BudgetManualAmount,
e.ApprovalWorkflowId, awCode, awName, awVersion,
e.Suppliers e.Suppliers
.OrderBy(s => s.Order) .OrderBy(s => s.Order)
.Select(s => new PurchaseEvaluationSupplierDto( .Select(s => new PurchaseEvaluationSupplierDto(

View File

@ -18,7 +18,8 @@ public class PurchaseEvaluation : AuditableEntity
public string? DiaDiem { get; set; } // Lô K, KCN Lộc An... public string? DiaDiem { get; set; } // Lô K, KCN Lộc An...
public string? MoTa { get; set; } public string? MoTa { get; set; }
public Guid? WorkflowDefinitionId { get; set; } // Pinned at create — config y như HĐ public Guid? WorkflowDefinitionId { get; set; } // [LEGACY Mig 21] Pinned at create — config y như HĐ
public Guid? ApprovalWorkflowId { get; set; } // [Mig 23 Session 17] Pin schema mới ApprovalWorkflowsV2
public DateTime? SlaDeadline { get; set; } public DateTime? SlaDeadline { get; set; }
public bool SlaWarningSent { get; set; } public bool SlaWarningSent { get; set; }

View File

@ -27,9 +27,18 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
b.HasIndex(x => x.ProjectId); b.HasIndex(x => x.ProjectId);
b.HasIndex(x => x.SlaDeadline); b.HasIndex(x => x.SlaDeadline);
b.HasIndex(x => x.WorkflowDefinitionId); b.HasIndex(x => x.WorkflowDefinitionId);
b.HasIndex(x => x.ApprovalWorkflowId);
b.HasIndex(x => x.ContractId); b.HasIndex(x => x.ContractId);
b.HasIndex(x => x.BudgetId); b.HasIndex(x => x.BudgetId);
// FK ApprovalWorkflowId Restrict (Mig 23 Session 17) — schema mới
// ApprovalWorkflowsV2 pin lúc create. Restrict để KHÔNG xóa workflow
// nếu còn phiếu pin.
b.HasOne<SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow>()
.WithMany()
.HasForeignKey(x => x.ApprovalWorkflowId)
.OnDelete(DeleteBehavior.Restrict);
b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Approvals).WithOne(a => a.PurchaseEvaluation).HasForeignKey(a => a.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade); b.HasMany(x => x.Approvals).WithOne(a => a.PurchaseEvaluation).HasForeignKey(a => a.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);

View File

@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddApprovalWorkflowIdToPurchaseEvaluation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ApprovalWorkflowId",
table: "PurchaseEvaluations",
type: "uniqueidentifier",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_ApprovalWorkflowId",
table: "PurchaseEvaluations",
column: "ApprovalWorkflowId");
migrationBuilder.AddForeignKey(
name: "FK_PurchaseEvaluations_ApprovalWorkflows_ApprovalWorkflowId",
table: "PurchaseEvaluations",
column: "ApprovalWorkflowId",
principalTable: "ApprovalWorkflows",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PurchaseEvaluations_ApprovalWorkflows_ApprovalWorkflowId",
table: "PurchaseEvaluations");
migrationBuilder.DropIndex(
name: "IX_PurchaseEvaluations_ApprovalWorkflowId",
table: "PurchaseEvaluations");
migrationBuilder.DropColumn(
name: "ApprovalWorkflowId",
table: "PurchaseEvaluations");
}
}
}

View File

@ -2485,6 +2485,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
b.Property<Guid?>("ApprovalWorkflowId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("BudgetId") b.Property<Guid?>("BudgetId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -2578,6 +2581,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApprovalWorkflowId");
b.HasIndex("BudgetId"); b.HasIndex("BudgetId");
b.HasIndex("ContractId"); b.HasIndex("ContractId");
@ -3562,6 +3567,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
{
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
.WithMany()
.HasForeignKey("ApprovalWorkflowId")
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b => modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b =>
{ {
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation") b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")