Files

166 lines
6.5 KiB
TypeScript

'use client';
// ADR-021: FilePreviewModal — แสดงไฟล์แนบ Workflow Step โดยไม่บังคับดาวน์โหลด (US4)
// รองรับ: PDF (iframe), Image (img), อื่นๆ (ลิงก์ดาวน์โหลด)
// Auth: ดึงไฟล์ผ่าน apiClient เพื่อแนบ JWT header อัตโนมัติ → แปลงเป็น BlobURL
import { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Loader2, Download, FileIcon } from 'lucide-react';
import type { AxiosError } from 'axios';
import apiClient from '@/lib/api/client';
import { useTranslations } from '@/hooks/use-translations';
import type { WorkflowAttachmentSummary } from '@/types/workflow';
interface FilePreviewModalProps {
attachment: WorkflowAttachmentSummary | null;
onClose: () => void;
// ADR-021 T041: เรียกเมื่อ API คืน 404 (ไฟล์ถูกลบออกจาก Storage)
onUnavailable?: (publicId: string) => void;
}
// แปลง bytes เป็น KB/MB สำหรับแสดงขนาดไฟล์
function formatBytes(bytes?: number): string {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
// ตรวจสอบว่า mimeType เป็น PDF หรือ Image
function getPreviewType(mimeType?: string): 'pdf' | 'image' | 'none' {
if (!mimeType) return 'none';
if (mimeType === 'application/pdf') return 'pdf';
if (mimeType.startsWith('image/')) return 'image';
return 'none';
}
export function FilePreviewModal({ attachment, onClose, onUnavailable }: FilePreviewModalProps) {
const t = useTranslations();
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// ดึงไฟล์จาก API เมื่อ attachment เปลี่ยน — แปลงเป็น BlobURL เพื่อรองรับ JWT auth
useEffect(() => {
if (!attachment) {
setBlobUrl(null);
return;
}
let currentUrl: string | null = null;
setIsLoading(true);
setError(null);
apiClient
.get(`/files/preview/${attachment.publicId}`, { responseType: 'blob' })
.then((res) => {
const url = URL.createObjectURL(res.data as Blob);
currentUrl = url;
setBlobUrl(url);
})
.catch((err: AxiosError) => {
// ADR-021 T041: ตรวจสอบ 404 เพื่อแยกแยะกรณี ไฟล์ถูกลบ vs เกิดผิดพลาดอื่น
if (err.response?.status === 404) {
setError(t('filePreview.fileUnavailable'));
if (attachment?.publicId) onUnavailable?.(attachment.publicId);
} else {
setError(t('filePreview.loadError'));
}
})
.finally(() => {
setIsLoading(false);
});
// Cleanup: เพิกถอน BlobURL เพื่อป้องกัน memory leak
return () => {
if (currentUrl) URL.revokeObjectURL(currentUrl);
};
}, [attachment, onUnavailable, t]);
const previewType = getPreviewType(attachment?.mimeType);
return (
<Dialog open={!!attachment} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-4xl w-full h-[85vh] flex flex-col p-0">
{/* Header — ชื่อไฟล์ + ขนาด */}
<DialogHeader className="px-6 pt-5 pb-3 border-b shrink-0">
<DialogTitle className="flex items-center gap-2 text-base truncate">
<FileIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{attachment?.originalFilename ?? t('filePreview.fallbackTitle')}</span>
{attachment?.fileSize && (
<span className="ml-auto text-xs text-muted-foreground font-normal shrink-0">
{formatBytes(attachment.fileSize)}
</span>
)}
</DialogTitle>
</DialogHeader>
{/* Body — Preview Area */}
<div className="flex-1 overflow-hidden relative bg-muted/30">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)}
{error && !isLoading && (
<div className="absolute inset-0 flex items-center justify-center text-sm text-muted-foreground">
{error ?? t('filePreview.loadError')}
</div>
)}
{!isLoading && !error && blobUrl && previewType === 'pdf' && (
<iframe
src={blobUrl}
className="w-full h-full border-0"
title={attachment?.originalFilename}
/>
)}
{!isLoading && !error && blobUrl && previewType === 'image' && (
<div className="w-full h-full flex items-center justify-center p-4">
<img
src={blobUrl}
alt={attachment?.originalFilename}
className="max-w-full max-h-full object-contain rounded"
/>
</div>
)}
{!isLoading && !error && blobUrl && previewType === 'none' && (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-muted-foreground">
<FileIcon className="h-12 w-12 opacity-30" />
<p>{t('filePreview.unsupported')}</p>
</div>
)}
</div>
{/* Footer — ปุ่มดาวน์โหลด + ปิด */}
<DialogFooter className="px-6 py-3 border-t shrink-0">
{blobUrl && (
<a
href={blobUrl}
download={attachment?.originalFilename}
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Download className="h-4 w-4" />
{t('filePreview.download')}
</a>
)}
<Button variant="outline" size="sm" onClick={onClose}>
{t('filePreview.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}