[CLAUDE] FE-Admin+FE-User: PE detail polish B12 — Lưu (no close), Xóa phiếu, header bar simplify, NCC name col, no-delete có quotes
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m12s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m12s
User feedback 2026-05-07 (annotation screenshot):
1. "Lưu" thay "Lưu (đóng)" — KHÔNG đóng workspace, chỉ toast + invalidate sync
2. Thêm nút "Xóa phiếu" bottom — CHỈ Bản nháp (DangSoanThao), KHÔNG xóa Trả lại
(đã có lịch sử workflow). Soft-delete (AuditableEntity IsDeleted=true,
không xóa hoàn toàn DB).
3. Bỏ nút "Sửa header" + "Đóng" + "Xóa" header bar workspace mode (chuyển
xuống bottom action bar). Header bar chỉ còn nhóm display info + nút "Đóng"
cho non-workspace view (Danh sách / Duyệt readOnly).
4. Section 4 column header NCC: dùng s.supplierName (master) thay vì
displayName ?? supplierName (custom). displayName fallback sang title tooltip.
5. Section 3 row Xóa: nếu NCC đã có quotes (báo giá ở Section 4) → KHÔNG cho
xóa (tránh mất báo giá). Hiển thị icon disabled + tooltip "xóa báo giá
trước rồi mới xóa NCC".
Implementation:
~ PeDetailTabs.tsx (× 2 app)
- Header bar workspace mode actions: bỏ "Sửa header" Pencil button (có
inline edit Section 1 + pencil hover Panel 1 thay thế), bỏ "Xóa" (chuyển
xuống bottom). "Đóng" giữ chỉ cho readOnly + non-workspace view.
- useNavigate import bỏ (chỉ dùng còn ở CreateContractDialog scope local).
- Bottom action bar workspace + canEdit + !readOnly:
* LEFT: "Xóa phiếu" red button (chỉ phase === DangSoanThao) + confirm
dialog "soft-delete, không xóa hoàn toàn DB" + onDelete callback (existing
DELETE /pe/:id endpoint, AuditableEntity IsDeleted=true).
* CENTER: status text "✓ Các thay đổi đã tự động lưu khi chỉnh sửa..."
* RIGHT: "Lưu" ghost button → invalidate ['pe-detail', id] + ['pe-list']
+ toast "Đã lưu — sync server" (KHÔNG onBack — workspace stay open).
* RIGHT: "Lưu & Gửi Duyệt →" giữ nguyên (POST transitions).
- SuppliersTab row actions: hasQuotes computed (= ev.details.some(d =>
d.quotes.some(q => q.purchaseEvaluationSupplierId === s.id))). canDelete
= !isWinner && !hasQuotes. Render Trash button enabled vs disabled span
với tooltip "xóa báo giá trước".
- ItemsTab matrix column header: {s.supplierName} (was {s.displayName ??
s.supplierName}). title attr giữ displayName tooltip.
Verify: npm run build fe-admin + fe-user pass · 0 TS error · áp rule strict
verify khi remove import + button/condition logic changes.
UAT mode: skip dotnet test (FE-only), push ngay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -69,10 +69,10 @@ export function PeDetailTabs({
|
||||
/** Auto open Section 1 InfoTab in edit mode khi mount — triggered từ pencil icon Panel 1 */
|
||||
autoEditHeader?: boolean
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
// isDraft renamed → canEditPhase: bao gồm cả TraLai (per user 2026-05-07).
|
||||
// Header bar action buttons (Sửa header / Xóa) hiện khi phase editable + !readOnly.
|
||||
// canEditPhase: bao gồm cả TraLai (user 2026-05-07). Header bar action
|
||||
// buttons "Sửa header" + "Xóa" + "Đóng" workspace mode đã chuyển xuống bottom
|
||||
// action bar (B11+ user 2026-05-07).
|
||||
const canEditPhase = isEditablePhase(evaluation.phase)
|
||||
const opinionsReadOnly = readOnly || mode === 'workspace'
|
||||
|
||||
@ -137,19 +137,14 @@ export function PeDetailTabs({
|
||||
{evaluation.drafterName && <><span>·</span><span>Soạn: {evaluation.drafterName}</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{canEditPhase && !readOnly && (
|
||||
<>
|
||||
<Button variant="ghost" onClick={() => navigate(`/purchase-evaluations/new?id=${evaluation.id}`)} className="gap-1.5 text-xs">
|
||||
<Pencil className="h-3.5 w-3.5" /> Sửa header
|
||||
</Button>
|
||||
<Button variant="danger" onClick={onDelete} className="gap-1.5 text-xs">
|
||||
<Trash2 className="h-3.5 w-3.5" /> Xóa
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="ghost" onClick={onBack} className="text-xs">← Đóng</Button>
|
||||
</div>
|
||||
{/* Header bar actions: User 2026-05-07 chốt bỏ "Sửa header" + "Xóa" +
|
||||
"Đóng" (workspace mode actions chuyển xuống bottom action bar). Vẫn
|
||||
giữ Đóng cho non-workspace view (Danh sách + Duyệt — readOnly). */}
|
||||
{(readOnly || mode !== 'workspace') && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={onBack} className="text-xs">← Đóng</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-200">
|
||||
@ -176,17 +171,45 @@ export function PeDetailTabs({
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
{/* Action bar bottom — workspace mode + canEdit + !readOnly. 2 nút Lưu
|
||||
(đóng workspace, các thay đổi đã auto-save inline) + Lưu & Gửi Duyệt
|
||||
(POST /transitions → next phase, vào quy trình duyệt). User 2026-05-07. */}
|
||||
{/* Action bar bottom — workspace mode + canEdit + !readOnly. 3 nút:
|
||||
- Xóa phiếu (CHỈ Bản nháp, soft-delete BE) — bên trái red
|
||||
- Lưu (toast confirm, KHÔNG đóng workspace) — chính giữa ghost
|
||||
- Lưu & Gửi Duyệt → (POST /transitions → next phase) — bên phải brand
|
||||
User 2026-05-07. */}
|
||||
{mode === 'workspace' && canEditPhase && !readOnly && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-slate-200 bg-slate-50 px-5 py-3">
|
||||
<div className="text-[11px] text-slate-500">
|
||||
✓ Các thay đổi đã tự động lưu khi chỉnh sửa từng phần.
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Xóa phiếu — CHỈ DangSoanThao (bản nháp). TraLai không cho xóa
|
||||
(đã có lịch sử workflow). Soft-delete qua DELETE /pe/:id endpoint
|
||||
(AuditableEntity IsDeleted=true, không xóa hoàn toàn DB). */}
|
||||
{evaluation.phase === PurchaseEvaluationPhase.DangSoanThao && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (confirm(`Xóa phiếu "${evaluation.tenGoiThau}"? Phiếu sẽ ẩn khỏi danh sách (soft-delete, không xóa hoàn toàn trong DB).`)) {
|
||||
onDelete()
|
||||
}
|
||||
}}
|
||||
className="gap-1.5 text-xs"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> Xóa phiếu
|
||||
</Button>
|
||||
)}
|
||||
<span className="text-[11px] text-slate-500">
|
||||
✓ Các thay đổi đã tự động lưu khi chỉnh sửa từng phần.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" onClick={onBack} className="text-xs">
|
||||
Lưu (đóng)
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
qc.invalidateQueries({ queryKey: ['pe-detail', evaluation.id] })
|
||||
qc.invalidateQueries({ queryKey: ['pe-list'] })
|
||||
toast.success('Đã lưu — sync server.')
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
Lưu
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -926,7 +949,11 @@ function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?:
|
||||
// User 2026-05-07: NCC đã được chọn (winner) → KHÔNG cho
|
||||
// sửa/xóa (tránh thay đổi NCC đã chốt). Chỉ hiển thị
|
||||
// checkmark active state.
|
||||
// User 2026-05-07 (B11+): NCC đã có hạng mục báo giá (quotes
|
||||
// entered in Section 4) → KHÔNG cho xóa (tránh mất báo giá đã nhập).
|
||||
const isWinner = ev.selectedSupplierId === s.supplierId
|
||||
const hasQuotes = ev.details.some(d => d.quotes.some(q => q.purchaseEvaluationSupplierId === s.id))
|
||||
const canDelete = !isWinner && !hasQuotes
|
||||
return (
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex justify-end gap-1">
|
||||
@ -943,22 +970,29 @@ function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?:
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{!isWinner && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setEditRow(s)}
|
||||
className="rounded px-1.5 py-0.5 text-slate-500 hover:bg-slate-100"
|
||||
title="Sửa"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (confirm('Xóa NCC này khỏi phiếu?')) remove.mutate(s.id) }}
|
||||
className="rounded px-1.5 py-0.5 text-red-500 hover:bg-red-50"
|
||||
title="Xóa"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</>
|
||||
<button
|
||||
onClick={() => setEditRow(s)}
|
||||
className="rounded px-1.5 py-0.5 text-slate-500 hover:bg-slate-100"
|
||||
title="Sửa"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{canDelete ? (
|
||||
<button
|
||||
onClick={() => { if (confirm('Xóa NCC này khỏi phiếu?')) remove.mutate(s.id) }}
|
||||
className="rounded px-1.5 py-0.5 text-red-500 hover:bg-red-50"
|
||||
title="Xóa"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : !isWinner && hasQuotes && (
|
||||
<span
|
||||
className="rounded px-1.5 py-0.5 text-slate-300 cursor-not-allowed"
|
||||
title="NCC đã có báo giá ở Section 4 — xóa báo giá trước rồi mới xóa NCC"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@ -1138,8 +1172,11 @@ function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boo
|
||||
</th>
|
||||
)}
|
||||
{ev.suppliers.map(s => (
|
||||
<th key={s.id} className="border-r border-slate-200 px-2 py-2 text-right">
|
||||
{s.displayName ?? s.supplierName}
|
||||
<th key={s.id} className="border-r border-slate-200 px-2 py-2 text-right" title={s.displayName ?? undefined}>
|
||||
{/* User 2026-05-07: dùng tên NCC (master) thay vì displayName
|
||||
(custom name) để column header rõ ràng. displayName fallback
|
||||
sang title tooltip nếu có. */}
|
||||
{s.supplierName}
|
||||
</th>
|
||||
))}
|
||||
{!readOnly && <th className="px-2 py-2"></th>}
|
||||
|
||||
Reference in New Issue
Block a user