[CLAUDE] PurchaseEvaluation FE-Admin FE-User: Chunk L5 — PE list UX: ngày tạo thay SLA countdown + sort UpdatedAt DESC
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m10s

Bro UAT S23 t2 yêu cầu 2 UX changes PE list:

1. Đổi "Còn N ngày Mh" (SlaTimer countdown) → "DD/MM/YYYY HH:mm" (ngày giờ tạo phiếu).
2. Sort: phiếu vừa update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu, phiếu cũ phía dưới.

BE changes:
- PurchaseEvaluationListItemDto +UpdatedAt: DateTime? field (auto AuditingInterceptor refresh
  mọi SaveChanges — covers Insert/Update/Transition events natural).
- ListPurchaseEvaluationsQueryHandler sort: OrderByDescending(UpdatedAt ?? CreatedAt)
  (was: OrderByDescending(CreatedAt)).
- GetMyPurchaseEvaluationInboxQueryHandler sort: OrderByDescending(UpdatedAt ?? CreatedAt)
  (was: OrderBy(SlaDeadline ?? MaxValue) — SLA priority deprecated).
- CreateContractFromEvaluationFeatures.cs: +UpdatedAt arg trong DTO ctor (compile fix
  consumer downstream).
- Select projection 3 callsites populate UpdatedAt.

FE × 2 app (mirror rule §3.9):
- PeListItem type +updatedAt: string | null (optional — null khi phiếu chưa Update).
- PurchaseEvaluationsListPage: replace <SlaTimer deadline={p.slaDeadline} ... /> với
  Vietnamese date format "{DD/MM/YYYY HH:mm}" qua Intl.DateTimeFormat (vi-VN locale,
  full date+time options). title tooltip hiện full timestamp.
- Remove SlaTimer import (unused warning).

UpdatedAt sort logic insight: AuditingInterceptor (Infrastructure) auto-refresh
UpdatedAt mọi SaveChanges → mọi event tự nhiên (Drafter tạo / Gửi duyệt từ Workspace
/ Approver duyệt Cấp tiếp / Approver trả lại / Admin override) đều bump UpdatedAt
→ phiếu vừa action lên đầu list. Phiếu mới Insert UpdatedAt=null → fallback CreatedAt
→ vẫn lên đầu (vì CreatedAt vừa now).

