[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
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:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user