[CLAUDE] PurchaseEvaluation: PE gắn Hạng mục công việc (Mig 49) + mở quyền Pe all-role + menu Cá nhân + khóa 14 demo user
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m24s

Sếp chốt deadline 15:00 (Zalo 11:02-11:17): flow tạo phiếu chọn quy trình → dự án → HẠNG MỤC → NCC/TP; phiếu dạng «Dự án – Hạng mục»; all-user thấy Duyệt NCC + master config; clear data cũ.

- Mig 49 AddWorkItemToPurchaseEvaluation: PE.WorkItemId Guid? loose-Guid + index (KHÔNG FK vật lý — convention PE, database-agent design). Validator NotEmpty (create) + FK-guard AnyAsync(IsActive) → Conflict + UpdateDraft NULL-SAFE (client không gửi → giữ, chống null-hóa bug-class S42). 3 projection ListItemDto LEFT-join WorkItems.
- FE ×2 app: PeWorkspaceCreateView select «c. Hạng mục *» + PeHeaderForm (load existing + PUT gửi lại, SHA256 IDENTICAL) + PeDetailTabs (header «Dự án – Hạng mục» + FormRow + inline khóa) + types. Route reuse /catalogs/work-items.
- Perm: SeedAllRolesReviewReadPermissionsAsync extend Pe_* 11 key (factory — Pe leaf không nằm All) CanRead+CanCreate upgrade-only mọi role; PeWf_*/AwV2 GIỮ Admin. HRM/Office/Master/Catalogs CanRead (S57). Master write-lock Admin,CatalogManager ×3 controller.
- Menu «Cá nhân» (Personal root 30, mirror Puro) + Chấm công re-parent + HrmConfig→Master + parentBackfill idempotent + admin bỏ ẩn Master (đảo S29).
- LockDemoSampleUsersAsync: khóa 14/16 sample (GIỮ nv.cao+nv.truong IT-pool + catalog.manager) — ungated idempotent, IsActive=0+Lockout+SecurityStamp rotate.
- Tests +12 PeWorkItemGuardTests (validator/FK-guard/null-safe) → 240 PASS. npm ×2 + BE 0W/0E.
- Excel (3) đối chiếu: 62/71/3 identical S55 — no data change.
- Gate: em main evidence-checklist (2 reviewer-spawn die-0-byte — resume-kill; backstop 12 guard-test + authz-key/role-string/Mig-49 evidence-lệnh).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-11 12:13:26 +07:00
parent 17b23a418a
commit dd117b749c
26 changed files with 7461 additions and 29 deletions

View File

@ -24,6 +24,15 @@ import type { Paged, Project } from '@/types/master'
const parseVnd = (s: string): number => Number(s.replace(/[^\d]/g, '')) || 0
const formatVndInput = (n: number): string => (n > 0 ? n.toLocaleString('vi-VN') : '')
// S57bis — Hạng mục công việc (WorkItem master, mirror PeWorkspaceCreateView).
type WorkItemOption = {
id: string
code: string
name: string
category?: string | null
isActive?: boolean
}
export function PeHeaderForm({
editId,
defaultType,
@ -49,10 +58,18 @@ export function PeHeaderForm({
enabled: !!editId,
})
// S57bis — list Hạng mục công việc (active only, mirror PeWorkspaceCreateView).
const workItems = useQuery({
queryKey: ['catalogs', 'work-items'],
queryFn: async () => (await api.get<WorkItemOption[]>('/catalogs/work-items')).data,
select: rows => rows.filter(r => r.isActive !== false),
})
const [form, setForm] = useState({
type: initialType as number,
tenGoiThau: '',
projectId: '',
workItemId: '',
diaDiem: '',
moTa: '',
paymentTerms: '',
@ -82,6 +99,7 @@ export function PeHeaderForm({
type: existing.data.type,
tenGoiThau: existing.data.tenGoiThau,
projectId: existing.data.projectId,
workItemId: existing.data.workItemId ?? '', // S57bis — load để PUT gửi lại, tránh null-hóa
diaDiem: existing.data.diaDiem ?? '',
moTa: existing.data.moTa ?? '',
paymentTerms: existing.data.paymentTerms ?? '',
@ -113,6 +131,7 @@ export function PeHeaderForm({
return api.put(`/purchase-evaluations/${editId}`, {
id: editId,
tenGoiThau: form.tenGoiThau,
workItemId: form.workItemId || null, // S57bis — BE null-safe: null = giữ nguyên
diaDiem: form.diaDiem || null,
moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null,
@ -123,6 +142,7 @@ export function PeHeaderForm({
type: form.type,
tenGoiThau: form.tenGoiThau,
projectId: form.projectId,
workItemId: form.workItemId || null, // S57bis — create require (BE validator NotEmpty)
diaDiem: form.diaDiem || null,
moTa: form.moTa || null,
paymentTerms: form.paymentTerms || null,
@ -187,6 +207,23 @@ export function PeHeaderForm({
</Select>
</div>
<div>
{/* S57bis — phiếu dạng "Dự án Hạng mục công việc" (sếp chốt). Edit-mode
CHO đổi (BE UpdateDraft hỗ trợ); phiếu cũ null → bắt đầu rỗng, không ép. */}
<Label>Hạng mục công việc {!editId && '*'}</Label>
<Select
value={form.workItemId}
onChange={e => setForm({ ...form, workItemId: e.target.value })}
>
<option value="">{editId ? '— (giữ nguyên / chưa gắn)' : '-- Chọn --'}</option>
{workItems.data?.map(w => (
<option key={w.id} value={w.id}>
{w.category ? `[${w.category}] ` : ''}{w.code} {w.name}
</option>
))}
</Select>
</div>
<div>
<div className="mb-1.5 flex items-center justify-between">
<Label className="mb-0">Ngân sách (đi chiếu chi phí)</Label>
@ -272,7 +309,7 @@ export function PeHeaderForm({
)}
<Button
onClick={() => mut.mutate()}
disabled={!form.tenGoiThau || !form.projectId || mut.isPending}
disabled={!form.tenGoiThau || !form.projectId || (!editId && !form.workItemId) || mut.isPending}
>
{editId ? 'Lưu' : 'Tạo phiếu'}
</Button>