Verify:
- dotnet build production projects clean (0 err, 2 pre-existing warn)
- dotnet test SolutionErp.slnx 104/104 PASS (DTO change KHÔNG impact test — tests
  don't construct ListItemDto)
- npm run build × 2 app pass clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-15 01:53:19 +07:00
parent da30e270c8
commit 83c9f7b45d
7 changed files with 43 additions and 10 deletions

View File

@ -7,7 +7,6 @@ import { ClipboardCheck, Search, X } from 'lucide-react'
import { Input } from '@/components/ui/Input' import { Input } from '@/components/ui/Input'
import { Select } from '@/components/ui/Select' import { Select } from '@/components/ui/Select'
import { EmptyState } from '@/components/EmptyState' import { EmptyState } from '@/components/EmptyState'
import { SlaTimer } from '@/components/SlaTimer'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError' import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn' import { cn } from '@/lib/cn'
@ -234,7 +233,15 @@ export function PurchaseEvaluationsListPage() {
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600"> <span className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600">
{PurchaseEvaluationTypeLabel[p.type]} {PurchaseEvaluationTypeLabel[p.type]}
</span> </span>
<SlaTimer deadline={p.slaDeadline} createdAt={p.createdAt} /> {/* S23 t2 UAT: bro yêu cầu đổi SLA countdown → ngày giờ tạo phiếu.
BE list sort theo UpdatedAt DESC (fallback CreatedAt) — phiếu vừa
update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu list. */}
<span className="font-medium text-slate-600" title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
{new Date(p.createdAt).toLocaleString('vi-VN', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})}
</span>
</div> </div>
{p.contractId && ( {p.contractId && (
<div className="mt-1 text-[10px] text-brand-600"> Đã tạo </div> <div className="mt-1 text-[10px] text-brand-600"> Đã tạo </div>

View File

@ -118,6 +118,10 @@ export type PeListItem = {
contractId: string | null contractId: string | null
slaDeadline: string | null slaDeadline: string | null
createdAt: string createdAt: string
// S23 t2 UAT: BE sort theo UpdatedAt DESC (fallback CreatedAt) — phiếu vừa
// 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
} }
export type PeSupplier = { export type PeSupplier = {

View File

@ -7,7 +7,6 @@ import { ClipboardCheck, Search, X } from 'lucide-react'
import { Input } from '@/components/ui/Input' import { Input } from '@/components/ui/Input'
import { Select } from '@/components/ui/Select' import { Select } from '@/components/ui/Select'
import { EmptyState } from '@/components/EmptyState' import { EmptyState } from '@/components/EmptyState'
import { SlaTimer } from '@/components/SlaTimer'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError' import { getErrorMessage } from '@/lib/apiError'
import { cn } from '@/lib/cn' import { cn } from '@/lib/cn'
@ -234,7 +233,15 @@ export function PurchaseEvaluationsListPage() {
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600"> <span className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-600">
{PurchaseEvaluationTypeLabel[p.type]} {PurchaseEvaluationTypeLabel[p.type]}
</span> </span>
<SlaTimer deadline={p.slaDeadline} createdAt={p.createdAt} /> {/* S23 t2 UAT: bro yêu cầu đổi SLA countdown → ngày giờ tạo phiếu.
BE list sort theo UpdatedAt DESC (fallback CreatedAt) — phiếu vừa
update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu list. */}
<span className="font-medium text-slate-600" title={`Tạo lúc ${new Date(p.createdAt).toLocaleString('vi-VN')}`}>
{new Date(p.createdAt).toLocaleString('vi-VN', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})}
</span>
</div> </div>
{p.contractId && ( {p.contractId && (
<div className="mt-1 text-[10px] text-brand-600"> Đã tạo </div> <div className="mt-1 text-[10px] text-brand-600"> Đã tạo </div>

View File

@ -117,6 +117,10 @@ export type PeListItem = {
contractId: string | null contractId: string | null
slaDeadline: string | null slaDeadline: string | null
createdAt: string createdAt: string
// S23 t2 UAT: BE sort theo UpdatedAt DESC (fallback CreatedAt) — phiếu vừa
// 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
} }
export type PeSupplier = { export type PeSupplier = {

View File

@ -140,6 +140,6 @@ public class ListApprovedPurchaseEvaluationsQueryHandler(IApplicationDbContext d
e.Id, e.MaPhieu, e.TenGoiThau, e.Type, e.Phase, e.Id, e.MaPhieu, e.TenGoiThau, e.Type, e.Phase,
e.ProjectId, p.Name, e.ProjectId, p.Name,
e.SelectedSupplierId, s != null ? s.Name : null, e.SelectedSupplierId, s != null ? s.Name : null,
e.ContractId, e.SlaDeadline, e.CreatedAt)).ToListAsync(ct); e.ContractId, e.SlaDeadline, e.CreatedAt, e.UpdatedAt)).ToListAsync(ct);
} }
} }

View File

@ -16,7 +16,11 @@ public record PurchaseEvaluationListItemDto(
string? SelectedSupplierName, string? SelectedSupplierName,
Guid? ContractId, Guid? ContractId,
DateTime? SlaDeadline, DateTime? SlaDeadline,
DateTime CreatedAt); DateTime CreatedAt,
// 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);
public record PurchaseEvaluationSupplierDto( public record PurchaseEvaluationSupplierDto(
Guid Id, Guid Id,

View File

@ -504,7 +504,13 @@ public class ListPurchaseEvaluationsQueryHandler(
x.p.Name.Contains(s)); x.p.Name.Contains(s));
} }
q = request.SortDesc ? q.OrderByDescending(x => x.e.CreatedAt) : q.OrderBy(x => x.e.CreatedAt); // S23 t2 UAT: sort theo "phiếu vừa update" — UpdatedAt fallback CreatedAt
// (phiếu mới Insert UpdatedAt=null → CreatedAt làm proxy). Latest event lên
// đầu (Tạo / Gửi duyệt / Trả lại / Approve advance pointer — mọi SaveChanges
// AuditingInterceptor refresh UpdatedAt). SortDesc=true default (mới đầu).
q = request.SortDesc
? q.OrderByDescending(x => x.e.UpdatedAt ?? x.e.CreatedAt)
: q.OrderBy(x => x.e.UpdatedAt ?? x.e.CreatedAt);
var total = await q.CountAsync(ct); var total = await q.CountAsync(ct);
var items = await q var items = await q
@ -513,7 +519,7 @@ public class ListPurchaseEvaluationsQueryHandler(
x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase, x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase,
x.e.ProjectId, x.p.Name, x.e.ProjectId, x.p.Name,
x.e.SelectedSupplierId, x.s != null ? x.s.Name : null, x.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt)) x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt))
.ToListAsync(ct); .ToListAsync(ct);
return new PagedResult<PurchaseEvaluationListItemDto>(items, total, request.Page, request.PageSize); return new PagedResult<PurchaseEvaluationListItemDto>(items, total, request.Page, request.PageSize);
@ -584,13 +590,14 @@ public class GetMyPurchaseEvaluationInboxQueryHandler(
if (request.ApprovalWorkflowId is not null) if (request.ApprovalWorkflowId is not null)
q = q.Where(x => x.e.ApprovalWorkflowId == request.ApprovalWorkflowId); q = q.Where(x => x.e.ApprovalWorkflowId == request.ApprovalWorkflowId);
// S23 t2 UAT: Inbox cũng sort theo "vừa update" lên đầu (mirror List sort).
return await q return await q
.OrderBy(x => x.e.SlaDeadline ?? DateTime.MaxValue) .OrderByDescending(x => x.e.UpdatedAt ?? x.e.CreatedAt)
.Select(x => new PurchaseEvaluationListItemDto( .Select(x => new PurchaseEvaluationListItemDto(
x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase, x.e.Id, x.e.MaPhieu, x.e.TenGoiThau, x.e.Type, x.e.Phase,
x.e.ProjectId, x.p.Name, x.e.ProjectId, x.p.Name,
x.e.SelectedSupplierId, x.s != null ? x.s.Name : null, x.e.SelectedSupplierId, x.s != null ? x.s.Name : null,
x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt)) x.e.ContractId, x.e.SlaDeadline, x.e.CreatedAt, x.e.UpdatedAt))
.Take(100) .Take(100)
.ToListAsync(ct); .ToListAsync(ct);
} }