diff --git a/src/components/admin/admin-panel.tsx b/src/components/admin/admin-panel.tsx index a577580..323f8e6 100644 --- a/src/components/admin/admin-panel.tsx +++ b/src/components/admin/admin-panel.tsx @@ -35,6 +35,11 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { const [actionLoading, setActionLoading] = useState(null); const [rejectModal, setRejectModal] = useState(null); const [rejectNotes, setRejectNotes] = useState(""); + // Approve is the highest-consequence request action — it provisions + // real infrastructure and triggers the billable setup fee — so it now + // goes through a confirmation modal like reject/delete, instead of + // firing on a single click. + const [approveModal, setApproveModal] = useState(null); // Tenants state const [tenants, setTenants] = useState(initialTenants); @@ -47,6 +52,11 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { // Shared const [error, setError] = useState(""); + // Action-scoped error — shown inside the active confirmation modal so + // a failed approve/reject/delete surfaces next to the action that + // caused it (and keeps the modal open), rather than as a detached + // panel-level banner that isn't tied to any row. + const [actionError, setActionError] = useState(""); // ─── Requests fetching ─── const fetchRequests = useCallback(async () => { @@ -125,18 +135,21 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { // ─── Request actions ─── const handleApprove = async (id: string) => { setActionLoading(id); - setError(""); + setActionError(""); try { const res = await fetch(`/api/admin/requests/${id}/approve`, { method: "POST", }); if (!res.ok) { - const data = await res.json(); + const data = await res.json().catch(() => ({})); throw new Error(data.error || "Approve failed"); } + setApproveModal(null); await fetchRequests(); } catch (e: any) { - setError(e.message); + // Keep the modal open so the admin sees why provisioning didn't + // start; the error renders inside the dialog next to the action. + setActionError(e.message); } finally { setActionLoading(null); } @@ -144,7 +157,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { const handleReject = async (id: string) => { setActionLoading(id); - setError(""); + setActionError(""); try { const res = await fetch(`/api/admin/requests/${id}/reject`, { method: "POST", @@ -152,14 +165,14 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { body: JSON.stringify({ adminNotes: rejectNotes || undefined }), }); if (!res.ok) { - const data = await res.json(); + const data = await res.json().catch(() => ({})); throw new Error(data.error || "Reject failed"); } setRejectModal(null); setRejectNotes(""); await fetchRequests(); } catch (e: any) { - setError(e.message); + setActionError(e.message); } finally { setActionLoading(null); } @@ -189,7 +202,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { const handleDelete = async (name: string) => { setActionLoading(name); - setError(""); + setActionError(""); try { const res = await fetch(`/api/admin/tenants/${name}/delete`, { method: "POST", @@ -216,7 +229,7 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { fetchTenants(); setTimeout(() => fetchTenants(), 1500); } catch (e: any) { - setError(e.message); + setActionError(e.message); } finally { setActionLoading(null); } @@ -436,16 +449,20 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { {req.status === "pending" && ( <> + + + + + ); + })()} + {/* ───── REJECT MODAL ───── */} {rejectModal && (
@@ -789,6 +860,11 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { rows={3} className="w-full px-3 py-2 bg-surface-2 border border-border rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors resize-none mb-4" /> + {actionError && ( +

+ {actionError} +

+ )}