[CLAUDE] App+FE-User+FE-Admin: Plan AG4 — bổ sung Drafter + Department vào PE List card
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m27s

Anh UAT 2026-05-21: PE card danh sách thiếu người tạo + phòng ban tạo. Bổ sung 4 field
qua BE JOIN Users + Departments LEFT (cả 2 nullable theo PE entity).

BE — 4 file:
- PurchaseEvaluationDtos.cs: +4 fields DrafterUserId/DrafterName/DepartmentId/DepartmentName
- PurchaseEvaluationFeatures.cs ListHandler: JOIN Users + Departments LEFT, projection +4
- PurchaseEvaluationFeatures.cs InboxHandler: mirror JOIN + projection +4
- CreateContractFromEvaluationFeatures.cs ListApproved: mirror JOIN + projection +4

FE — 4 file × 2 app mirror:
- types/purchaseEvaluation.ts: PeListItem +4 fields
- pages/pe/PurchaseEvaluationsListPage.tsx: PE card render thêm dòng "👤 {drafterName} · {departmentName}"
  giữa Mã phiếu và Supplier. Conditional: chỉ render khi có ít nhất 1 field.

Verify:
- dotnet build clean 0 err
- dotnet test SolutionErp.slnx 111/111 PASS (58 Domain + 53 Infra) — no regression
- npm build fe-user PASS 0 TS err 1290.31 KB (gzip 336.79 KB) 1907 modules
- npm build fe-admin PASS 0 TS err 1401.66 KB (gzip 357.30 KB) 1926 modules
- 2 FE PE List file SHA256 IDENTICAL C6996194... (mirror §3.9)
- KHÔNG Mig (chỉ DTO + projection extend)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-21 18:52:04 +07:00
parent fbad4a9251
commit 2bf01184ca
8 changed files with 60 additions and 6 deletions

View File

@ -129,17 +129,24 @@ public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext d
public async Task<List<PurchaseEvaluationListItemDto>> Handle(
ListApprovedPurchaseEvaluationsQuery request, CancellationToken ct)
{
// Plan AG4: JOIN Users + Departments LEFT (mirror ListPurchaseEvaluations).
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()
join u in db.Users.AsNoTracking() on e.DrafterUserId equals u.Id into uj
from u in uj.DefaultIfEmpty()
join d in db.Departments.AsNoTracking() on e.DepartmentId equals d.Id into dj
from d in dj.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, e.UpdatedAt)).ToListAsync(ct);
e.ContractId, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
e.DrafterUserId, u != null ? u.FullName : null,
e.DepartmentId, d != null ? d.Name : null)).ToListAsync(ct);
}
}

View File

@ -20,7 +20,14 @@ public record PurchaseEvaluationListItemDto(
// S23 t2 UAT: bro UI polish PE list — display "Ngày giờ tạo" thay SLA countdown,
// sort theo "phiếu vừa update" (Tạo / Gửi duyệt / Trả lại). UpdatedAt được
// AuditingInterceptor auto-set mỗi SaveChanges → covers cả 3 event tự nhiên.
DateTime? UpdatedAt);
DateTime? UpdatedAt,
// Plan AG4 (2026-05-21): bro UAT yêu cầu bổ sung Người tạo + Phòng ban tạo
// vào PE card list. JOIN Users + Departments (LEFT join — cả 2 nullable theo
// PE entity). FullName resolve từ Users.FullName, Department.Name từ Departments.
Guid? DrafterUserId,
string? DrafterName,
Guid? DepartmentId,
string? DepartmentName);
public record PurchaseEvaluationSupplierDto(
Guid Id,

View File

@ -468,11 +468,16 @@ public class ListPurchaseEvaluationsQueryHandler(
public async Task<PagedResult<PurchaseEvaluationListItemDto>> Handle(
ListPurchaseEvaluationsQuery request, CancellationToken ct)
{
// Plan AG4: JOIN Users + Departments LEFT (cả 2 nullable theo PE entity).
var q = 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()
select new { e, p, s };
join u in db.Users.AsNoTracking() on e.DrafterUserId equals u.Id into uj
from u in uj.DefaultIfEmpty()
join d in db.Departments.AsNoTracking() on e.DepartmentId equals d.Id into dj
from d in dj.DefaultIfEmpty()
select new { e, p, s, u, d };
// IDOR strict (Plan E S22 — Session 21 +1): non-admin chỉ thấy phiếu khi:
// 1. là Drafter (mình tạo)
@ -527,7 +532,9 @@ public class ListPurchaseEvaluationsQueryHandler(
x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase,
x.e.ProjectId, x.p.Name,
x.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt))
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt,
x.e.DrafterUserId, x.u != null ? x.u.FullName : null,
x.e.DepartmentId, x.d != null ? x.d.Name : null))
.ToListAsync(ct);
return new PagedResult<PurchaseEvaluationListItemDto>(items, total, request.Page, request.PageSize);
@ -587,12 +594,17 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
? new HashSet<Guid>()
: await ResolveV2InboxIdsAsync(userId, ct);
// Plan AG4: JOIN Users + Departments LEFT (mirror ListPurchaseEvaluations).
var q = 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()
join u in db.Users.AsNoTracking() on e.DrafterUserId equals u.Id into uj
from u in uj.DefaultIfEmpty()
join d in db.Departments.AsNoTracking() on e.DepartmentId equals d.Id into dj
from d in dj.DefaultIfEmpty()
where eligiblePhases.Contains(e.Phase) || v2InboxIds.Contains(e.Id)
select new { e, p, s };
select new { e, p, s, u, d };
if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);
if (request.ApprovalWorkflowId is not null)
@ -605,7 +617,9 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase,
x.e.ProjectId, x.p.Name,
x.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt))
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt,
x.e.DrafterUserId, x.u != null ? x.u.FullName : null,
x.e.DepartmentId, x.d != null ? x.d.Name : null))
.Take(100)
.ToListAsync(ct);
}