[CLAUDE] App+Api+FE: Kế thừa HĐ từ phiếu Duyệt NCC (Phase 4)
BE:
- CreateContractFromEvaluationCommand: guard DaDuyet + SelectedSupplier
+ ContractId=null → tạo Contract draft mới với SupplierId/ProjectId/
DepartmentId kế thừa từ PE. GiaTri = sum(details.thanhTienNganSach).
DraftData = PE.PaymentTerms. Gen MaHopDong ngay + pin WorkflowDefinitionId
theo ContractType user chọn. Log Changelog cả 2 bảng (Contract +
PurchaseEvaluation), link 2 chiều PE.ContractId = contract.Id.
- ListApprovedPurchaseEvaluationsQuery: DaDuyet + ContractId=null cho
FE picker.
- 2 endpoint mới:
GET /api/purchase-evaluations/approved-pending-contract
POST /api/purchase-evaluations/{id}/create-contract
FE:
- PeDetailTabs InfoTab: nếu Phase=DaDuyet && !ContractId && SelectedSupplierId
→ banner emerald + button "Tạo HĐ từ phiếu" → CreateContractDialog
(pick ContractType dropdown 7 loại + TenHopDong + bypass CCM flag)
- Sau khi tạo → navigate /contracts/{newId}
- Mirror fe-user.
KHÔNG auto-map PE Details → Contract Details per-type (PE schema ≠ 7
ContractType details schemas — user điền lại sau). PE → Contract link
qua FK ContractId cho navigation + history.
This commit is contained in:
@ -121,7 +121,10 @@ export function PeDetailTabs({
|
|||||||
|
|
||||||
// ===== Tab: Thông tin =====
|
// ===== Tab: Thông tin =====
|
||||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||||
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
||||||
<Field label="Dự án" value={ev.projectName} />
|
<Field label="Dự án" value={ev.projectName} />
|
||||||
@ -133,6 +136,82 @@ function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
{canCreateContract && (
|
||||||
|
<div className="rounded border border-emerald-200 bg-emerald-50 p-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="text-sm text-emerald-800">
|
||||||
|
✓ Phiếu đã duyệt. Bấm để tạo HĐ mới kế thừa NCC + hạng mục.
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setCreateOpen(true)} className="gap-1.5 text-xs">
|
||||||
|
<Plus className="h-3.5 w-3.5" /> Tạo HĐ từ phiếu
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{createOpen && <CreateContractDialog evaluation={ev} onClose={() => setCreateOpen(false)} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBundle; onClose: () => void }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
contractType: 1,
|
||||||
|
tenHopDong: evaluation.tenGoiThau,
|
||||||
|
bypassProcurementAndCCM: false,
|
||||||
|
})
|
||||||
|
const mut = useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
api.post<{ contractId: string }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form),
|
||||||
|
onSuccess: res => {
|
||||||
|
toast.success('Đã tạo HĐ từ phiếu.')
|
||||||
|
navigate(`/contracts/${res.data.contractId}`)
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
const typeOptions = [
|
||||||
|
[1, 'HĐ Thầu phụ'],
|
||||||
|
[2, 'HĐ Giao khoán'],
|
||||||
|
[3, 'HĐ Nhà cung cấp'],
|
||||||
|
[4, 'HĐ Dịch vụ'],
|
||||||
|
[5, 'HĐ Mua bán'],
|
||||||
|
[6, 'HĐ Nguyên tắc NCC'],
|
||||||
|
[7, 'HĐ Nguyên tắc DV'],
|
||||||
|
] as const
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onClose={onClose}
|
||||||
|
title="Tạo HĐ từ phiếu Duyệt NCC"
|
||||||
|
footer={<>
|
||||||
|
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
||||||
|
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Tạo</Button>
|
||||||
|
</>}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
NCC: <strong>{evaluation.selectedSupplierName}</strong> · Dự án: {evaluation.projectName}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<Label>Loại HĐ</Label>
|
||||||
|
<Select value={form.contractType} onChange={e => setForm({ ...form, contractType: Number(e.target.value) })}>
|
||||||
|
{typeOptions.map(([v, lbl]) => <option key={v} value={v}>{lbl}</option>)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Tên HĐ</Label>
|
||||||
|
<Input value={form.tenHopDong} onChange={e => setForm({ ...form, tenHopDong: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.bypassProcurementAndCCM}
|
||||||
|
onChange={e => setForm({ ...form, bypassProcurementAndCCM: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Bypass CCM (áp dụng HĐ với Chủ đầu tư)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -121,7 +121,10 @@ export function PeDetailTabs({
|
|||||||
|
|
||||||
// ===== Tab: Thông tin =====
|
// ===== Tab: Thông tin =====
|
||||||
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
||||||
|
const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||||
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
<Field label="Tên gói thầu" value={ev.tenGoiThau} />
|
||||||
<Field label="Dự án" value={ev.projectName} />
|
<Field label="Dự án" value={ev.projectName} />
|
||||||
@ -133,6 +136,82 @@ function InfoTab({ ev }: { ev: PeDetailBundle }) {
|
|||||||
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
<Field label="HĐ kế thừa" value={<a href={`/contracts/${ev.contractId}`} className="text-brand-600 hover:underline">✓ Xem HĐ</a>} />
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
{canCreateContract && (
|
||||||
|
<div className="rounded border border-emerald-200 bg-emerald-50 p-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="text-sm text-emerald-800">
|
||||||
|
✓ Phiếu đã duyệt. Bấm để tạo HĐ mới kế thừa NCC + hạng mục.
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setCreateOpen(true)} className="gap-1.5 text-xs">
|
||||||
|
<Plus className="h-3.5 w-3.5" /> Tạo HĐ từ phiếu
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{createOpen && <CreateContractDialog evaluation={ev} onClose={() => setCreateOpen(false)} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateContractDialog({ evaluation, onClose }: { evaluation: PeDetailBundle; onClose: () => void }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
contractType: 1,
|
||||||
|
tenHopDong: evaluation.tenGoiThau,
|
||||||
|
bypassProcurementAndCCM: false,
|
||||||
|
})
|
||||||
|
const mut = useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
api.post<{ contractId: string }>(`/purchase-evaluations/${evaluation.id}/create-contract`, form),
|
||||||
|
onSuccess: res => {
|
||||||
|
toast.success('Đã tạo HĐ từ phiếu.')
|
||||||
|
navigate(`/contracts/${res.data.contractId}`)
|
||||||
|
},
|
||||||
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
|
})
|
||||||
|
const typeOptions = [
|
||||||
|
[1, 'HĐ Thầu phụ'],
|
||||||
|
[2, 'HĐ Giao khoán'],
|
||||||
|
[3, 'HĐ Nhà cung cấp'],
|
||||||
|
[4, 'HĐ Dịch vụ'],
|
||||||
|
[5, 'HĐ Mua bán'],
|
||||||
|
[6, 'HĐ Nguyên tắc NCC'],
|
||||||
|
[7, 'HĐ Nguyên tắc DV'],
|
||||||
|
] as const
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onClose={onClose}
|
||||||
|
title="Tạo HĐ từ phiếu Duyệt NCC"
|
||||||
|
footer={<>
|
||||||
|
<Button variant="ghost" onClick={onClose}>Hủy</Button>
|
||||||
|
<Button onClick={() => mut.mutate()} disabled={mut.isPending}>Tạo</Button>
|
||||||
|
</>}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
NCC: <strong>{evaluation.selectedSupplierName}</strong> · Dự án: {evaluation.projectName}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<Label>Loại HĐ</Label>
|
||||||
|
<Select value={form.contractType} onChange={e => setForm({ ...form, contractType: Number(e.target.value) })}>
|
||||||
|
{typeOptions.map(([v, lbl]) => <option key={v} value={v}>{lbl}</option>)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Tên HĐ</Label>
|
||||||
|
<Input value={form.tenHopDong} onChange={e => setForm({ ...form, tenHopDong: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.bypassProcurementAndCCM}
|
||||||
|
onChange={e => setForm({ ...form, bypassProcurementAndCCM: e.target.checked })}
|
||||||
|
/>
|
||||||
|
Bypass CCM (áp dụng HĐ với Chủ đầu tư)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -149,7 +149,28 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
|||||||
[HttpGet("{id:guid}/changelogs")]
|
[HttpGet("{id:guid}/changelogs")]
|
||||||
public async Task<List<PurchaseEvaluationChangelogDto>> GetChangelogs(Guid id, CancellationToken ct)
|
public async Task<List<PurchaseEvaluationChangelogDto>> GetChangelogs(Guid id, CancellationToken ct)
|
||||||
=> await mediator.Send(new ListPurchaseEvaluationChangelogsQuery(id), ct);
|
=> await mediator.Send(new ListPurchaseEvaluationChangelogsQuery(id), ct);
|
||||||
|
|
||||||
|
// ========== Kế thừa HĐ ==========
|
||||||
|
|
||||||
|
// List phiếu đã DaDuyet chưa gen HĐ — dùng cho modal "Tạo HĐ từ phiếu"
|
||||||
|
[HttpGet("approved-pending-contract")]
|
||||||
|
public async Task<List<PurchaseEvaluationListItemDto>> ListApproved(CancellationToken ct)
|
||||||
|
=> await mediator.Send(new ListApprovedPurchaseEvaluationsQuery(), ct);
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/create-contract")]
|
||||||
|
public async Task<ActionResult<object>> CreateContractFromEvaluation(
|
||||||
|
Guid id, [FromBody] CreateContractFromEvaluationBody body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var contractId = await mediator.Send(new CreateContractFromEvaluationCommand(
|
||||||
|
id, body.ContractType, body.TenHopDong, body.BypassProcurementAndCCM), ct);
|
||||||
|
return Ok(new { contractId });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CreateContractFromEvaluationBody(
|
||||||
|
Domain.Contracts.ContractType ContractType,
|
||||||
|
string? TenHopDong,
|
||||||
|
bool BypassProcurementAndCCM = false);
|
||||||
|
|
||||||
public record TransitionPeBody(PurchaseEvaluationPhase TargetPhase, ApprovalDecision Decision, string? Comment);
|
public record TransitionPeBody(PurchaseEvaluationPhase TargetPhase, ApprovalDecision Decision, string? Comment);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Application.Common.Exceptions;
|
||||||
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Application.Contracts.Services;
|
||||||
|
using SolutionErp.Application.PurchaseEvaluations.Dtos;
|
||||||
|
using SolutionErp.Domain.Contracts;
|
||||||
|
using SolutionErp.Domain.PurchaseEvaluations;
|
||||||
|
|
||||||
|
namespace SolutionErp.Application.PurchaseEvaluations;
|
||||||
|
|
||||||
|
// Kế thừa từ phiếu Duyệt NCC đã DaDuyet → tạo HĐ Draft mới. Map cơ bản:
|
||||||
|
// SupplierId = PE.SelectedSupplierId, ProjectId, DepartmentId, TenHopDong,
|
||||||
|
// GiaTri (sum ThanhTienNganSach của details). ContractType do user chọn
|
||||||
|
// (phiếu có thể gen HĐ ThauPhu/NhaCungCap/MuaBan... tùy gói thầu).
|
||||||
|
//
|
||||||
|
// KHÔNG copy Details per-type automatically — user điền riêng sau khi HĐ
|
||||||
|
// gen, tránh mapping sai (PE detail schema ≠ 7 Contract detail schemas).
|
||||||
|
// User có thể reference PE qua PE.ContractId để xem lại báo giá.
|
||||||
|
public record CreateContractFromEvaluationCommand(
|
||||||
|
Guid PurchaseEvaluationId,
|
||||||
|
ContractType ContractType,
|
||||||
|
string? TenHopDong,
|
||||||
|
bool BypassProcurementAndCCM = false) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateContractFromEvaluationCommandValidator : AbstractValidator<CreateContractFromEvaluationCommand>
|
||||||
|
{
|
||||||
|
public CreateContractFromEvaluationCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PurchaseEvaluationId).NotEmpty();
|
||||||
|
RuleFor(x => x.ContractType).IsInEnum();
|
||||||
|
RuleFor(x => x.TenHopDong).MaximumLength(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateContractFromEvaluationCommandHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
ICurrentUser currentUser,
|
||||||
|
IContractWorkflowService workflow,
|
||||||
|
IContractCodeGenerator codeGenerator) : IRequestHandler<CreateContractFromEvaluationCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateContractFromEvaluationCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var pe = await db.PurchaseEvaluations
|
||||||
|
.Include(p => p.Details)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.PurchaseEvaluationId, ct)
|
||||||
|
?? throw new NotFoundException("PurchaseEvaluation", request.PurchaseEvaluationId);
|
||||||
|
|
||||||
|
if (pe.Phase != PurchaseEvaluationPhase.DaDuyet)
|
||||||
|
throw new ConflictException("Chỉ tạo HĐ từ phiếu đã duyệt xong (DaDuyet).");
|
||||||
|
if (pe.SelectedSupplierId is null)
|
||||||
|
throw new ConflictException("Phiếu chưa chọn NCC thắng — click 'Chọn NCC' trước.");
|
||||||
|
if (pe.ContractId is not null)
|
||||||
|
throw new ConflictException("Phiếu này đã tạo HĐ rồi.");
|
||||||
|
|
||||||
|
var supplier = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == pe.SelectedSupplierId, ct)
|
||||||
|
?? throw new NotFoundException("Supplier", pe.SelectedSupplierId.Value);
|
||||||
|
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == pe.ProjectId, ct)
|
||||||
|
?? throw new NotFoundException("Project", pe.ProjectId);
|
||||||
|
|
||||||
|
var activeWfId = await db.WorkflowDefinitions.AsNoTracking()
|
||||||
|
.Where(w => w.ContractType == request.ContractType && w.IsActive)
|
||||||
|
.Select(w => (Guid?)w.Id)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
var giaTri = pe.Details.Sum(d => d.ThanhTienNganSach);
|
||||||
|
|
||||||
|
var contract = new Contract
|
||||||
|
{
|
||||||
|
Type = request.ContractType,
|
||||||
|
Phase = ContractPhase.DangSoanThao,
|
||||||
|
SupplierId = pe.SelectedSupplierId.Value,
|
||||||
|
ProjectId = pe.ProjectId,
|
||||||
|
DepartmentId = pe.DepartmentId,
|
||||||
|
DrafterUserId = currentUser.UserId,
|
||||||
|
GiaTri = giaTri,
|
||||||
|
TenHopDong = request.TenHopDong ?? pe.TenGoiThau,
|
||||||
|
NoiDung = pe.MoTa,
|
||||||
|
BypassProcurementAndCCM = request.BypassProcurementAndCCM,
|
||||||
|
DraftData = pe.PaymentTerms, // carry forward payment terms
|
||||||
|
WorkflowDefinitionId = activeWfId,
|
||||||
|
SlaDeadline = DateTime.UtcNow.Add(
|
||||||
|
workflow.GetPhaseSla(ContractPhase.DangSoanThao) ?? TimeSpan.FromDays(7)),
|
||||||
|
};
|
||||||
|
contract.MaHopDong = await codeGenerator.GenerateAsync(contract, project.Code, supplier.Code, ct);
|
||||||
|
|
||||||
|
db.Contracts.Add(contract);
|
||||||
|
|
||||||
|
// Changelog HĐ: note kế thừa từ phiếu
|
||||||
|
db.ContractChangelogs.Add(new ContractChangelog
|
||||||
|
{
|
||||||
|
ContractId = contract.Id,
|
||||||
|
EntityType = ChangelogEntityType.Contract,
|
||||||
|
Action = ChangelogAction.Insert,
|
||||||
|
PhaseAtChange = contract.Phase,
|
||||||
|
UserId = currentUser.UserId,
|
||||||
|
Summary = $"Tạo HĐ {contract.MaHopDong} từ phiếu {pe.MaPhieu ?? pe.TenGoiThau}",
|
||||||
|
ContextNote = $"Kế thừa từ PurchaseEvaluation {pe.Id}",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link 2 chiều
|
||||||
|
pe.ContractId = contract.Id;
|
||||||
|
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||||
|
{
|
||||||
|
PurchaseEvaluationId = pe.Id,
|
||||||
|
EntityType = PurchaseEvaluationEntityType.Header,
|
||||||
|
Action = ChangelogAction.Update,
|
||||||
|
PhaseAtChange = pe.Phase,
|
||||||
|
UserId = currentUser.UserId,
|
||||||
|
Summary = $"Tạo HĐ {contract.MaHopDong} từ phiếu",
|
||||||
|
ContextNote = $"Contract {contract.Id}",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return contract.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List phiếu đã duyệt chưa gen HĐ (cho FE modal picker trong ContractCreatePage)
|
||||||
|
public record ListApprovedPurchaseEvaluationsQuery : IRequest<List<PurchaseEvaluationListItemDto>>;
|
||||||
|
|
||||||
|
public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext db)
|
||||||
|
: IRequestHandler<ListApprovedPurchaseEvaluationsQuery, List<PurchaseEvaluationListItemDto>>
|
||||||
|
{
|
||||||
|
public async Task<List<PurchaseEvaluationListItemDto>> Handle(
|
||||||
|
ListApprovedPurchaseEvaluationsQuery request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await (
|
||||||
|
from e in db.PurchaseEvaluations.AsNoTracking()
|
||||||
|
join p in db.Projects.AsNoTracking() on e.ProjectId equals p.Id
|
||||||
|
join s in db.Suppliers.AsNoTracking() on e.SelectedSupplierId equals s.Id into sj
|
||||||
|
from s in sj.DefaultIfEmpty()
|
||||||
|
where e.Phase == PurchaseEvaluationPhase.DaDuyet && e.ContractId == null
|
||||||
|
orderby e.CreatedAt descending
|
||||||
|
select new PurchaseEvaluationListItemDto(
|
||||||
|
e.Id, e.MaPhieu, e.TenGoiThau, e.Type, e.Phase,
|
||||||
|
e.ProjectId, p.Name,
|
||||||
|
e.SelectedSupplierId, s != null ? s.Name : null,
|
||||||
|
e.ContractId, e.SlaDeadline, e.CreatedAt)).ToListAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user