"use client" import { useParams, useRouter } from "next/navigation" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useState, useRef, useTransition } from "react" import { Plus, Trash2, FileIcon, ImageIcon, FileTextIcon } from "lucide-react" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { confirm } from "@/shared/components/confirm-dialog" import { Button } from "@/shared/components/ui/button" import { Card, CardContent } from "@/shared/components/ui/card" import { ApiError, JOB_CARD_ROUTES } from "@garage/api" import DashboardPage from "@/base/components/layout/dashboard/dashboard-page" import { useJobCard } from "@/modules/job-cards/job-card-context" import { CONSTANTS } from "@/config/constants" function getFileIcon(mimeType?: string) { if (mimeType?.startsWith("image/")) return ImageIcon if (mimeType?.includes("pdf")) return FileTextIcon return FileIcon } const ALLOWED_HINT = "Allowed: images, PDF, Office docs, audio, video." function describeAttachmentError(rawMessage: string, filename: string): string { const msg = rawMessage.toLowerCase() if (msg.includes("must be a file of type") || msg.includes("mimes")) { return `${filename}: unsupported file type. ${ALLOWED_HINT}` } if (msg.includes("may not be greater") || msg.includes("max")) { return `${filename}: exceeds 5 MB limit.` } if (msg.includes("must be a file")) { return `${filename}: invalid file.` } return `${filename}: ${rawMessage}` } function showUploadError(err: unknown, files: File[]) { if (!(err instanceof ApiError)) { toast.error("Failed to upload attachment(s)") return } const validation = err.validationErrors if (!validation || Object.keys(validation).length === 0) { toast.error(err.payload?.message ?? "Failed to upload attachment(s)") return } const messages: string[] = [] for (const [key, msgs] of Object.entries(validation)) { const firstMsg = Array.isArray(msgs) ? msgs[0] : String(msgs) if (!firstMsg) continue const match = key.match(/^attachments\.(\d+)$/) if (match) { const idx = Number(match[1]) const filename = files[idx]?.name ?? `File #${idx + 1}` messages.push(describeAttachmentError(firstMsg, filename)) } else if (key === "attachments") { messages.push(firstMsg) } else { messages.push(firstMsg) } } if (messages.length === 0) { toast.error(err.payload?.message ?? "Failed to upload attachment(s)") return } const shown = messages.slice(0, 3) const extra = messages.length - shown.length shown.forEach((m) => toast.error(m)) if (extra > 0) toast.error(`...and ${extra} more`) } export default function JobCardAttachmentsPage() { const { id: jobCardId } = useParams<{ id: string }>() const api = useAuthApi() const queryClient = useQueryClient() const router = useRouter() const [isRefreshing, startRefreshTransition] = useTransition() const fileInputRef = useRef(null) const [isUploading, setIsUploading] = useState(false) const queryKey = [JOB_CARD_ROUTES.INDEX, jobCardId, "attachments"] const jobcard = useJobCard() const attachments = jobcard?.attachment_files const deleteMutation = useMutation({ mutationFn: (attachmentId: number) => api.jobCards.deleteAttachment(jobCardId, attachmentId), onSuccess: () => { toast.success("Attachment deleted successfully.") queryClient.invalidateQueries({ queryKey }) startRefreshTransition(() => router.refresh()) }, onError: () => { toast.error("Failed to delete attachment.") }, }) const handleDelete = async (attachment: any) => { const confirmed = await confirm({ title: "Delete Attachment", description: `Are you sure you want to delete "${attachment.original_name}"?`, confirmLabel: "Delete", variant: "destructive", }) if (confirmed) { deleteMutation.mutate(attachment.id) } } const handleUpload = async (e: React.ChangeEvent) => { const files = e.target.files if (!files || files.length === 0) return const fileList = Array.from(files) setIsUploading(true) const loadingToast = toast.loading("Uploading attachment(s)...") try { await api.jobCards.addAttachment(jobCardId, fileList) toast.dismiss(loadingToast) toast.success("Attachment(s) uploaded successfully") queryClient.invalidateQueries({ queryKey }) startRefreshTransition(() => router.refresh()) } catch (err) { toast.dismiss(loadingToast) showUploadError(err, fileList) } finally { setIsUploading(false) if (fileInputRef.current) { fileInputRef.current.value = "" } } } return (
{attachments?.length === 0 ? ( No attachments yet. Click "Upload Attachment" to add files. ) : (
{(attachments as any[])?.map((attachment) => { const Icon = getFileIcon(attachment.attachment_path) return (
{attachment.original_name} {attachment.created_at && ( {new Date(attachment.created_at).toLocaleDateString()} )}
) })}
)}
) }