From 5a6125494b5a8ee51f32db54023f37d052893463 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Tue, 23 Jun 2026 08:47:51 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20PurchaseEvaluation:=20CV=20PRO=20ref?= =?UTF-8?q?inement=20batch=20(anh=20Ki=E1=BB=87t=20FDC)=20=E2=80=94=20live?= =?UTF-8?q?=20budget=20recompute=20+=20clear=20winner=20+=20fullscreen=20p?= =?UTF-8?q?review=20+=20hide=20create=20c/d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 ẩn "c. Giá chào thầu" + "d. Bảng so sánh giá" khỏi form TẠO (vẫn hiện ở Detail tabs) C3 ô 8 "Giá trị TH dự kiến còn lại" pre-fill từ dòng 7 (Ngân sách còn lại) C4a live-recompute: VndInlineEdit +onLiveChange, PeBudgetSummaryTable draftRow3/8 → dòng 5/6/7/9 + So sánh + % nhảy NGAY khi gõ ô 3/8 (chưa cần Lưu) C4b So sánh = 0 → "Bằng ngân sách" / "—" thay "0 đ" (chỉ khi base>0) C5 hủy/đổi NCC winner: BE SelectWinnerBody(Guid?) + Command/Handler 2 nhánh (null=clear bỏ-qua-validate-list / non-null=giữ logic cũ); FE dropdown ""→clear + nút ✓ toggle. KHÔNG migration (SelectedSupplierId đã Guid?) C7 nút "Toàn màn hình" preview file báo giá (overlay inset-0 + Esc thoát) BE 2 file (Api controller record + Application command/handler). FE 3 file × 2 app SHA-identical. Test +10 PeSelectWinnerClearTests (4 clear + 4 select-regression + 2 PE-existence) → 366 PASS. Both FE build PASS, slnx build 0 err. Co-Authored-By: Claude Opus 4.8 --- .../components/pe/AttachmentPreviewDialog.tsx | 103 +++++-- fe-admin/src/components/pe/PeDetailTabs.tsx | 64 ++-- .../components/pe/PeWorkspaceCreateView.tsx | 10 +- .../components/pe/AttachmentPreviewDialog.tsx | 103 +++++-- fe-user/src/components/pe/PeDetailTabs.tsx | 64 ++-- .../components/pe/PeWorkspaceCreateView.tsx | 10 +- .../PurchaseEvaluationsController.cs | 2 +- .../PurchaseEvaluationDetailFeatures.cs | 32 +- .../Application/PeSelectWinnerClearTests.cs | 282 ++++++++++++++++++ 9 files changed, 546 insertions(+), 124 deletions(-) create mode 100644 tests/SolutionErp.Infrastructure.Tests/Application/PeSelectWinnerClearTests.cs diff --git a/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx b/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx index 85c9a01..38d45e7 100644 --- a/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx +++ b/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Loader2, AlertTriangle } from 'lucide-react' +import { Loader2, AlertTriangle, Maximize2, Minimize2 } from 'lucide-react' import { Dialog } from '@/components/ui/Dialog' import { Button } from '@/components/ui/Button' import { api } from '@/lib/api' @@ -24,17 +24,33 @@ type Props = { /** Preview file inline qua BE endpoint `/view` (Content-Disposition: inline). * Fetch as blob → object URL → iframe (PDF) hoặc img (image). * Bearer auth qua axios api client (KHÔNG thể set iframe src trực tiếp vì - * iframe không inherit Authorization header). */ + * iframe không inherit Authorization header). + * [C7 anh Kiệt FDC] Nút "Toàn màn hình" → lớp phủ inset-0 phóng to preview hết + * cỡ viewport (Dialog dùng chung chỉ có sm/md/lg → tự render overlay riêng). */ export function AttachmentPreviewDialog({ open, evaluationId, attachmentId, fileName, onClose, }: Props) { const [blobUrl, setBlobUrl] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [fullscreen, setFullscreen] = useState(false) const ext = fileName.toLowerCase().split('.').pop() ?? '' const isImage = ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(ext) + // Reset toàn-màn-hình mỗi khi đóng / đổi file. + useEffect(() => { if (!open) setFullscreen(false) }, [open]) + + // Esc khi đang toàn-màn-hình → thoát toàn-màn-hình TRƯỚC (không đóng luôn Dialog). + useEffect(() => { + if (!fullscreen) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { e.stopPropagation(); setFullscreen(false) } + } + window.addEventListener('keydown', onKey, true) + return () => window.removeEventListener('keydown', onKey, true) + }, [fullscreen]) + useEffect(() => { if (!open) return let cancelled = false @@ -64,34 +80,65 @@ export function AttachmentPreviewDialog({ } }, [open, evaluationId, attachmentId]) + const ready = blobUrl && !loading && !error + return ( - Đóng} - > -
- {loading && ( -
- - Đang tải file… + <> + + + + + } + > +
+ {loading && ( +
+ + Đang tải file… +
+ )} + {error && !loading && ( +
+ +
Không tải được file
+
{error}
+
+ )} + {ready && ( + isImage + ? {fileName} + :