From 9a96d74f5c1e7223bb192e471be89363ac7d71db Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 11 Apr 2026 11:54:21 +0200 Subject: [PATCH] All the initial admin requests approval flow --- src/app/[locale]/admin/page.tsx | 73 +-- .../api/admin/requests/[id]/approve/route.ts | 3 +- src/components/admin/admin-panel.tsx | 508 ++++++++++++++++++ src/messages/de.json | 27 +- src/messages/en.json | 27 +- src/messages/fr.json | 27 +- src/messages/it.json | 27 +- 7 files changed, 604 insertions(+), 88 deletions(-) create mode 100644 src/components/admin/admin-panel.tsx diff --git a/src/app/[locale]/admin/page.tsx b/src/app/[locale]/admin/page.tsx index bf75d93..f6ff294 100644 --- a/src/app/[locale]/admin/page.tsx +++ b/src/app/[locale]/admin/page.tsx @@ -2,7 +2,7 @@ import { getSessionUser } from "@/lib/session"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { listTenants } from "@/lib/k8s"; -import { StatusBadge } from "@/components/ui/status-badge"; +import { AdminPanel } from "@/components/admin/admin-panel"; export default async function AdminPage() { const user = await getSessionUser(); @@ -30,76 +30,7 @@ export default async function AdminPage() {
-
-

- {t("allTenants")} -

- - {tenants.length} - -
- - {tenants.length === 0 ? ( -
-

{t("noTenants")}

-
- ) : ( -
-
- - - - - - - - - - - - {tenants.map((tenant) => ( - - - - - - - - ))} - -
- {t("name")} - - {t("displayName")} - - {t("phase")} - - {t("packages")} - - {t("created")} -
- {tenant.metadata.name} - - {tenant.spec.displayName} - - - - {tenant.spec.packages?.join(", ") || "—"} - - {tenant.metadata.creationTimestamp - ? new Date( - tenant.metadata.creationTimestamp - ).toLocaleDateString() - : "—"} -
-
-
- )} +
); diff --git a/src/app/api/admin/requests/[id]/approve/route.ts b/src/app/api/admin/requests/[id]/approve/route.ts index ca0d68d..20a4fb1 100644 --- a/src/app/api/admin/requests/[id]/approve/route.ts +++ b/src/app/api/admin/requests/[id]/approve/route.ts @@ -6,6 +6,7 @@ import { createTenant } from "@/lib/k8s"; /** * POST /api/admin/requests/[id]/approve * Approve a tenant request: create the PiecedTenant CR and update status. + * Also supports re-approving a previously rejected request. */ export async function POST( request: Request, @@ -29,7 +30,7 @@ export async function POST( ); } - if (tenantRequest.status !== "pending") { + if (tenantRequest.status !== "pending" && tenantRequest.status !== "rejected") { return NextResponse.json( { error: `Request is already ${tenantRequest.status}` }, { status: 400 } diff --git a/src/components/admin/admin-panel.tsx b/src/components/admin/admin-panel.tsx new file mode 100644 index 0000000..5fb7de2 --- /dev/null +++ b/src/components/admin/admin-panel.tsx @@ -0,0 +1,508 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useTranslations } from "next-intl"; +import type { PiecedTenant, TenantRequest } from "@/types"; +import { StatusBadge } from "@/components/ui/status-badge"; +import Link from "next/link"; + +type Tab = "requests" | "tenants"; +type RequestFilter = "all" | "pending" | "provisioning" | "approved" | "rejected"; + +interface AdminPanelProps { + initialTenants: PiecedTenant[]; +} + +export function AdminPanel({ initialTenants }: AdminPanelProps) { + const t = useTranslations("admin"); + const [tab, setTab] = useState("requests"); + const [requests, setRequests] = useState([]); + const [filter, setFilter] = useState("all"); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + const [rejectModal, setRejectModal] = useState(null); + const [rejectNotes, setRejectNotes] = useState(""); + const [error, setError] = useState(""); + + const fetchRequests = useCallback(async () => { + try { + const url = + filter === "all" + ? "/api/admin/requests" + : `/api/admin/requests?status=${filter}`; + const res = await fetch(url); + if (!res.ok) throw new Error("Failed to fetch"); + const data = await res.json(); + setRequests(data); + } catch (e: any) { + setError(e.message); + } finally { + setLoading(false); + } + }, [filter]); + + useEffect(() => { + if (tab === "requests") { + setLoading(true); + fetchRequests(); + } + }, [tab, filter, fetchRequests]); + + const handleApprove = async (id: string) => { + setActionLoading(id); + setError(""); + try { + const res = await fetch(`/api/admin/requests/${id}/approve`, { + method: "POST", + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Approve failed"); + } + await fetchRequests(); + } catch (e: any) { + setError(e.message); + } finally { + setActionLoading(null); + } + }; + + const handleReject = async (id: string) => { + setActionLoading(id); + setError(""); + try { + const res = await fetch(`/api/admin/requests/${id}/reject`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ adminNotes: rejectNotes || undefined }), + }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || "Reject failed"); + } + setRejectModal(null); + setRejectNotes(""); + await fetchRequests(); + } catch (e: any) { + setError(e.message); + } finally { + setActionLoading(null); + } + }; + + const FILTERS: RequestFilter[] = [ + "all", + "pending", + "provisioning", + "approved", + "rejected", + ]; + + const pendingCount = requests.filter((r) => r.status === "pending").length; + + return ( + <> + {/* Tab bar */} +
+ + +
+ + {/* Error banner */} + {error && ( +
+ {error} + +
+ )} + + {/* ───── REQUESTS TAB ───── */} + {tab === "requests" && ( + <> +
+ {FILTERS.map((f) => ( + + ))} +
+ + {loading ? ( +
+
+

{t("loadingRequests")}

+
+ ) : requests.length === 0 ? ( +
+

{t("noRequests")}

+
+ ) : ( +
+
+ + + + + + + + + + + + + + {requests.map((req) => ( + + + + + + + + + + ))} + +
+ {t("company")} + + {t("contact")} + + {t("agentName")} + + {t("packages")} + + {t("status")} + + {t("submitted")} + + {t("actions")} +
+
+ {req.companyName} +
+
+
+ {req.contactName} +
+
+ {req.contactEmail} +
+
+ {req.agentName} + + {req.packages?.length + ? req.packages.join(", ") + : "—"} + + + + {new Date(req.createdAt).toLocaleDateString()} + +
+ {req.status === "pending" && ( + <> + + + + )} + {(req.status === "provisioning" || + req.status === "approved") && + req.tenantName && ( + + {t("viewTenant")} + + )} + {req.status === "rejected" && ( + + )} +
+ {req.adminNotes && ( +

+ {req.adminNotes} +

+ )} +
+
+
+ )} + + )} + + {/* ───── TENANTS TAB ───── */} + {tab === "tenants" && ( + <> +
+ + t.status?.phase === "Running") + .length + } + color="text-emerald-400" + /> + + t.status?.phase === "Provisioning" || + t.status?.phase === "Pending" + ).length + } + color="text-amber-400" + /> + t.status?.phase === "Error").length + } + color="text-red-400" + /> +
+ + {initialTenants.length === 0 ? ( +
+

{t("noTenants")}

+
+ ) : ( +
+
+ + + + + + + + + + + + + {initialTenants.map((tenant) => ( + + + + + + + + + ))} + +
+ {t("name")} + + {t("displayName")} + + {t("phase")} + + {t("packages")} + + {t("created")} + + {t("actions")} +
+ {tenant.metadata.name} + + {tenant.spec.displayName} + + + + {tenant.spec.packages?.join(", ") || "—"} + + {tenant.metadata.creationTimestamp + ? new Date( + tenant.metadata.creationTimestamp + ).toLocaleDateString() + : "—"} + + + {t("manage")} + +
+
+
+ )} + + )} + + {/* ───── REJECT MODAL ───── */} + {rejectModal && ( +
+
+

+ {t("rejectTitle")} +

+ +