Uncommitted work in progress at the point of starting the Kanban project. Captures: - Registration wizard refactor + multi-step routing - structured physical/postal address fields on companies and directors - appointments flow (sign + verify) - document type manager + upload/panel for companies and directors - status actions, approve button, action/dropdown buttons - step2 review page tsconfig.tsbuildinfo is now ignored (build artefact).
240 lines
9 KiB
TypeScript
240 lines
9 KiB
TypeScript
"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>
|
||
);
|
||
}
|