diff --git a/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx b/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx new file mode 100644 index 0000000..85c9a01 --- /dev/null +++ b/fe-admin/src/components/pe/AttachmentPreviewDialog.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from 'react' +import { Loader2, AlertTriangle } from 'lucide-react' +import { Dialog } from '@/components/ui/Dialog' +import { Button } from '@/components/ui/Button' +import { api } from '@/lib/api' + +/** Extensions hỗ trợ preview inline (PDF native + images native). Word/Excel + * KHÔNG support — user phải download mở local Office. */ +const PREVIEWABLE_EXT = ['pdf', 'png', 'jpg', 'jpeg', 'webp', 'gif'] + +export function isPreviewable(fileName: string): boolean { + const ext = fileName.toLowerCase().split('.').pop() ?? '' + return PREVIEWABLE_EXT.includes(ext) +} + +type Props = { + open: boolean + evaluationId: string + attachmentId: string + fileName: string + onClose: () => void +} + +/** 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). */ +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 ext = fileName.toLowerCase().split('.').pop() ?? '' + const isImage = ['png', 'jpg', 'jpeg', 'webp', 'gif'].includes(ext) + + useEffect(() => { + if (!open) return + let cancelled = false + let currentUrl: string | null = null + setLoading(true) + setError(null) + + api.get(`/purchase-evaluations/${evaluationId}/attachments/${attachmentId}/view`, { + responseType: 'blob', + }) + .then(res => { + if (cancelled) return + currentUrl = window.URL.createObjectURL(res.data as Blob) + setBlobUrl(currentUrl) + setLoading(false) + }) + .catch(err => { + if (cancelled) return + setError(err?.message ?? 'Lỗi tải file') + setLoading(false) + }) + + return () => { + cancelled = true + if (currentUrl) window.URL.revokeObjectURL(currentUrl) + setBlobUrl(null) + } + }, [open, evaluationId, attachmentId]) + + return ( + Đóng} + > +
+ {loading && ( +
+ + Đang tải file… +
+ )} + {error && !loading && ( +
+ +
Không tải được file
+
{error}
+
+ )} + {blobUrl && !loading && !error && ( + isImage + ? {fileName} + :