import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getSessionUser } from "@/lib/session"; import { listPendingSkillActivationRequests, getOrgBilling } from "@/lib/db"; import { getPackageDef } from "@/lib/packages"; import { BackLink } from "@/components/ui/back-link"; import { PendingSkillRequests } from "@/components/admin/skills/pending-skill-requests"; /** * /admin/skills/pending — admin queue for manual-setup skill * activation requests. Each row shows tenant, skill, requester * info, and offers Approve / Reject actions. * * Server-renders the initial list. Approval/rejection trigger a * client-side fetch + router.refresh() so the row disappears and * the count updates without a hard reload. */ export default async function AdminPendingSkillRequestsPage() { const user = await getSessionUser(); if (!user) redirect("/login"); if (!user.isPlatform) redirect("/dashboard"); const t = await getTranslations("adminSkills"); const pending = await listPendingSkillActivationRequests(); // Hydrate display fields: skill name from catalog, org company name // from billing. Skill name fallback to skillId for off-catalog // entries (shouldn't happen but defensive). Company name is // looked up lazily per row; dedup'd via a Map so we don't issue // duplicate getOrgBilling calls for the same org. const seenOrg = new Map(); const rows = await Promise.all( pending.map(async (r) => { if (!seenOrg.has(r.zitadelOrgId)) { const billing = await getOrgBilling(r.zitadelOrgId).catch(() => null); seenOrg.set(r.zitadelOrgId, billing?.companyName ?? null); } const def = getPackageDef(r.skillId); return { ...r, skillName: def?.name ?? r.skillId, companyName: seenOrg.get(r.zitadelOrgId) ?? null, }; }) ); return (

{t("title")}

{t("subtitle")}

); }