Workflow-App/Workflow/components/DocumentsPanel.tsx

232 lines
8.9 KiB
TypeScript
Raw Normal View History

"use client";
// components/DocumentsPanel.tsx
// Shared document panel for directors and companies.
// Lists documents, provides download + delete, and opens the upload modal.
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { pbList } from "@/lib/api";
import { deleteDirectorDocument, deleteCompanyDocument } from "@/lib/api";
import { useQueryClient } from "@tanstack/react-query";
import UploadDocumentModal from "./UploadDocumentModal";
import type { YsterbaardDirectorDocument, YsterbaardCompanyDocument } from "@/lib/types";
interface Props {
context: "director" | "company";
entityId: string;
}
const PB_URL = process.env.NEXT_PUBLIC_PB_URL ?? "https://pb.agent.apphub.co.za";
type DocItem = YsterbaardDirectorDocument | YsterbaardCompanyDocument;
function DocType({ doc }: { doc: DocItem }) {
const dt = doc.expand?.document_type;
if (!dt) return <span className="text-muted">—</span>;
return (
<span>
<span className="font-semibold text-fg">{dt.name}</span>
{dt.description ? <span className="text-muted text-xs ml-1">— {dt.description}</span> : null}
</span>
);
}
function FileIcon({ filename }: { filename: string }) {
const ext = filename?.split(".").pop()?.toLowerCase();
const isPdf = ext === "pdf";
return (
<span className={`inline-flex items-center justify-center w-8 h-8 rounded ${isPdf ? "bg-red-500/10 text-red-500" : "bg-blue-500/10 text-blue-500"} text-xs font-bold shrink-0`}>
{ext?.toUpperCase()}
</span>
);
}
export default function DocumentsPanel({ context, entityId }: Props) {
const qc = useQueryClient();
const [showUpload, setShowUpload] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
const collection =
context === "director"
? "ysterbaard_director_documents"
: "ysterbaard_company_documents";
const { data, isLoading } = useQuery({
queryKey: ["documents", context, entityId],
queryFn: () =>
pbList<DocItem>({
collection,
filter: context === "director" ? `director = '${entityId}'` : `company = '${entityId}'`,
sort: "-uploaded_at",
perPage: 100,
expand: "document_type",
fields: "*,expand.document_type.id,expand.document_type.name,expand.document_type.description",
}),
});
const docs = data?.items ?? [];
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
setDeleteError(null);
try {
if (context === "director") {
await deleteDirectorDocument(deleteId);
} else {
await deleteCompanyDocument(deleteId);
}
await qc.invalidateQueries({ queryKey: ["documents", context, entityId] });
setDeleteId(null);
} catch (err) {
setDeleteError(err instanceof Error ? err.message : "Delete failed");
} finally {
setDeleting(false);
}
};
return (
<>
<div className="bg-surface border border-border rounded-lg overflow-hidden">
{/* Header */}
<div className="px-5 py-4 border-b border-border flex items-center justify-between gap-3">
<div>
<h3 className="text-base font-bold text-fg">Documents</h3>
<p className="text-xs text-muted mt-0.5">
{docs.length} file{docs.length !== 1 ? "s" : ""} · PDF or JPEG, max 10MB
</p>
</div>
<button
onClick={() => setShowUpload(true)}
className="shrink-0 px-4 py-2 rounded-lg bg-accent text-accent-on text-sm font-bold hover:bg-accent-hover transition-colors"
>
+ Upload
</button>
</div>
{/* List */}
{isLoading ? (
<div className="p-8 text-center text-muted text-sm">Loading…</div>
) : docs.length === 0 ? (
<div className="p-8 text-center">
<p className="text-muted text-sm">No documents yet.</p>
<button
onClick={() => setShowUpload(true)}
className="mt-2 text-accent text-sm font-semibold hover:underline"
>
Upload the first document →
</button>
</div>
) : (
<ul className="divide-y divide-border">
{docs.map((doc) => {
const fileUrl = `${PB_URL}/api/files/${collection}/${doc.id}/${doc.file}`;
const uploadedAt = (doc as DocItem).uploaded_at ?? doc.created;
const ext = (doc.file as string)?.split(".").pop()?.toLowerCase() ?? "pdf";
return (
<li key={doc.id} className="px-5 py-4 flex items-start gap-3 hover:bg-surface-warm transition-colors">
{/* File type icon */}
<span className={`inline-flex items-center justify-center w-9 h-9 rounded shrink-0 mt-0.5 text-xs font-bold ${ext === "pdf" ? "bg-red-500/10 text-red-500" : "bg-blue-500/10 text-blue-500"}`}>
{ext.toUpperCase()}
</span>
{/* Info */}
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-fg truncate">
{(doc as DocItem).file ?? "Uploaded file"}
</p>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted">
<DocType doc={doc} />
<span>·</span>
<span>{uploadedAt ? new Date(uploadedAt).toLocaleDateString("en-ZA") : "—"}</span>
{(doc as DocItem).notes ? (
<>
<span>·</span>
<span className="text-fg-2 italic">{(doc as DocItem).notes}</span>
</>
) : null}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 shrink-0">
<a
href={fileUrl}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 rounded border border-border bg-surface-warm text-xs font-semibold text-fg hover:border-accent hover:text-accent transition-colors"
>
↓ Download
</a>
<button
onClick={() => setDeleteId(doc.id)}
className="px-3 py-1.5 rounded border border-border bg-surface-warm text-xs font-semibold text-muted hover:border-red-500 hover:text-red-500 transition-colors"
title="Delete document"
>
Delete
</button>
</div>
</li>
);
})}
</ul>
)}
</div>
{/* Upload modal */}
<UploadDocumentModal
context={context}
entityId={entityId}
open={showUpload}
onClose={() => setShowUpload(false)}
onUploaded={() => {
qc.invalidateQueries({ queryKey: ["documents", context, entityId] });
setShowUpload(false);
}}
/>
{/* Delete confirmation */}
{deleteId && (
<div
onClick={(e) => { if (e.target === e.currentTarget) setDeleteId(null); }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
role="dialog"
aria-modal="true"
>
<div className="bg-surface border border-border rounded-xl shadow-2xl w-full max-w-sm">
<div className="px-6 pt-6 pb-4">
<h2 className="text-lg font-bold text-fg">Delete this document?</h2>
<p className="text-sm text-muted mt-1">
This will permanently delete the file from PocketBase. This action cannot be undone.
</p>
</div>
{deleteError && (
<div className="mx-6 mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 text-sm text-red-400">
{deleteError}
</div>
)}
<div className="px-6 pb-6 flex gap-3">
<button
onClick={() => setDeleteId(null)}
className="flex-1 px-4 py-2.5 rounded-lg border border-border bg-surface-warm text-sm font-semibold text-fg hover:bg-surface"
>
Cancel
</button>
<button
onClick={handleDelete}
disabled={deleting}
className="flex-1 px-4 py-2.5 rounded-lg bg-red-600 text-white text-sm font-bold hover:bg-red-700 disabled:opacity-60 disabled:cursor-not-allowed"
>
{deleting ? "Deleting..." : "Yes, delete"}
</button>
</div>
</div>
</div>
)}
</>
);
}