WORKFLOW-APP/components/RegistrationDocuments.tsx

188 lines
6.7 KiB
TypeScript
Raw Permalink Normal View History

// components/RegistrationDocuments.tsx
// Client-side panel for managing a registration's `documents` field:
// - Lists current files with download links
// - Provides an upload form for the 5 post-wizard document types:
// Cor9.1 (name reserved letter), Cor9.4, Director Acceptance, Resolution,
// CIPC Registration, MOI
//
// All uploads go to /api/register/[id]/upload, which renames the file
// server-side per the type and appends it to the reg's `documents` array.
"use client";
import { useEffect, useState } from "react";
type DocType =
| "cipc_reservation"
| "cipc_approval"
| "director_acceptance"
| "resolution"
| "cipc_registration"
| "moi";
const TYPE_LABELS: Record<DocType, string> = {
cipc_reservation: "Cor9.1 — Name Reservation letter",
cipc_approval: "Cor9.4 — Name Approval letter",
director_acceptance: "Director Acceptance letter",
resolution: "Resolution to start company",
cipc_registration: "CIPC Registration document",
moi: "MOI from CIPC",
};
const TYPE_PREFIX: Record<DocType, string> = {
cipc_reservation: "Cor9.1",
cipc_approval: "Cor9.4",
director_acceptance: "Director Acceptance",
resolution: "Resolution",
cipc_registration: "CIPC Registration",
moi: "MOI",
};
interface PBFile {
name: string;
originalName?: string;
}
function extractFiles(documents: unknown): PBFile[] {
if (!documents) return [];
if (Array.isArray(documents)) {
return documents
.map((d) => {
if (typeof d === "string") return { name: d };
if (d && typeof d === "object" && "name" in (d as Record<string, unknown>)) {
return { name: String((d as Record<string, unknown>).name) };
}
return null;
})
.filter((x): x is PBFile => !!x);
}
return [];
}
export default function RegistrationDocuments({
registrationId,
initialDocuments,
cipcReservedName,
}: {
registrationId: string;
initialDocuments: unknown;
cipcReservedName?: string;
}) {
const [docs, setDocs] = useState<PBFile[]>(extractFiles(initialDocuments));
const [type, setType] = useState<DocType>("cipc_reservation");
const [file, setFile] = useState<File | null>(null);
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<{ kind: "ok" | "err"; text: string } | null>(null);
// Refresh the file list every time the component remounts
useEffect(() => {
setDocs(extractFiles(initialDocuments));
}, [initialDocuments]);
async function handleUpload(e: React.FormEvent) {
e.preventDefault();
if (!file) {
setMsg({ kind: "err", text: "Pick a file first." });
return;
}
setBusy(true);
setMsg(null);
try {
const fd = new FormData();
fd.append("type", type);
if (cipcReservedName) fd.append("reserved_name", cipcReservedName);
fd.append("files", file);
const res = await fetch(`/api/register/${registrationId}/upload`, {
method: "POST",
body: fd,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
// Append the new file to local state (server didn't return per-file names
// in a stable shape, so we just re-fetch by triggering a page refresh).
setFile(null);
setMsg({ kind: "ok", text: `Uploaded ${data.filesUploaded || 1} file(s). Reloading…` });
setTimeout(() => window.location.reload(), 800);
} catch (err: unknown) {
setMsg({ kind: "err", text: err instanceof Error ? err.message : "Upload failed" });
} finally {
setBusy(false);
}
}
return (
<div className="bg-surface border border-border rounded-lg p-5 space-y-4">
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-bold mb-2">Documents</p>
{docs.length === 0 ? (
<p className="text-xs text-muted italic">No documents uploaded yet.</p>
) : (
<ul className="space-y-1.5">
{docs.map((d, i) => (
<li key={i} className="flex items-center justify-between gap-3 text-sm">
<span className="font-mono text-fg truncate">{d.name}</span>
<a
href={`/api/register/${registrationId}/file?name=${encodeURIComponent(d.name)}`}
target="_blank"
rel="noreferrer"
className="text-xs text-accent hover:underline"
>
Download
</a>
</li>
))}
</ul>
)}
</div>
<form onSubmit={handleUpload} className="border-t border-border pt-4 space-y-3">
<p className="text-xs uppercase tracking-[0.08em] text-muted font-bold">Upload document</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="text-xs font-semibold text-muted mb-1 block">Type</label>
<select
value={type}
onChange={(e) => setType(e.target.value as DocType)}
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg focus:outline-none focus:border-accent"
>
{(Object.keys(TYPE_LABELS) as DocType[]).map((k) => (
<option key={k} value={k}>
{TYPE_LABELS[k]} → {TYPE_PREFIX[k]} - {cipcReservedName || "..."}
</option>
))}
</select>
{cipcReservedName && (
<p className="text-[10px] text-muted mt-1 font-mono">
Will save as: {TYPE_PREFIX[type]} - {cipcReservedName}.&lt;ext&gt;
</p>
)}
</div>
<div>
<label className="text-xs font-semibold text-muted mb-1 block">File (PDF / JPG / PNG, ≤ 10 MB)</label>
<input
type="file"
accept="application/pdf,image/jpeg,image/png"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="block w-full text-sm text-fg file:mr-3 file:py-1.5 file:px-3 file:rounded file:border file:border-border file:bg-surface-warm file:text-fg file:text-xs file:font-semibold file:cursor-pointer hover:file:bg-accent/10"
/>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="submit"
disabled={busy || !file}
className="px-4 py-2 rounded-md border border-accent bg-accent text-accent-on text-sm font-bold disabled:opacity-40 hover:opacity-90"
>
{busy ? "Uploading…" : "Upload"}
</button>
{msg && (
<span className={msg.kind === "ok" ? "text-xs text-success" : "text-xs text-danger"}>
{msg.text}
</span>
)}
</div>
</form>
</div>
);
}