160 lines
6.5 KiB
TypeScript

"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 { 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
}
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<HTMLInputElement>(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<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return
setIsUploading(true)
const promise = api.jobCards.addAttachment(jobCardId, Array.from(files))
toast.promise(promise, {
loading: "Uploading attachment(s)...",
success: "Attachment(s) uploaded successfully",
error: "Failed to upload attachment(s)",
})
try {
await promise
queryClient.invalidateQueries({ queryKey })
startRefreshTransition(() => router.refresh())
} finally {
setIsUploading(false)
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}
}
return (
<DashboardPage
header={null}
>
<div className="flex items-center justify-end mb-4">
<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={handleUpload}
/>
<Button
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
<Plus className="size-4" />
{isUploading ? "Uploading..." : "Upload Attachment"}
</Button>
</div>
{attachments?.length === 0 ? (
<Card>
<CardContent className="py-8 text-center text-muted-foreground">
No attachments yet. Click "Upload Attachment" to add files.
</CardContent>
</Card>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{(attachments as any[])?.map((attachment) => {
const Icon = getFileIcon(attachment.attachment_path)
return (
<Card key={attachment.id}>
<CardContent className="flex items-center gap-3 p-4">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
<Icon className="size-5" />
</div>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<a
href={CONSTANTS.getAssetUrl(attachment.attachment_path)}
target="_blank"
rel="noopener noreferrer"
className="truncate text-sm font-medium hover:underline"
title={attachment.original_name}
>
{attachment.original_name}
</a>
{attachment.created_at && (
<span className="text-xs text-muted-foreground">
{new Date(attachment.created_at).toLocaleDateString()}
</span>
)}
</div>
<Button
variant="ghost"
size="icon-sm"
onClick={() => handleDelete(attachment)}
title="Delete attachment"
>
<Trash2 className="size-4 text-destructive" />
</Button>
</CardContent>
</Card>
)
})}
</div>
)}
</DashboardPage>
)
}