Files
verify/frontend/app/admin/organizations/page.tsx
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

119 lines
6.3 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { Download, Search, Building2, CheckCircle2, Clock, Archive } from "lucide-react";
import { api, apiBase } from "@/lib/api";
function bar(used: number, limit: number | null) {
if (limit == null) return 12;
return Math.min(100, Math.round((used / limit) * 100));
}
export default function AdminOrgs() {
const [orgs, setOrgs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [q, setQ] = useState("");
const [status, setStatus] = useState("");
const [plan, setPlan] = useState("");
useEffect(() => {
api("/admin/organizations").then(setOrgs).catch(() => {}).finally(() => setLoading(false));
}, []);
const filtered = useMemo(() => orgs.filter((o) =>
(!q || o.name.toLowerCase().includes(q.toLowerCase())) &&
(!status || o.subscription_status === status) &&
(!plan || o.plan === plan)), [orgs, q, status, plan]);
const kpis = {
total: orgs.length,
active: orgs.filter((o) => o.subscription_status === "active").length,
grace: orgs.filter((o) => o.subscription_status === "grace").length,
archived: orgs.filter((o) => o.subscription_status === "archived").length,
};
const plans = [...new Set(orgs.map((o) => o.plan).filter(Boolean))];
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">All Organizations</h2>
<p className="text-sm text-slate-500">Utilization across every client org.</p>
</div>
<div className="flex gap-2">
<a href={`${apiBase}/admin/utilization-report?format=csv`} className="flex items-center gap-2 border border-slate-200 px-3 py-1.5 rounded-lg text-sm font-medium"><Download className="h-4 w-4" /> CSV</a>
<a href={`${apiBase}/admin/utilization-report?format=pdf`} className="flex items-center gap-2 border border-slate-200 px-3 py-1.5 rounded-lg text-sm font-medium"><Download className="h-4 w-4" /> PDF</a>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Kpi label="Total Orgs" value={kpis.total} icon={<Building2 className="h-5 w-5" />} tint="bg-blue-50 text-blue-600" />
<Kpi label="Active" value={kpis.active} icon={<CheckCircle2 className="h-5 w-5" />} tint="bg-emerald-50 text-emerald-600" />
<Kpi label="Grace Period" value={kpis.grace} icon={<Clock className="h-5 w-5" />} tint="bg-amber-50 text-amber-600" />
<Kpi label="Archived" value={kpis.archived} icon={<Archive className="h-5 w-5" />} tint="bg-slate-100 text-slate-600" />
</div>
<div className="card p-4 flex flex-wrap items-end gap-3">
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search org…" className="pl-9 pr-3 py-2 w-52 rounded-lg border border-slate-200 text-sm" />
</div>
<label className="block"><span className="text-xs text-slate-500">Status</span>
<select value={status} onChange={(e) => setStatus(e.target.value)} className="mt-1 block px-3 py-2 rounded-lg border border-slate-200 text-sm min-w-[130px]">
<option value="">All Status</option>{["active", "grace", "archived", "deleted"].map((s) => <option key={s} value={s} className="capitalize">{s}</option>)}
</select>
</label>
<label className="block"><span className="text-xs text-slate-500">Plan</span>
<select value={plan} onChange={(e) => setPlan(e.target.value)} className="mt-1 block px-3 py-2 rounded-lg border border-slate-200 text-sm min-w-[130px]">
<option value="">All Plans</option>{plans.map((pl) => <option key={pl} value={pl}>{pl}</option>)}
</select>
</label>
</div>
<div className="card overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-500 text-left">
<tr>
<th className="px-4 py-3 font-medium">Organization</th>
<th className="px-4 py-3 font-medium">Plan</th>
<th className="px-4 py-3 font-medium">Products</th>
<th className="px-4 py-3 font-medium">Users</th>
<th className="px-4 py-3 font-medium">QRs</th>
<th className="px-4 py-3 font-medium">Scans</th>
<th className="px-4 py-3 font-medium">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filtered.map((o) => (
<tr key={o.org_id} className="hover:bg-slate-50">
<td className="px-4 py-3 font-medium text-brand-700"><Link href={`/admin/organizations/${o.org_id}`} className="hover:underline">{o.name}</Link></td>
<td className="px-4 py-3">{o.plan ?? "—"}</td>
<td className="px-4 py-3 w-40">
<div className="text-xs text-slate-500 mb-1">{o.products_used} / {o.product_limit ?? "∞"}</div>
<div className="h-1.5 rounded-full bg-slate-100"><div className="h-full rounded-full bg-blue-500" style={{ width: `${bar(o.products_used, o.product_limit)}%` }} /></div>
</td>
<td className="px-4 py-3">{o.users_used} / {o.user_limit ?? "∞"}</td>
<td className="px-4 py-3">{o.qr_codes}</td>
<td className="px-4 py-3">{o.total_scans}</td>
<td className="px-4 py-3"><span className="text-xs font-semibold bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded-full capitalize">{o.subscription_status}</span></td>
</tr>
))}
{!loading && filtered.length === 0 && <tr><td colSpan={7} className="px-4 py-12 text-center text-slate-400">No organizations match.</td></tr>}
</tbody>
</table>
</div>
</div>
);
}
function Kpi({ label, value, icon, tint }: any) {
return (
<div className="card p-5">
<div className="flex items-start gap-4">
<div className={`h-12 w-12 rounded-xl grid place-items-center shrink-0 ${tint}`}>{icon}</div>
<div><div className="text-sm text-slate-500">{label}</div><div className="text-2xl font-extrabold text-slate-900 mt-0.5">{value}</div></div>
</div>
</div>
);
}