[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
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:
@ -287,6 +287,8 @@ KHÔNG `*` / `latest`. Critical pins:
|
||||
|
||||
## 📅 Recent activity (last 10 FIFO)
|
||||
|
||||
- **2026-05-21 (S26 t1, Plan AG Chunk A+B+C PASS — Phase 1 PE List tree view 2-level):** UAT feedback bro Tra Sol "đám rừng" flat list → Outlook folder tree. **3 chunk cumulative 1 commit** `0bf6c7e` 2 file +346/-116 LOC = +115 LOC each. Mirror 2 app §3.9 IDENTICAL post-edit (SHA256 verify match `21001E90...`). Chunk A useMemo group nested: `ProjectGroup{projectId, projectName, goiThauList[], totalCount}` + `GoiThauGroup{displayName, normalizedKey, items[]}`. Normalize trim + toLowerCase group key, display raw đầu tiên trong group. Fallback "(Dự án đã xoá)" empty projectName + "(Chưa phân loại)" empty TenGoiThau. Sort vi locale 2 cấp A-Z. Filter pendingMe → DaGuiDuyet áp dụng TRƯỚC group (empty state đúng). Chunk B UI `<details>/<summary>` HTML native 2-level — fe-user no shadcn Accordion → native browser disclosure widget free. Tailwind v3 named groups `group/proj` + `group/gt` cho chevron rotation `group-open/proj:rotate-90`. `[&::-webkit-details-marker]:hidden` ẩn default disclosure triangle browser. 📁 + 📄 emoji icon inline + count badge `rounded-full bg-slate-200/100`. PE card content preserve nguyên (text + badge + date format + contractId hint — line 209-248 cũ). Chunk C localStorage persist Set<string> key `pe_list_expanded_groups`. Project key: `projectId or '__no_project__'`. Gói thầu key: `${projectId}::${normalizedGoiThau}`. Default empty Set (all collapse) — Outlook-style closed default. `try/catch` defensive cho localStorage (storage quota / private browsing). Header badge `pendingMe ? totalRowCount : list.data?.total` (replace `rows.length`). Empty state check `projectGroups.length === 0` (replace `rows.length === 0`). Import `useMemo, useState` từ 'react' (file pre-existing chỉ import từ tanstack). Build: fe-user PASS 0 TS err 1291.33 KB gzip 337.00 KB 1907 modules 16.05s; fe-admin PASS 0 TS err 1402.68 KB gzip 357.51 KB 1926 modules 6.86s. Pre-existing CSS @import warn + INEFFECTIVE_DYNAMIC_IMPORT realtime.ts unchanged. KHÔNG ops git push (em main verify Reviewer rồi push). Token ~16k (close to ~14k baseline Case 2 mirror 2 app). **Pattern 19 NEW**: HTML native `<details>/<summary>` + Tailwind named groups (`group/<name>`) + localStorage Set<string> persist cho hierarchical UI when no Accordion lib available. Free open/close state native browser (Space/Enter keyboard accessible) + 0 JS state per node + serialize/deserialize Set ↔ JSON array string. Tailwind v3 named groups syntax `group/proj` parent + `group-open/proj:rotate-90` child differs from default unnamed `group` + `group-open:rotate-90` — critical when nested groups cùng level cần distinct event scope. Reusable cho future tree views: Project explorer · Dept hierarchy · Permission tree · Workflow definition step list (vs HTML5 native vs shadcn vs JS library). Anti-pattern: nested same-name `group` would inherit parent state → both rotate sync. **Pattern 5 mirror 2 app §3.9 applied 8th cumulative S20-S26** (proven reliable IDENTICAL hash check sau edit batch — recommend tooling `git diff fe-admin/X fe-user/X` after every multi-file edit batch).
|
||||
|
||||
- **2026-05-19 (S25 wrap — Plan AB Chunk A Case 1 + 6 follow-up plans em main solo):** Plan AB Chunk A spawn 1× ~12K Case 1 cookie-cutter mirror. BE refactor ApplyReturnModeAsync Drafter early return → common path (line 280-287 → if/else block) + single Changelog.Add() ở cuối hàm với modeName switch enum + actorName resolve via userManager.FindByIdAsync mirror LogTransitionAsync pattern. FE × 2 app HistoryTab filter relax (PE_ENTITY_HEADER=1 + summary contains 'ngân sách' for Bug 1 + Workflow summary contains 'Trả lại' for Bug 2). KHÔNG TS test (UAT mode skip). KHÔNG migration. KHÔNG endpoint. Commit cdfd542 3 file +146/-95 LOC PASS. **Em main solo từ Plan AC** (cross-stack reasoning + UAT iteration borderline scope — Implementer would REFUSE per criteria #4 tight coupling BE+FE same plan). AC capture pre-call Step/Level + add Approval row Reject branch + skipToFinal comment + FE Decision badge × 2 app. AC2 FE merge synthetic Reject + dedupe timestamp 5s bucket. AD drop phase badges + extractNextTargetHint regex parse. AE BE batch 9 Changelog.Add sites UserName preventive fix. AF FE userMap fallback từ embedded domain data PeDetailBundle. **Pattern 16 NEW** (cumulative S25): Preventive systemic batch fix khi audit phát hiện 9 sites cùng bug pattern — replace_all=true với context-aware key (UserId line + Summary line) — 1 pass cover N sites idempotent. **Pattern 17 NEW**: FE merge synthetic rows từ Changelog cho audit historical recovery — pattern reusable cho Contract V2 + Budget V2 audit visualization without DB write. **Pattern 18 NEW**: FE userMap fallback từ embedded domain data (drafter + approvals + approvalFlow + levelOpinions + departmentOpinions) — no extra API fetch cho historical name resolve.
|
||||
|
||||
- **2026-05-19 (S25, Plan AB Chunk A PASS):** Bug 1+2 fix Changelog visibility audit log UAT. Commit `cdfd542` 3 file +146/-95 LOC. **BE** `PurchaseEvaluationWorkflowService.cs` `ApplyReturnModeAsync` lines 215-378 refactor: Drafter early return (line 282-287) → if/else common path, `summary = "Trả về Người soạn thảo"` thay vì return early, SLA reset move bên trong else block 3 mode còn lại (Drafter có riêng `evaluation.SlaDeadline = null`). Single Changelog.Add() ở cuối hàm cover 4 mode uniform: `EntityType=Workflow + Action=Update + Summary=$"Trả lại ({modeName}): {summary}"` với modeName switch ("Người soạn thảo"/"1 Cấp"/"1 Bước"/"Người chỉ định"). `actorName` resolve qua `userManager.FindByIdAsync` mirror pattern existing line 660-667 (LogTransition helper). KHÔNG SaveChangesAsync mới — caller `TransitionAsync` line 100 đã có downstream save. **FE** 2 file `PeDetailTabs.tsx` × 2 app mirror exact: filter extend `if (l.summary?.includes('Trả lại')) return true` (Workflow entity) + `if (l.entityType === PE_ENTITY_HEADER && l.summary?.toLowerCase().includes('ngân sách')) return true` (Header entity new const = 1). Empty placeholder + comment 3-source rewrite (UAT 2026-05-08 + 2026-05-19 + bullet list 5 filter rule). Verify: BE build clean 0 err 2 pre-existing DocxRenderer warn (20.27s), fe-user 1907 modules 16.62s 0 TS err, fe-admin 1926 modules 6.98s 0 TS err. Test SKIP per UAT mode `feedback_uat_skip_verify` Phase 9 (111 baseline preserve). **NEW pattern observed (cumulative)**: `Changelog log common path refactor + FE filter substring summary discrimination`. Reusable cho future audit log derived state (vd Adjust*/Return*/Reset* action): refactor early return → if/else common path để single log call cover N branch, FE filter qua substring summary keyword chứ KHÔNG enum field strict (action verb tiếng Việt "Trả lại"/"ngân sách" dễ maintain hơn enum + cho FE flexibility filter mới mà không cần BE schema migrate). Cross-ref Pattern 4 `feedback_service_hook_vs_endpoint` (state X derived của action Y → log trong handler Y, KHÔNG endpoint /X riêng — Bug 2 ApplyReturnModeAsync log trong service hook KHÔNG endpoint /return-changelog rời). Pattern 5 mirror 2 app §3.9 applied 7th cumulative. Token ~12k. Diff: BE +83/-49 (refactor + new log block ~40 LOC), FE × 2 app +14/-6 each (filter + comment). KHÔNG ops git push (em main verify Reviewer rồi mới push).
|
||||
|
||||
@ -287,6 +287,12 @@ export function PurchaseEvaluationsListPage() {
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
||||
</div>
|
||||
{(p.drafterName || p.departmentName) && (
|
||||
<div className="mt-0.5 truncate text-[11px] text-slate-500">
|
||||
<span>👤 {p.drafterName ?? '—'}</span>
|
||||
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
||||
</div>
|
||||
)}
|
||||
{p.selectedSupplierName && (
|
||||
<div className="mt-0.5 truncate text-[11px] text-emerald-600">
|
||||
✓ {p.selectedSupplierName}
|
||||
|
||||
@ -122,6 +122,12 @@ export type PeListItem = {
|
||||
// update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu. FE list item hiển thị
|
||||
// "Tạo lúc createdAt" (KHÔNG còn SLA countdown "Còn N ngày").
|
||||
updatedAt: string | null
|
||||
// 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. BE JOIN Users + Departments LEFT, cả 2 nullable theo PE entity.
|
||||
drafterUserId: string | null
|
||||
drafterName: string | null
|
||||
departmentId: string | null
|
||||
departmentName: string | null
|
||||
}
|
||||
|
||||
export type PeSupplier = {
|
||||
|
||||
@ -287,6 +287,12 @@ export function PurchaseEvaluationsListPage() {
|
||||
<div className="mt-0.5 flex items-center gap-1.5 text-[11px] text-slate-500">
|
||||
<span className="font-mono">{p.maPhieu ?? '—'}</span>
|
||||
</div>
|
||||
{(p.drafterName || p.departmentName) && (
|
||||
<div className="mt-0.5 truncate text-[11px] text-slate-500">
|
||||
<span>👤 {p.drafterName ?? '—'}</span>
|
||||
{p.departmentName && <span className="text-slate-400"> · {p.departmentName}</span>}
|
||||
</div>
|
||||
)}
|
||||
{p.selectedSupplierName && (
|
||||
<div className="mt-0.5 truncate text-[11px] text-emerald-600">
|
||||
✓ {p.selectedSupplierName}
|
||||
|
||||
@ -121,6 +121,12 @@ export type PeListItem = {
|
||||
// update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu. FE list item hiển thị
|
||||
// "Tạo lúc createdAt" (KHÔNG còn SLA countdown "Còn N ngày").
|
||||
updatedAt: string | null
|
||||
// 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. BE JOIN Users + Departments LEFT, cả 2 nullable theo PE entity.
|
||||
drafterUserId: string | null
|
||||
drafterName: string | null
|
||||
departmentId: string | null
|
||||
departmentName: string | null
|
||||
}
|
||||
|
||||
export type PeSupplier = {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user