Redesign registrations list: client-side table with copy/share links, sorted newest first
This commit is contained in:
parent
45e017c7c3
commit
68ce2bd586
2 changed files with 112 additions and 36 deletions
|
|
@ -18,12 +18,15 @@ export async function GET(req: NextRequest) {
|
|||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = searchParams.get("q") || "";
|
||||
const fields = searchParams.get("fields") || "id,reference_name,status,proposed_name_1";
|
||||
const fields = searchParams.get("fields") || "id,reference_name,status,proposed_name_1,trading_address,created";
|
||||
const perPage = Math.min(Number(searchParams.get("perPage")) || 500, 500);
|
||||
const sort = searchParams.get("sort") || "-created";
|
||||
|
||||
const data = await fetchCollection<RegItem>("ysterbaard_company_registrations", {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
perPage,
|
||||
fields,
|
||||
sort,
|
||||
});
|
||||
|
||||
let items = data.items;
|
||||
|
|
@ -38,7 +41,7 @@ export async function GET(req: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
// Default sort: newest first (nanoid is time-sortable)
|
||||
items.sort((a, b) => b.id.localeCompare(a.id));
|
||||
|
||||
// Return lightweight response
|
||||
|
|
|
|||
|
|
@ -1,38 +1,84 @@
|
|||
// app/registrations/page.tsx
|
||||
// Server component. Lists submitted company registrations.
|
||||
// Client component. Lists submitted company registrations.
|
||||
// Uses /api/register/search for data (client-safe).
|
||||
|
||||
import { fetchCollection } from "@/lib/pb";
|
||||
import type { PBList, PBRecord } from "@/lib/pb";
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface RegItem extends PBRecord {
|
||||
interface RegItem {
|
||||
id: string;
|
||||
reference_name: string;
|
||||
status: string;
|
||||
proposed_name_1: string;
|
||||
trading_address: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export default async function RegistrationsPage() {
|
||||
const data = await fetchCollection<RegItem>("ysterbaard_company_registrations", {
|
||||
page: 1,
|
||||
perPage: 200,
|
||||
fields: "id,reference_name,status,proposed_name_1,trading_address",
|
||||
function formatDate(iso: string | null): string {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleDateString("en-ZA", {
|
||||
day: "2-digit", month: "short", year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
const items = data.items.sort((a, b) => {
|
||||
// Use registration ID as proxy for recency (nanoid is time-sortable)
|
||||
return b.id.localeCompare(a.id);
|
||||
});
|
||||
const statusLabel: Record<string, string> = {
|
||||
pending: "Pending",
|
||||
submitted: "Submitted",
|
||||
approved: "Approved",
|
||||
rejected: "Rejected",
|
||||
draft: "Draft",
|
||||
};
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
pending: "Pending",
|
||||
submitted: "Submitted",
|
||||
approved: "Approved",
|
||||
rejected: "Rejected",
|
||||
const statusStyle: Record<string, string> = {
|
||||
pending: "bg-surface border border-border text-muted",
|
||||
draft: "bg-surface border border-border text-muted",
|
||||
submitted: "bg-accent/10 text-accent border border-accent/20",
|
||||
approved: "bg-success/10 text-success border border-success/20",
|
||||
rejected: "bg-danger/10 text-danger border border-danger/20",
|
||||
};
|
||||
|
||||
export default function RegistrationsPage() {
|
||||
const [items, setItems] = useState<RegItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/register/search?q=&fields=id,reference_name,status,proposed_name_1,trading_address,created&sort=-created")
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
setItems(d.items || []);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleCopyLink = (id: string) => {
|
||||
const url = `${window.location.origin}/register/${id}`;
|
||||
const el = document.createElement("textarea");
|
||||
el.value = url;
|
||||
el.style.position = "fixed";
|
||||
el.style.opacity = "0";
|
||||
document.body.appendChild(el);
|
||||
el.focus();
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(el);
|
||||
setCopiedId(id);
|
||||
setTimeout(() => setCopiedId(null), 2000);
|
||||
};
|
||||
|
||||
const canShare = typeof navigator !== "undefined" && !!navigator.share;
|
||||
|
||||
const handleShare = (item: RegItem) => {
|
||||
const url = `${window.location.origin}/register/${item.id}`;
|
||||
navigator.share({
|
||||
title: "Company Registration",
|
||||
text: `Registration: ${item.reference_name || item.id}`,
|
||||
url,
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -43,7 +89,9 @@ export default async function RegistrationsPage() {
|
|||
subtitle="Client submissions from the registration wizard."
|
||||
/>
|
||||
<div className="container-x py-10">
|
||||
{items.length === 0 ? (
|
||||
{loading ? (
|
||||
<div className="text-center py-20 text-muted">Loading...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="bg-surface-warm border border-border rounded-lg p-12 text-center">
|
||||
<p className="text-muted">No registrations yet.</p>
|
||||
<p className="text-sm text-muted mt-1">
|
||||
|
|
@ -56,20 +104,15 @@ export default async function RegistrationsPage() {
|
|||
<thead>
|
||||
<tr className="border-b border-border bg-surface-warm text-left">
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider">Reference</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider">Company name</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider">Address</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider hidden sm:table-cell">Company</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider hidden md:table-cell">Date</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider">Status</th>
|
||||
<th className="px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wider text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{items.map((r) => {
|
||||
const status = r.status || "pending";
|
||||
const statusStyle: Record<string, string> = {
|
||||
pending: "bg-surface border border-border text-muted",
|
||||
submitted: "bg-accent/10 text-accent border border-accent/20",
|
||||
approved: "bg-success/10 text-success border border-success/20",
|
||||
rejected: "bg-danger/10 text-danger border border-danger/20",
|
||||
};
|
||||
const status = r.status || "draft";
|
||||
return (
|
||||
<tr key={r.id} className="hover:bg-surface-warm transition-colors">
|
||||
<td className="px-5 py-4">
|
||||
|
|
@ -80,13 +123,43 @@ export default async function RegistrationsPage() {
|
|||
{r.reference_name || r.id}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-muted">{r.proposed_name_1 || "—"}</td>
|
||||
<td className="px-5 py-4 text-muted max-w-xs truncate">{r.trading_address || "—"}</td>
|
||||
<td className="px-5 py-4 text-muted hidden sm:table-cell max-w-xs truncate">
|
||||
{r.proposed_name_1 || "—"}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-muted hidden md:table-cell text-xs tabular-nums">
|
||||
{formatDate(r.created)}
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<span className={`inline-flex px-2 py-0.5 rounded text-xs font-semibold ${statusStyle[status] || statusStyle.pending}`}>
|
||||
<span className={`inline-flex px-2 py-0.5 rounded text-xs font-semibold ${statusStyle[status] || statusStyle.draft}`}>
|
||||
{statusLabel[status] || status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleCopyLink(r.id)}
|
||||
className="px-3 py-1.5 bg-surface border border-border rounded text-xs font-semibold text-fg hover:border-accent hover:text-accent transition-colors whitespace-nowrap"
|
||||
title="Copy client wizard link"
|
||||
>
|
||||
{copiedId === r.id ? "✓ Copied" : "Copy link"}
|
||||
</button>
|
||||
{canShare && (
|
||||
<button
|
||||
onClick={() => handleShare(r)}
|
||||
className="px-3 py-1.5 bg-surface border border-border rounded text-xs font-semibold text-fg hover:border-accent hover:text-accent transition-colors"
|
||||
title="Share link"
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href={`/registrations/${r.id}`}
|
||||
className="px-3 py-1.5 bg-accent text-accent-on rounded text-xs font-semibold hover:opacity-90 no-underline transition-opacity"
|
||||
>
|
||||
View →
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in a new issue