import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, } from "@mui/material"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"; import DownloadIcon from "@mui/icons-material/Download"; import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; import { api } from "../api"; import { useToast } from "../toast"; import { useDialogActions } from "../dialogs"; interface AttachmentItem { id: number; fileName: string; uploadDate: string; fileType: string; fileSize: number; } function fmtSize(n: number) { if (!n) return ""; const kb = n / 1024; if (kb < 1024) return `${kb.toFixed(0)} KB`; return `${(kb / 1024).toFixed(1)} MB`; } function isImageType(t: string) { return (t || "").toLowerCase().startsWith("image/"); } function isPdfType(t: string) { return (t || "").toLowerCase() === "application/pdf"; } function guessKind(fileName: string): string { const n = (fileName || "").toLowerCase(); if (n.includes("cover")) return "Cover letter"; if (n.includes("resume") || n.includes("résumé") || n.includes(" cv") || n.endsWith("cv.pdf")) return "Resume"; if (n.includes("portfolio")) return "Portfolio"; return "Attachment"; } export default function Attachments({ jobId }: { jobId: number }) { const { toast } = useToast(); const { confirmAction, promptForValue } = useDialogActions(); const [items, setItems] = useState([]); const [previewOpen, setPreviewOpen] = useState(false); const [preview, setPreview] = useState<{ url: string; type: string; name: string } | null>(null); const load = useCallback(async () => { try { const res = await api.get(`/attachments/${jobId}`); setItems(res.data); } catch { // non-fatal } }, [jobId]); useEffect(() => { void load(); }, [load]); useEffect(() => { return () => { if (preview?.url) URL.revokeObjectURL(preview.url); }; }, [preview?.url]); const rename = async (a: AttachmentItem) => { const next = await promptForValue("Rename attachment to:", a.fileName, { title: "Rename attachment", confirmLabel: "Rename" }); if (!next || next.trim() === a.fileName) return; try { await api.patch(`/attachments/${a.id}`, { fileName: next.trim() }); toast("Renamed.", "success"); await load(); } catch { toast("Rename failed.", "error"); } }; const remove = async (a: AttachmentItem) => { if (!(await confirmAction(`Delete attachment "${a.fileName}"?`, { title: "Delete attachment", confirmLabel: "Delete", destructive: true }))) return; try { await api.delete(`/attachments/${a.id}`); toast("Deleted attachment.", "success"); await load(); } catch { toast("Delete failed.", "error"); } }; const upload = async (e: any) => { if (!e.target.files) return; const data = new FormData(); const files = Array.from(e.target.files) as File[]; files.forEach((f) => data.append("files", f)); data.append("jobId", jobId.toString()); try { await api.post("/attachments", data, { headers: { "Content-Type": "multipart/form-data" }, }); toast("Files uploaded.", "success"); await load(); } catch { toast("Upload failed.", "error"); } }; const getBlobUrl = async (id: number) => { const res = await api.get(`/attachments/download/${id}`, { responseType: "blob" }); const blob: Blob = res.data; return URL.createObjectURL(blob); }; const download = async (a: AttachmentItem) => { try { const url = await getBlobUrl(a.id); const link = document.createElement("a"); link.href = url; link.download = a.fileName || `attachment_${a.id}`; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 5000); } catch { toast("Download failed.", "error"); } }; const openPreview = async (a: AttachmentItem) => { try { if (preview?.url) URL.revokeObjectURL(preview.url); const url = await getBlobUrl(a.id); setPreview({ url, type: a.fileType || "", name: a.fileName }); setPreviewOpen(true); } catch { toast("Preview failed.", "error"); } }; const count = useMemo(() => items.length, [items.length]); return ( Attachments ({count}) Name Kind Type Size Uploaded Actions {items.map((a) => { const canPreview = isImageType(a.fileType) || isPdfType(a.fileType); const kind = guessKind(a.fileName); return ( {kind} {a.fileType ? a.fileType.replace("application/", "") : ""} {a.fileSize ? fmtSize(a.fileSize) : ""} {a.uploadDate ? new Date(a.uploadDate).toLocaleString() : ""} {canPreview ? ( void openPreview(a)} title="Preview"> ) : null} void download(a)} title="Download"> void rename(a)} title="Rename"> void remove(a)} title="Delete"> ); })} {items.length === 0 ? ( No attachments yet. ) : null}
setPreviewOpen(false)} fullWidth maxWidth="md"> Preview{preview?.name ? `: ${preview.name}` : ""} {!preview ? null : isImageType(preview.type) ? ( {preview.name} ) : isPdfType(preview.type) ? (