WORKFLOW-APP/components/DocumentTypeManager.tsx

241 lines
9 KiB
TypeScript
Raw Permalink Normal View History

"use client";
// components/DocumentTypeManager.tsx
// Modal for managing document types: rename name/description.
// Located under the filters bar on the Directors/Companies tab pages.
import { useState, useEffect, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
fetchDocumentTypes,
updateDocumentType,
deleteDocumentType,
type DocType,
} from "@/lib/api";
interface Props {
context: "director" | "company";
open: boolean;
onClose: () => void;
}
export default function DocumentTypeManager({ context, open, onClose }: Props) {
const qc = useQueryClient();
const overlayRef = useRef<HTMLDivElement>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editDesc, setEditDesc] = useState("");
const [saving, setSaving] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const { data, isLoading } = useQuery({
queryKey: ["document-types", context],
queryFn: () => fetchDocumentTypes(context),
enabled: open,
});
useEffect(() => {
if (!open) {
setEditingId(null);
setDeleteId(null);
setError(null);
}
}, [open]);
if (!open) return null;
const types: DocType[] = data?.items ?? [];
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === overlayRef.current) onClose();
};
const startEdit = (t: DocType) => {
setEditingId(t.id);
setEditName(t.name);
setEditDesc(t.description ?? "");
setError(null);
};
const handleSaveEdit = async () => {
if (!editName.trim()) return;
setSaving(true);
setError(null);
try {
await updateDocumentType(editingId!, {
name: editName.trim(),
description: editDesc.trim() || undefined,
});
await qc.invalidateQueries({ queryKey: ["document-types", context] });
setEditingId(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Save failed");
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!deleteId) return;
setDeleting(true);
setError(null);
try {
await deleteDocumentType(deleteId);
await qc.invalidateQueries({ queryKey: ["document-types", context] });
setDeleteId(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed");
} finally {
setDeleting(false);
}
};
return (
<div
ref={overlayRef}
onClick={handleOverlayClick}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
role="dialog"
aria-modal="true"
aria-label="Manage document types"
>
<div className="bg-surface border border-border rounded-xl shadow-2xl w-full max-w-lg flex flex-col max-h-[85vh]">
{/* Header */}
<div className="px-6 pt-6 pb-4 border-b border-border flex-shrink-0">
<div className="flex items-start justify-between">
<div>
<h2 className="text-lg font-bold text-fg">Document types</h2>
<p className="text-sm text-muted mt-0.5">
Rename or delete types. Docs using a deleted type will show the old type name.
</p>
</div>
<button
onClick={onClose}
className="ml-4 text-muted hover:text-fg text-xl leading-none p-1 rounded transition-colors"
>
×
</button>
</div>
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-5">
{error && (
<div className="mb-4 bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 text-sm text-red-400">
{error}
</div>
)}
{isLoading ? (
<p className="text-center text-muted py-8">Loading…</p>
) : types.length === 0 ? (
<p className="text-center text-muted py-8">
No {context} document types yet. Create one when you upload a document.
</p>
) : (
<ul className="space-y-2">
{types.map((t) => (
<li key={t.id} className="border border-border rounded-lg p-4">
{editingId === t.id ? (
/* Edit form */
<div className="space-y-3">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full rounded border border-border bg-surface-warm px-3 py-2 text-sm text-fg focus:outline-none focus:border-accent"
placeholder="Type name"
required
/>
<input
type="text"
value={editDesc}
onChange={(e) => setEditDesc(e.target.value)}
className="w-full rounded border border-border bg-surface-warm px-3 py-2 text-sm text-fg placeholder:text-muted focus:outline-none focus:border-accent"
placeholder="Description (optional)"
/>
<div className="flex gap-2">
<button
onClick={() => setEditingId(null)}
className="flex-1 px-3 py-2 rounded border border-border bg-surface-warm text-xs font-semibold text-fg hover:bg-surface"
>
Cancel
</button>
<button
onClick={handleSaveEdit}
disabled={saving || !editName.trim()}
className="flex-1 px-3 py-2 rounded bg-accent text-accent-on text-xs font-bold hover:bg-accent-hover disabled:opacity-60"
>
{saving ? "Saving..." : "Save"}
</button>
</div>
</div>
) : deleteId === t.id ? (
/* Delete confirm */
<div className="space-y-3">
<p className="text-sm text-fg">
Delete type <strong>"{t.name}"</strong>? Existing documents will keep this name.
</p>
<div className="flex gap-2">
<button
onClick={() => setDeleteId(null)}
className="flex-1 px-3 py-2 rounded border border-border bg-surface-warm text-xs font-semibold text-fg hover:bg-surface"
>
Cancel
</button>
<button
onClick={handleDelete}
disabled={deleting}
className="flex-1 px-3 py-2 rounded bg-red-600 text-white text-xs font-bold hover:bg-red-700 disabled:opacity-60"
>
{deleting ? "Deleting..." : "Yes, delete"}
</button>
</div>
</div>
) : (
/* Display row */
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-fg">{t.name}</p>
{t.description ? (
<p className="text-xs text-muted mt-0.5">{t.description}</p>
) : null}
</div>
<div className="flex items-center gap-1.5 shrink-0">
<button
onClick={() => startEdit(t)}
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"
>
Rename
</button>
<button
onClick={() => setDeleteId(t.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"
>
Delete
</button>
</div>
</div>
)}
</li>
))}
</ul>
)}
</div>
{/* Footer */}
<div className="px-6 pb-6 pt-4 border-t border-border flex-shrink-0">
<button
onClick={onClose}
className="w-full px-4 py-2.5 rounded-lg border border-border bg-surface-warm text-sm font-semibold text-fg hover:bg-surface transition-colors"
>
Done
</button>
</div>
</div>
</div>
);
}