# Document register

> The organization's controlled documents in a data grid with their number, status, effective version and next review date, flagged when the review is due.

- Block: [drive](https://bettersupabase.com/blocks/drive.md)
- Kind: component
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/document-register

## Install the block

```bash
npx better-supabase add drive
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add alert badge button empty skeleton @reui/data-grid @reui/frame
```

From `apps/examples/nextjs/src/features/drive/components/document-register.tsx` (https://github.com/ScaleDockHQ/better-supabase/blob/main/apps/examples/nextjs/src/features/drive/components/document-register.tsx):

```tsx
"use client";

import type { CellContext, ColumnDef } from "@tanstack/react-table";
import type { DriveDocumentStatus } from "better-supabase/blocks/drive";

import { useTable } from "@tanstack/react-table";
import { FileCheckIcon, TriangleAlertIcon } from "lucide-react";
import { useExtracted, useFormatter } from "next-intl";
import { useRouter } from "next/navigation";
import { useTransition } from "react";

import type { DataGridFeatures } from "@/components/reui/data-grid/data-grid";

import {
  Alert,
  AlertAction,
  AlertDescription,
  AlertTitle,
} from "@/components/reui/alert";
import { Badge } from "@/components/reui/badge";
import {
  DataGrid,
  DataGridContainer,
  dataGridFeatures,
} from "@/components/reui/data-grid/data-grid";
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area";
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table";
import {
  Frame,
  FrameDescription,
  FrameHeader,
  FramePanel,
  FrameTitle,
} from "@/components/reui/frame";
import { Button } from "@/components/ui/button";
import {
  Empty,
  EmptyContent,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/components/ui/empty";
import { Skeleton } from "@/components/ui/skeleton";

import type { DocumentRegisterView, DocumentRow } from "../drive-queries";

type DocumentCell = CellContext<DataGridFeatures, DocumentRow>;

function NumberCell({ row }: DocumentCell) {
  return (
    <span className="font-mono text-xs text-muted-foreground">
      {row.original.number}
    </span>
  );
}

function NameCell({ row }: DocumentCell) {
  return <span className="font-medium">{row.original.name}</span>;
}

function StatusCell({ row }: DocumentCell) {
  const t = useExtracted("drive");
  const status: DriveDocumentStatus = row.original.status;

  switch (status) {
    case "draft": {
      return <Badge variant="secondary">{t("Draft")}</Badge>;
    }

    case "in_review": {
      return <Badge variant="info-light">{t("In review")}</Badge>;
    }

    case "effective": {
      return <Badge variant="success-light">{t("Effective")}</Badge>;
    }

    case "obsolete": {
      return <Badge variant="secondary">{t("Obsolete")}</Badge>;
    }

    default: {
      const unknown: never = status;

      return unknown;
    }
  }
}

function VersionCell({ row }: DocumentCell) {
  const t = useExtracted("drive");
  const { effectiveVersion } = row.original;

  return effectiveVersion === null ? (
    <span className="text-muted-foreground">–</span>
  ) : (
    <span className="tabular-nums">
      {t("v{version, number}", { version: effectiveVersion })}
    </span>
  );
}

function ReviewCell({ row }: DocumentCell) {
  const t = useExtracted("drive");
  const format = useFormatter();
  const { nextReviewOn, reviewDue } = row.original;

  if (nextReviewOn === null) {
    return <span className="text-muted-foreground">–</span>;
  }

  return (
    <span className="flex items-center gap-2">
      <span className="tabular-nums">
        {format.dateTime(new Date(nextReviewOn), {
          dateStyle: "medium",
          timeZone: "UTC",
        })}
      </span>
      {reviewDue ? (
        <Badge variant="warning-light">{t("Review due")}</Badge>
      ) : null}
    </span>
  );
}

function DocumentGrid({ documents }: { documents: readonly DocumentRow[] }) {
  const t = useExtracted("drive");

  const columns: ColumnDef<DataGridFeatures, DocumentRow>[] = [
    { accessorKey: "number", header: t("Number"), cell: NumberCell, size: 110 },
    { accessorKey: "name", header: t("Document"), cell: NameCell, size: 260 },
    { accessorKey: "type", header: t("Type"), size: 110 },
    { accessorKey: "status", header: t("Status"), cell: StatusCell, size: 120 },
    {
      accessorKey: "effectiveVersion",
      header: t("Effective"),
      cell: VersionCell,
      size: 100,
    },
    {
      accessorKey: "nextReviewOn",
      header: t("Next review"),
      cell: ReviewCell,
      size: 200,
    },
  ];

  const table = useTable({
    features: dataGridFeatures,
    columns,
    data: [...documents],
    getRowId: (row) => row.id,
  });

  return (
    <DataGrid
      table={table}
      recordCount={documents.length}
      tableLayout={{ dense: true }}
    >
      <DataGridContainer>
        <DataGridScrollArea>
          <DataGridTable />
        </DataGridScrollArea>
      </DataGridContainer>
    </DataGrid>
  );
}

/**
 * The organization's controlled documents: each with its number, status, the
 * released version readers get and the date of its next review.
 */
export function DocumentRegister({ state }: { state: DocumentRegisterView }) {
  const t = useExtracted("drive");
  const router = useRouter();
  const [retrying, startRetry] = useTransition();

  if (state.kind === "no-organization") {
    return null;
  }

  return (
    <Frame spacing="sm" data-recipe="document-register">
      <FrameHeader className="gap-0.5">
        <FrameTitle>{t("Document register")}</FrameTitle>
        <FrameDescription>
          {t(
            "Files under document control. Readers get the effective version; a revision replaces it once its release is approved.",
          )}
        </FrameDescription>
      </FrameHeader>
      <FramePanel>
        {state.kind === "error" ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The document register didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <Button
                size="sm"
                variant="outline"
                type="button"
                disabled={retrying}
                onClick={() => {
                  startRetry(() => {
                    router.refresh();
                  });
                }}
              >
                {t("Retry")}
              </Button>
            </AlertAction>
          </Alert>
        ) : state.documents.length === 0 ? (
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FileCheckIcon />
              </EmptyMedia>
              <EmptyTitle>{t("No controlled documents yet")}</EmptyTitle>
              <EmptyDescription>
                {t(
                  "Put a file under document control to give it a number, an owner and a review date.",
                )}
              </EmptyDescription>
            </EmptyHeader>
            <EmptyContent>
              <a
                className="text-sm underline underline-offset-4"
                href="https://bettersupabase.com/docs/blocks/drive-documents"
              >
                {t("Read the controlled documents guide")}
              </a>
            </EmptyContent>
          </Empty>
        ) : (
          <DocumentGrid documents={state.documents} />
        )}
      </FramePanel>
    </Frame>
  );
}

export function DocumentRegisterSkeleton() {
  return (
    <Frame spacing="sm" aria-busy="true">
      <FrameHeader className="gap-1">
        <Skeleton className="h-4 w-36" />
        <Skeleton className="h-4 w-80" />
      </FrameHeader>
      <FramePanel>
        <Skeleton className="h-48" />
      </FramePanel>
    </Frame>
  );
}
```

## Prompt

Paste this into a coding agent to build the recipe in your app.

### Web

```text
Build "Document register" in a React web app that uses shadcn/ui components. The organization's controlled documents in a data grid with their number, status, effective version and next review date, flagged when the review is due.

It uses the better-supabase `drive` block (Drive). It loads its data on the server through the block's client.

Steps:

1. If the app doesn't have the block yet, add the SQL with `npx better-supabase add drive` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add alert badge button empty skeleton @reui/data-grid @reui/frame`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the component. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/drive/components/document-register.tsx`). Adapt it to the app's existing components, styling and data loading instead of copying it as is.
5. Check the result against the recipe and the block's docs: it typechecks, it reads the same block data, and it handles the loading and empty states the snippet handles.

Read the full code first:

- Recipe: https://bettersupabase.com/recipes/document-register.md
- Block docs: https://bettersupabase.com/docs/blocks/drive
```
