feat(ui): add inline preview modal for invoice files in FileList
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useRef } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { Eye, Download, Trash2, X, FileText, Image as ImageIcon, ExternalLink } from "lucide-react";
|
||||||
import type { Attachment } from "../lib/types";
|
import type { Attachment } from "../lib/types";
|
||||||
|
|
||||||
export interface FileListProps {
|
export interface FileListProps {
|
||||||
@@ -14,9 +15,18 @@ export interface FileListProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// مدیریت چند فاکتور: آپلود مستقیم به سرور (MinIO) + دانلود/حذف فایلهای ذخیرهشده
|
const isImageFile = (name: string = "") => {
|
||||||
|
return /\.(jpe?g|png|webp|gif|svg|bmp)$/i.test(name.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPdfFile = (name: string = "") => {
|
||||||
|
return /\.pdf$/i.test(name.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
// مدیریت چند فاکتور: آپلود مستقیم، پیشنمایش آنلاین تصویر/PDF و دانلود
|
||||||
export function FileList({ files, onChange, upload, onDelete, urlOf, label = "افزودن فاکتور" }: FileListProps) {
|
export function FileList({ files, onChange, upload, onDelete, urlOf, label = "افزودن فاکتور" }: FileListProps) {
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [previewItem, setPreviewItem] = useState<{ url: string; name: string } | null>(null);
|
||||||
|
|
||||||
const remove = (i: number) => {
|
const remove = (i: number) => {
|
||||||
const removed = files[i];
|
const removed = files[i];
|
||||||
@@ -39,22 +49,94 @@ export function FileList({ files, onChange, upload, onDelete, urlOf, label = "ا
|
|||||||
if (added.length) onChange([...files, ...added]);
|
if (added.length) onChange([...files, ...added]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenPreview = (inv: Attachment) => {
|
||||||
|
const url = urlOf?.(inv);
|
||||||
|
if (!url) {
|
||||||
|
toast.error("فایل هنوز به طور کامل در سرور ذخیره نشده است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPreviewItem({ url, name: inv.name });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
{!files.length && <div className="inv-empty">هنوز فایلی انتخاب نشده است.</div>}
|
{!files.length && (
|
||||||
{files.map((inv, i) => (
|
<div className="rounded-lg border border-dashed border-gray-200 bg-gray-50/60 p-3 text-center text-xs text-gray-500">
|
||||||
<div key={i} className="inv-row">
|
هنوز فاکتور یا سندی بارگذاری نشده است.
|
||||||
<span className="inv-name">📎 {inv.name}</span>
|
|
||||||
{urlOf?.(inv) && (
|
|
||||||
<a className="inv-open" href={urlOf(inv)!} download={inv.name} title="دانلود">
|
|
||||||
↓
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<button type="button" className="inv-x" title="حذف" onClick={() => remove(i)}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{files.map((inv, i) => {
|
||||||
|
const url = urlOf?.(inv);
|
||||||
|
const isImg = isImageFile(inv.name);
|
||||||
|
const isPdf = isPdfFile(inv.name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={inv.id || i}
|
||||||
|
className="flex items-center justify-between rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs transition-colors hover:border-gray-300 shadow-xs"
|
||||||
|
>
|
||||||
|
{/* نام و آیکون فایل — با قابلیت کلیک برای پیشنمایش */}
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 min-w-0 flex-1 ${url ? "cursor-pointer group" : ""}`}
|
||||||
|
onClick={() => url && handleOpenPreview(inv)}
|
||||||
|
title={url ? "مشاهده پیشنمایش" : inv.name}
|
||||||
|
>
|
||||||
|
{isImg ? (
|
||||||
|
<ImageIcon className="h-4 w-4 shrink-0 text-blue-600" />
|
||||||
|
) : isPdf ? (
|
||||||
|
<FileText className="h-4 w-4 shrink-0 text-rose-600" />
|
||||||
|
) : (
|
||||||
|
<FileText className="h-4 w-4 shrink-0 text-gray-400" />
|
||||||
|
)}
|
||||||
|
<span className="truncate font-medium text-gray-700 group-hover:text-blue-600 transition-colors">
|
||||||
|
{inv.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* دکمههای عملیات: پیشنمایش، دانلود مستقیم، حذف */}
|
||||||
|
<div className="flex items-center gap-1 shrink-0 me-1">
|
||||||
|
{url && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleOpenPreview(inv)}
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md text-gray-500 hover:bg-blue-50 hover:text-blue-600 transition-colors"
|
||||||
|
title="مشاهده فاکتور"
|
||||||
|
>
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
download={inv.name}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 transition-colors"
|
||||||
|
title="دانلود فایل"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md text-gray-400 hover:bg-red-50 hover:text-red-600 transition-colors"
|
||||||
|
title="حذف فایل"
|
||||||
|
onClick={() => remove(i)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -64,10 +146,105 @@ export function FileList({ files, onChange, upload, onDelete, urlOf, label = "ا
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<button type="button" className="btn-line-file" onClick={() => inputRef.current?.click()}>
|
<button
|
||||||
+ {label}
|
type="button"
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-lg border border-dashed border-gray-300 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 hover:border-blue-500 hover:bg-blue-50/40 hover:text-blue-600 transition-all"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<span>+</span>
|
||||||
|
<span>{label}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* پنجره مودال پیشنمایش (Preview Modal) */}
|
||||||
|
{previewItem && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[80] flex items-center justify-center bg-black/60 p-3 sm:p-5 backdrop-blur-xs animate-in fade-in duration-200"
|
||||||
|
onClick={() => setPreviewItem(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-col w-full max-w-4xl max-h-[90vh] rounded-2xl border border-gray-200 bg-white shadow-2xl overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* هدر پیشنمایش */}
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-100 px-4 py-3 bg-white">
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
{isImageFile(previewItem.name) ? (
|
||||||
|
<ImageIcon className="h-4 w-4 shrink-0 text-blue-600" />
|
||||||
|
) : (
|
||||||
|
<FileText className="h-4 w-4 shrink-0 text-rose-600" />
|
||||||
|
)}
|
||||||
|
<span className="text-xs sm:text-sm font-bold text-gray-900 truncate">
|
||||||
|
{previewItem.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<a
|
||||||
|
href={previewItem.url}
|
||||||
|
download={previewItem.name}
|
||||||
|
className="inline-flex items-center gap-1 rounded-lg border border-gray-200 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-50 hover:text-gray-900 transition-colors shadow-2xs"
|
||||||
|
title="دانلود فاکتور"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
<span className="hidden xs:inline">دانلود فاکتور</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={previewItem.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 hover:text-gray-900 transition-colors shadow-2xs"
|
||||||
|
title="باز کردن در برگه جدید"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPreviewItem(null)}
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 hover:bg-gray-100 hover:text-gray-700 transition-colors ms-1"
|
||||||
|
title="بستن"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* بدنه نمایش فایل */}
|
||||||
|
<div className="flex-1 overflow-auto p-3 sm:p-4 bg-gray-50/70 flex items-center justify-center min-h-[300px]">
|
||||||
|
{isImageFile(previewItem.name) ? (
|
||||||
|
<img
|
||||||
|
src={previewItem.url}
|
||||||
|
alt={previewItem.name}
|
||||||
|
className="max-h-[75vh] w-auto max-w-full rounded-lg object-contain shadow-xs border border-gray-200 bg-white"
|
||||||
|
/>
|
||||||
|
) : isPdfFile(previewItem.name) ? (
|
||||||
|
<iframe
|
||||||
|
src={previewItem.url}
|
||||||
|
title={previewItem.name}
|
||||||
|
className="h-[75vh] w-full rounded-lg border border-gray-200 bg-white shadow-xs"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 p-8 text-center bg-white rounded-xl border border-gray-200 shadow-xs">
|
||||||
|
<FileText className="h-12 w-12 text-gray-400" />
|
||||||
|
<div className="text-xs text-gray-600">
|
||||||
|
امکان نمایش پیشنمایش زنده برای این فرمت فایل وجود ندارد.
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={previewItem.url}
|
||||||
|
download={previewItem.name}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3.5 py-1.5 text-xs font-semibold text-white hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Download className="h-3.5 w-3.5" />
|
||||||
|
دانلود فایل
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user