TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { Building2 } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { listOrganizations, pageNum, str, PLAN_OPTIONS } from "@/lib/queries/admin";5import { formatNumber, formatUsd } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Badge, StatusBadge } from "@/components/ui/badge";8import { EmptyState } from "@/components/ui/empty-state";9import { NativeSelect } from "@/components/ui/input";10import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";11import { DateCell, Pagination, Panel, PlanBadge, SearchForm, numCell } from "@/components/admin/primitives";1213export const dynamic = "force-dynamic";1415export default async function AdminOrganizationsPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {16 await requireAdmin();17 const sp = await searchParams;18 const q = str(sp.q);19 const plan = str(sp.plan);20 const page = pageNum(sp.page);21 const data = await listOrganizations({ q: q || undefined, plan: plan || undefined, page });2223 return (24 <>25 <PageHeader26 eyebrow="Customers"27 title="Organizations"28 description={`${formatNumber(data.total)} organizations. Requests and spend are for the last 30 days.`}29 actions={30 <SearchForm action="/admin/organizations" q={q} placeholder="name, slug, owner email or id">31 <NativeSelect name="plan" defaultValue={plan} className="h-8 w-[150px] text-[13px]" aria-label="Plan">32 <option value="">All plans</option>33 {PLAN_OPTIONS.map((p) => (34 <option key={p.value} value={p.value}>35 {p.label}36 </option>37 ))}38 </NativeSelect>39 </SearchForm>40 }41 />42 <Panel flush>43 {data.total === 0 ? (44 <div className="p-4">45 <EmptyState icon={Building2} title="No organizations match" description="Every signup creates a personal organization automatically." compact />46 </div>47 ) : (48 <>49 <Table>50 <TableHeader>51 <TableRow>52 <TableHead>Name</TableHead>53 <TableHead>Plan</TableHead>54 <TableHead>Owner</TableHead>55 <TableHead className="text-right">Projects</TableHead>56 <TableHead className="text-right">Requests 30d</TableHead>57 <TableHead className="text-right">Spend 30d</TableHead>58 <TableHead>Status</TableHead>59 <TableHead>Provider vis.</TableHead>60 <TableHead className="text-right">Created</TableHead>61 </TableRow>62 </TableHeader>63 <TableBody>64 {data.rows.map((o) => (65 <TableRow key={o.id}>66 <TableCell>67 <Link href={`/admin/organizations/${o.id}`} className="font-medium hover:underline">68 {o.name}69 </Link>70 <div className="font-mono text-[11.5px] text-fg-subtle">{o.slug}</div>71 </TableCell>72 <TableCell>73 <PlanBadge plan={o.plan} />74 </TableCell>75 <TableCell className="max-w-[220px] truncate text-fg-muted">{o.ownerEmail ?? "—"}</TableCell>76 <TableCell className={numCell}>{o.projects}</TableCell>77 <TableCell className={numCell}>{formatNumber(o.requests30d)}</TableCell>78 <TableCell className={numCell}>{formatUsd(o.spend30d)}</TableCell>79 <TableCell>{o.suspended ? <StatusBadge status="suspended" /> : <StatusBadge status="active" />}</TableCell>80 <TableCell>{o.providerVisibility ? <Badge variant="warning" dot>debug on</Badge> : <span className="text-fg-subtle">off</span>}</TableCell>81 <TableCell className="text-right">82 <DateCell value={o.createdAt} />83 </TableCell>84 </TableRow>85 ))}86 </TableBody>87 </Table>88 <Pagination page={data.page} pages={data.pages} total={data.total} pageSize={data.pageSize} basePath="/admin/organizations" params={{ q, plan }} />89 </>90 )}91 </Panel>92 </>93 );94}95