SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.4 KB · 95 lines tsx
Raw Blame History
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