From eda9e84187d985a63fee07ef2e06a7eeaea96117 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 24 Apr 2026 13:13:40 +0700 Subject: [PATCH] [CLAUDE] PE: readOnly mode cho menu 'Duyet' (pendingMe=1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User request: 'Menu duyet cua NCC -> chi de duyet thoi nhe khong co cac action them sua j vao'. Them readOnly prop vao PeDetailTabs — propagate xuong 3 sub-component (InfoTab / SuppliersTab / ItemsTab) + SupplierAttachmentsCell. URL pendingMe=1 (menu 'Duyet') → set readOnly=true. Hide khi readOnly: - Header: [Sua header] [Xoa] button - SuppliersTab: [+ Them NCC] button + action column (Check winner/Pencil edit/Trash delete per row) - ItemsTab: [+ Them hang muc] button + action column (Pencil/Trash per row) + click cell bao gia popup - SupplierAttachmentsCell: [+ Them file] button + Trash delete icon (giu download tren file name) - InfoTab: [Tao HD tu phieu] button Giu: - Moi thong tin doc-only - Download file dinh kem (click ten file) - Panel 3: Quy trinh + transition button (de action duyet phase) - [Dong] button - Chip 'che do duyet' gan phase badge de user biet mode Mirror fe-admin + fe-user. --- fe-admin/src/components/pe/PeDetailTabs.tsx | 205 ++++++++++-------- .../pages/pe/PurchaseEvaluationsListPage.tsx | 1 + fe-user/src/components/pe/PeDetailTabs.tsx | 205 ++++++++++-------- .../pages/pe/PurchaseEvaluationsListPage.tsx | 1 + 4 files changed, 238 insertions(+), 174 deletions(-) diff --git a/fe-admin/src/components/pe/PeDetailTabs.tsx b/fe-admin/src/components/pe/PeDetailTabs.tsx index a6c9e41..056733c 100644 --- a/fe-admin/src/components/pe/PeDetailTabs.tsx +++ b/fe-admin/src/components/pe/PeDetailTabs.tsx @@ -39,10 +39,13 @@ export function PeDetailTabs({ evaluation, onBack, onDelete, + readOnly = false, }: { evaluation: PeDetailBundle onBack: () => void onDelete: () => void + /** Menu "Duyệt" (pendingMe=1) — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */ + readOnly?: boolean }) { const navigate = useNavigate() const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao @@ -61,6 +64,11 @@ export function PeDetailTabs({ > {PurchaseEvaluationPhaseLabel[evaluation.phase]} + {readOnly && ( + + chế độ duyệt + + )}
{evaluation.maPhieu ?? '—'} @@ -72,7 +80,7 @@ export function PeDetailTabs({
- {isDraft && ( + {isDraft && !readOnly && ( <>
@@ -131,8 +139,8 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) { } // ===== Tab: Thông tin ===== -function InfoTab({ ev }: { ev: PeDetailBundle }) { - const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId +function InfoTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { + const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId const [createOpen, setCreateOpen] = useState(false) return (
@@ -236,7 +244,7 @@ function Field({ label, value }: { label: string; value: React.ReactNode }) { } // ===== Tab: NCC ===== -function SuppliersTab({ ev }: { ev: PeDetailBundle }) { +function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { const qc = useQueryClient() const [open, setOpen] = useState(false) const [editRow, setEditRow] = useState(null) @@ -255,13 +263,17 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { return (
-
- -
+ {!readOnly && ( +
+ +
+ )} {ev.suppliers.length === 0 ? ( -

Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.

+

+ {readOnly ? 'Chưa có NCC.' : 'Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.'} +

) : (
@@ -271,7 +283,7 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { - + {!readOnly && } @@ -281,6 +293,9 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) {
{s.supplierName}
{s.displayName &&
{s.displayName}
} {s.note &&
{s.note}
} + {readOnly && ev.selectedSupplierId === s.supplierId && ( +
✓ NCC được chọn
+ )} - + {!readOnly && ( + + )} ))} @@ -431,7 +449,7 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri } // ===== Tab: Hạng mục + Báo giá (matrix) ===== -function ItemsTab({ ev }: { ev: PeDetailBundle }) { +function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { const qc = useQueryClient() const [addOpen, setAddOpen] = useState(false) const [editDetail, setEditDetail] = useState(null) @@ -451,12 +469,16 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) {

{ev.suppliers.length === 0 - ? 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.' - : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`} + ? (readOnly ? 'Chưa có NCC tham gia.' : 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.') + : readOnly + ? `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC` + : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`}

- + {!readOnly && ( + + )}
{ev.details.length === 0 ? ( @@ -475,7 +497,7 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { {s.displayName ?? s.supplierName} ))} -
+ {!readOnly && } @@ -493,9 +515,10 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { return ( ) })} - + {!readOnly && ( + + )} ))} @@ -729,10 +754,12 @@ function SupplierAttachmentsCell({ evaluationId, supplierRowId, attachments, + readOnly = false, }: { evaluationId: string supplierRowId: string attachments: PeAttachment[] + readOnly?: boolean }) { const qc = useQueryClient() const fileInputRef = useRef(null) @@ -809,32 +836,36 @@ function SupplierAttachmentsCell({ {PeAttachmentPurposeLabel[a.purpose] ?? ''} - + {!readOnly && ( + + )} ))} -
- - -
+ {!readOnly && ( +
+ + +
+ )} ) } diff --git a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx index a953083..fa017a7 100644 --- a/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-admin/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -204,6 +204,7 @@ export function PurchaseEvaluationsListPage() { evaluation={detail.data} onBack={() => setParam('id', null)} onDelete={() => del.mutate(detail.data!.id)} + readOnly={pendingMe} /> )} diff --git a/fe-user/src/components/pe/PeDetailTabs.tsx b/fe-user/src/components/pe/PeDetailTabs.tsx index a6c9e41..056733c 100644 --- a/fe-user/src/components/pe/PeDetailTabs.tsx +++ b/fe-user/src/components/pe/PeDetailTabs.tsx @@ -39,10 +39,13 @@ export function PeDetailTabs({ evaluation, onBack, onDelete, + readOnly = false, }: { evaluation: PeDetailBundle onBack: () => void onDelete: () => void + /** Menu "Duyệt" (pendingMe=1) — ẩn mọi action thêm/sửa/xóa, chỉ xem + duyệt phase. */ + readOnly?: boolean }) { const navigate = useNavigate() const isDraft = evaluation.phase === PurchaseEvaluationPhase.DangSoanThao @@ -61,6 +64,11 @@ export function PeDetailTabs({ > {PurchaseEvaluationPhaseLabel[evaluation.phase]} + {readOnly && ( + + chế độ duyệt + + )}
{evaluation.maPhieu ?? '—'} @@ -72,7 +80,7 @@ export function PeDetailTabs({
- {isDraft && ( + {isDraft && !readOnly && ( <>
@@ -131,8 +139,8 @@ export function PeHistorySection({ ev }: { ev: PeDetailBundle }) { } // ===== Tab: Thông tin ===== -function InfoTab({ ev }: { ev: PeDetailBundle }) { - const canCreateContract = ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId +function InfoTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { + const canCreateContract = !readOnly && ev.phase === PurchaseEvaluationPhase.DaDuyet && !ev.contractId && ev.selectedSupplierId const [createOpen, setCreateOpen] = useState(false) return (
@@ -236,7 +244,7 @@ function Field({ label, value }: { label: string; value: React.ReactNode }) { } // ===== Tab: NCC ===== -function SuppliersTab({ ev }: { ev: PeDetailBundle }) { +function SuppliersTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { const qc = useQueryClient() const [open, setOpen] = useState(false) const [editRow, setEditRow] = useState(null) @@ -255,13 +263,17 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { return (
-
- -
+ {!readOnly && ( +
+ +
+ )} {ev.suppliers.length === 0 ? ( -

Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.

+

+ {readOnly ? 'Chưa có NCC.' : 'Chưa có NCC. Thêm NCC để bắt đầu so sánh giá.'} +

) : (
Liên hệ Điều khoản TT File đính kèm
{s.contactName &&
{s.contactName}
} @@ -293,38 +308,41 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { evaluationId={ev.id} supplierRowId={s.id} attachments={ev.attachments.filter(a => a.purchaseEvaluationSupplierId === s.id)} + readOnly={readOnly} />
-
- - - -
-
+
+ + + +
+
setQuoteEdit({ detail: d, supplier: s, existing: q })} + onClick={readOnly ? undefined : () => setQuoteEdit({ detail: d, supplier: s, existing: q })} className={cn( - 'cursor-pointer border-r border-slate-200 px-2 py-2 text-right font-mono transition hover:bg-brand-50', + 'border-r border-slate-200 px-2 py-2 text-right font-mono transition', + !readOnly && 'cursor-pointer hover:bg-brand-50', q?.isSelected && 'bg-emerald-50 font-semibold text-emerald-700', )} > @@ -503,16 +526,18 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { -
- - -
-
+
+ + +
+
@@ -271,7 +283,7 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { - + {!readOnly && } @@ -281,6 +293,9 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) {
{s.supplierName}
{s.displayName &&
{s.displayName}
} {s.note &&
{s.note}
} + {readOnly && ev.selectedSupplierId === s.supplierId && ( +
✓ NCC được chọn
+ )} - + {!readOnly && ( + + )} ))} @@ -431,7 +449,7 @@ function EditSupplierDialog({ evaluationId, row, onClose }: { evaluationId: stri } // ===== Tab: Hạng mục + Báo giá (matrix) ===== -function ItemsTab({ ev }: { ev: PeDetailBundle }) { +function ItemsTab({ ev, readOnly = false }: { ev: PeDetailBundle; readOnly?: boolean }) { const qc = useQueryClient() const [addOpen, setAddOpen] = useState(false) const [editDetail, setEditDetail] = useState(null) @@ -451,12 +469,16 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) {

{ev.suppliers.length === 0 - ? 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.' - : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`} + ? (readOnly ? 'Chưa có NCC tham gia.' : 'Thêm NCC ở tab "NCC" trước khi nhập báo giá.') + : readOnly + ? `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC` + : `${ev.details.length} hạng mục × ${ev.suppliers.length} NCC — click ô để nhập báo giá.`}

- + {!readOnly && ( + + )}
{ev.details.length === 0 ? ( @@ -475,7 +497,7 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { {s.displayName ?? s.supplierName} ))} -
+ {!readOnly && } @@ -493,9 +515,10 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { return ( ) })} - + {!readOnly && ( + + )} ))} @@ -729,10 +754,12 @@ function SupplierAttachmentsCell({ evaluationId, supplierRowId, attachments, + readOnly = false, }: { evaluationId: string supplierRowId: string attachments: PeAttachment[] + readOnly?: boolean }) { const qc = useQueryClient() const fileInputRef = useRef(null) @@ -809,32 +836,36 @@ function SupplierAttachmentsCell({ {PeAttachmentPurposeLabel[a.purpose] ?? ''} - + {!readOnly && ( + + )} ))} -
- - -
+ {!readOnly && ( +
+ + +
+ )} ) } diff --git a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx index a953083..fa017a7 100644 --- a/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx +++ b/fe-user/src/pages/pe/PurchaseEvaluationsListPage.tsx @@ -204,6 +204,7 @@ export function PurchaseEvaluationsListPage() { evaluation={detail.data} onBack={() => setParam('id', null)} onDelete={() => del.mutate(detail.data!.id)} + readOnly={pendingMe} /> )}
Liên hệ Điều khoản TT File đính kèm
{s.contactName &&
{s.contactName}
} @@ -293,38 +308,41 @@ function SuppliersTab({ ev }: { ev: PeDetailBundle }) { evaluationId={ev.id} supplierRowId={s.id} attachments={ev.attachments.filter(a => a.purchaseEvaluationSupplierId === s.id)} + readOnly={readOnly} />
-
- - - -
-
+
+ + + +
+
setQuoteEdit({ detail: d, supplier: s, existing: q })} + onClick={readOnly ? undefined : () => setQuoteEdit({ detail: d, supplier: s, existing: q })} className={cn( - 'cursor-pointer border-r border-slate-200 px-2 py-2 text-right font-mono transition hover:bg-brand-50', + 'border-r border-slate-200 px-2 py-2 text-right font-mono transition', + !readOnly && 'cursor-pointer hover:bg-brand-50', q?.isSelected && 'bg-emerald-50 font-semibold text-emerald-700', )} > @@ -503,16 +526,18 @@ function ItemsTab({ ev }: { ev: PeDetailBundle }) { -
- - -
-
+
+ + +
+