From 2bf01184ca2414f8ae7446b4dd684474ca6ba315 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Thu, 21 May 2026 18:52:04 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20App+FE-User+FE-Admin:=20Plan=20AG4?= =?UTF-8?q?=20=E2=80=94=20b=E1=BB=95=20sung=20Drafter=20+=20Department=20v?= =?UTF-8?q?=C3=A0o=20PE=20List=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .claude/agent-memory/implementer/MEMORY.md | 2 ++ .../pages/pe/PurchaseEvaluationsListPage.tsx | 6 +++++ fe-admin/src/types/purchaseEvaluation.ts | 6 +++++ .../pages/pe/PurchaseEvaluationsListPage.tsx | 6 +++++ fe-user/src/types/purchaseEvaluation.ts | 6 +++++ .../CreateContractFromEvaluationFeatures.cs | 9 +++++++- .../Dtos/PurchaseEvaluationDtos.cs | 9 +++++++- .../PurchaseEvaluationFeatures.cs | 22 +++++++++++++++---- 8 files changed, 60 insertions(+), 6 deletions(-) diff --git a/.claude/agent-memory/implementer/MEMORY.md b/.claude/agent-memory/implementer/MEMORY.md index 6b5a68e..4748ae4 100644 --- a/.claude/agent-memory/implementer/MEMORY.md +++ b/.claude/agent-memory/implementer/MEMORY.md @@ -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 `
/` 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 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 `
/` + Tailwind named groups (`group/`) + localStorage Set 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). diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index e5a3565..d2003d5 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -287,6 +287,12 @@ export function PurchaseEvaluationsListPage() {
{p.maPhieu ?? '—'}
+ {(p.drafterName || p.departmentName) && ( +
+ 👤 {p.drafterName ?? '—'} + {p.departmentName && · {p.departmentName}} +
+ )} {p.selectedSupplierName && (
✓ {p.selectedSupplierName} diff --git a/fe-admin/src/types/purchaseEvaluation.ts b/fe-admin/src/types/purchaseEvaluation.ts index 98c0c25..9b63748 100644 --- a/fe-admin/src/types/purchaseEvaluation.ts +++ b/fe-admin/src/types/purchaseEvaluation.ts @@ -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 = { diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index e5a3565..d2003d5 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -287,6 +287,12 @@ export function PurchaseEvaluationsListPage() {
{p.maPhieu ?? '—'}
+ {(p.drafterName || p.departmentName) && ( +
+ 👤 {p.drafterName ?? '—'} + {p.departmentName && · {p.departmentName}} +
+ )} {p.selectedSupplierName && (
✓ {p.selectedSupplierName} diff --git a/fe-user/src/types/purchaseEvaluation.ts b/fe-user/src/types/purchaseEvaluation.ts index ec5b7fc..6bf3f38 100644 --- a/fe-user/src/types/purchaseEvaluation.ts +++ b/fe-user/src/types/purchaseEvaluation.ts @@ -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 = { diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs index 1983376..21e41b0 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs @@ -129,17 +129,24 @@ public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext d public async Task> 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); } } diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs index 4404b42..f0e0eb3 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs @@ -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, diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs index 888f72b..69d686e 100644 --- a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs @@ -468,11 +468,16 @@ public class ListPurchaseEvaluationsQueryHandler( public async Task> 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(items, total, request.Page, request.PageSize); @@ -587,12 +594,17 @@ public class GetMyPurchaseEvaluationInboxQueryHandler( ? new HashSet() : 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); }