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

187 lines
8.2 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Mail, UserCog, Clock, Plus } from "lucide-react";
import { api } from "@/lib/api";
const LIFECYCLE = ["active", "grace", "archived", "deleted"];
export default function OrgDetail() {
const { org_id } = useParams<{ org_id: string }>();
const [d, setD] = useState<any>(null);
const [hist, setHist] = useState<any>(null);
const [notes, setNotes] = useState<any[]>([]);
const [note, setNote] = useState("");
const [toast, setToast] = useState("");
function load() {
api(`/admin/organizations/${org_id}`).then(setD).catch(() => {});
api(`/admin/organizations/${org_id}/history`).then(setHist).catch(() => {});
api(`/admin/organizations/${org_id}/notes`).then(setNotes).catch(() => setNotes([]));
}
useEffect(() => { load(); }, [org_id]);
async function addNote() {
if (!note.trim()) return;
await api(`/admin/organizations/${org_id}/notes`, { method: "POST", body: JSON.stringify({ note }) });
setNote(""); load();
}
async function sendReminder() {
const r = await api(`/admin/organizations/${org_id}/send-reminder`, { method: "POST" });
setToast(`Reminder sent to ${r.to}`);
setTimeout(() => setToast(""), 2500);
}
async function extend() {
await api(`/admin/organizations/${org_id}/subscription`, { method: "PUT", body: JSON.stringify({ extend_days: 30 }) });
setToast("Subscription extended 30 days"); load();
setTimeout(() => setToast(""), 2500);
}
async function impersonate() {
const r = await api(`/admin/organizations/${org_id}/impersonate`, { method: "POST" });
setToast(r.message);
setTimeout(() => setToast(""), 2500);
}
if (!d) return <div className="text-slate-400">Loading</div>;
const curStage = LIFECYCLE.indexOf(d.subscription_status);
return (
<div className="space-y-5">
{toast && <div className="card p-3 bg-emerald-50 border-emerald-200 text-emerald-800 text-sm">{toast}</div>}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">{d.name}</h2>
<p className="text-sm text-slate-500 capitalize">{d.plan} · {d.subscription_status}</p>
</div>
<div className="flex gap-2">
<button onClick={extend} className="flex items-center gap-2 border border-slate-200 px-3 py-1.5 rounded-lg text-sm font-medium"><Clock className="h-4 w-4" /> Extend 30d</button>
<button onClick={sendReminder} className="flex items-center gap-2 border border-slate-200 px-3 py-1.5 rounded-lg text-sm font-medium"><Mail className="h-4 w-4" /> Send reminder</button>
<button onClick={impersonate} className="flex items-center gap-2 bg-brand-600 text-white px-3 py-1.5 rounded-lg text-sm font-semibold"><UserCog className="h-4 w-4" /> Impersonate</button>
</div>
</div>
{/* Lifecycle timeline */}
<div className="card p-5">
<h3 className="font-bold mb-4">Subscription Lifecycle</h3>
<div className="flex items-center">
{LIFECYCLE.map((stage, i) => (
<div key={stage} className="flex items-center flex-1 last:flex-none">
<div className="flex flex-col items-center">
<div className={`h-9 w-9 rounded-full grid place-items-center text-xs font-bold capitalize
${i <= curStage ? "bg-brand-600 text-white" : "bg-slate-100 text-slate-400"}`}>
{i + 1}
</div>
<span className="text-xs mt-1 capitalize text-slate-500">{stage}</span>
</div>
{i < LIFECYCLE.length - 1 && (
<div className={`flex-1 h-0.5 mx-2 ${i < curStage ? "bg-brand-600" : "bg-slate-200"}`} />
)}
</div>
))}
</div>
</div>
{/* Utilization */}
<div className="grid sm:grid-cols-3 gap-4">
<UtilBar label="Products" used={d.products_used} limit={d.product_limit} />
<UtilBar label="Users" used={d.users_used} limit={d.user_limit} />
<UtilBar label="Storage (GB)" used={d.storage_used_gb} limit={d.storage_total_gb} />
</div>
<div className="grid sm:grid-cols-3 gap-4">
<Stat label="QR Codes" value={d.qr_codes} />
<Stat label="Total Scans" value={d.total_scans} />
<Stat label="Plan Price" value={`${d.plan_price}`} />
</div>
{/* Admins */}
<div className="card p-5">
<h3 className="font-bold mb-3">Admin Users</h3>
<div className="space-y-2 text-sm">
{(d.admins ?? []).map((a: any, i: number) => (
<div key={i} className="flex justify-between border-b border-slate-100 py-2">
<span>{a.name} <span className="text-slate-400">({a.email})</span></span>
<span className="text-slate-500">{a.last_login ? new Date(a.last_login).toLocaleDateString() : "never"}</span>
</div>
))}
{(!d.admins || d.admins.length === 0) && <p className="text-slate-400">No admins.</p>}
</div>
</div>
{/* History */}
{hist && (
<div className="grid md:grid-cols-2 gap-4">
<div className="card p-5">
<h3 className="font-bold mb-3">Invoices</h3>
<div className="space-y-1 text-sm">
{hist.invoices.map((i: any, k: number) => (
<div key={k} className="flex justify-between border-b border-slate-100 py-1.5">
<span className="font-mono">{i.invoice_number}</span>
<span>{i.total} · <span className="capitalize text-slate-500">{i.status}</span></span>
</div>
))}
{hist.invoices.length === 0 && <p className="text-slate-400">No invoices.</p>}
</div>
</div>
<div className="card p-5">
<h3 className="font-bold mb-3">Plan History</h3>
<div className="space-y-1 text-sm">
{hist.plan_history.map((h: any, k: number) => (
<div key={k} className="flex justify-between border-b border-slate-100 py-1.5">
<span>{h.from ?? "—"} {h.to}</span>
<span className="text-slate-400">{new Date(h.at).toLocaleDateString()}</span>
</div>
))}
{hist.plan_history.length === 0 && <p className="text-slate-400">No changes.</p>}
</div>
</div>
</div>
)}
{/* Support notes */}
<div className="card p-5">
<h3 className="font-bold mb-3">Internal Support Notes</h3>
<div className="flex gap-2 mb-3">
<input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Add an internal note…"
className="flex-1 px-3 py-2 rounded-lg border border-slate-200 text-sm" />
<button onClick={addNote} className="flex items-center gap-1 bg-brand-600 text-white px-3 py-2 rounded-lg text-sm font-semibold">
<Plus className="h-4 w-4" /> Add
</button>
</div>
<div className="space-y-2">
{notes.map((n) => (
<div key={n.id} className="text-sm border-b border-slate-100 py-2">
<div>{n.note}</div>
<div className="text-xs text-slate-400">{n.author} · {new Date(n.at).toLocaleString()}</div>
</div>
))}
{notes.length === 0 && <p className="text-sm text-slate-400">No notes yet.</p>}
</div>
</div>
</div>
);
}
function UtilBar({ label, used, limit }: { label: string; used: number; limit: number | null }) {
const pct = limit == null ? 8 : Math.min(100, Math.round((used / limit) * 100));
return (
<div className="card p-4">
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-600">{label}</span>
<span className="text-slate-500">{used} / {limit ?? "∞"}</span>
</div>
<div className="h-2 rounded-full bg-slate-100">
<div className="h-full rounded-full bg-brand-500" style={{ width: `${pct}%` }} />
</div>
</div>
);
}
function Stat({ label, value }: any) {
return (
<div className="card p-4">
<div className="text-sm text-slate-500">{label}</div>
<div className="text-2xl font-extrabold mt-1">{value}</div>
</div>
);
}