# Timesheet

> A week of time entries per project with a running timer and totals.

- Block: [time-tracking](https://bettersupabase.com/blocks/time-tracking.md)
- Kind: screen
- Data: loaded on the server through the block's client
- Page: https://bettersupabase.com/recipes/timesheet

## Install the block

```bash
npx better-supabase add time-tracking
```

## Web (Next.js)

Add the shadcn components:

```bash
npx shadcn add badge button card empty skeleton table
```

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

```tsx
/**
 * Render inside `<Suspense>`: the caller's week as one row per project
 * with hours per day and totals, a running timer, and Submit once the
 * week is done. `?week=YYYY-MM-DD` picks the week.
 */
export async function Timesheet({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  const [session, t, format, query] = await Promise.all([
    getSession(),
    getExtracted("people"),
    getFormatter(),
    searchParams,
    connection(),
  ]);

  if (!can(session, "time_tracking.track")) {
    forbidden();
  }

  const monday = mondayOf(query["week"]);
  const week = monday.toString();
  const sheet = await settle(() => loadTimesheet(week));

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

  const dayLabel = (date: string) =>
    format.dateTime(new Date(`${date}T12:00:00Z`), {
      weekday: "short",
      day: "numeric",
      timeZone: "UTC",
    });

  const range = format.dateTimeRange(
    new Date(`${week}T12:00:00Z`),
    new Date(`${monday.add({ days: 6 }).toString()}T12:00:00Z`),
    { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" },
  );

  const locked =
    sheet.kind === "ok" &&
    (sheet.data.status === "submitted" || sheet.data.status === "approved");

  const logTime = locked ? null : <LogTimeDialog week={week} />;

  return (
    <Card data-recipe="timesheet">
      <CardHeader>
        <CardTitle>{t("Week of {range}", { range })}</CardTitle>
        <CardDescription>
          {t("Hours per project and day, in {zone}.", { zone: TIME_ZONE })}
        </CardDescription>
        <CardAction className="flex items-center gap-2">
          <Link
            href={`/people/time?week=${monday.subtract({ weeks: 1 }).toString()}`}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
            aria-label={t("Previous week")}
          >
            <ChevronLeftIcon />
          </Link>
          <Link
            href={`/people/time?week=${monday.add({ weeks: 1 }).toString()}`}
            className={buttonVariants({ variant: "outline", size: "icon-sm" })}
            aria-label={t("Next week")}
          >
            <ChevronRightIcon />
          </Link>
        </CardAction>
      </CardHeader>
      <CardContent className="flex flex-col gap-4">
        {sheet.kind === "error" ? (
          <PeopleLoadError title={t("The timesheet didn't load")} />
        ) : (
          <>
            <div className="flex flex-wrap items-center justify-between gap-3">
              <div className="flex items-center gap-2">
                <StatusBadge
                  status={sheet.data.status}
                  labels={{
                    open: t("Open"),
                    submitted: t("Submitted"),
                    approved: t("Approved"),
                    rejected: t("Rejected"),
                  }}
                />
                <span className="text-sm text-muted-foreground tabular-nums">
                  {t("{total} logged, {billable} billable", {
                    total: hours(sheet.data.seconds),
                    billable: hours(sheet.data.billableSeconds),
                  })}
                </span>
              </div>
              <div className="flex flex-wrap items-center gap-2">
                {locked ? null : (
                  <TimerControls
                    running={
                      sheet.data.running
                        ? {
                            project: sheet.data.running.note ?? "",
                            since: format.dateTime(
                              new Date(sheet.data.running.startedAt),
                              { timeStyle: "short" },
                            ),
                          }
                        : null
                    }
                  />
                )}
                {sheet.data.rows.length > 0 ? logTime : null}
                {locked || sheet.data.rows.length === 0 ? null : (
                  <SubmitWeekButton week={week} />
                )}
              </div>
            </div>
            {sheet.data.rows.length === 0 ? (
              <Empty className="border">
                <EmptyHeader>
                  <EmptyMedia variant="icon">
                    <ClockIcon />
                  </EmptyMedia>
                  <EmptyTitle>{t("Nothing logged this week")}</EmptyTitle>
                  <EmptyDescription>
                    {locked
                      ? t("This week is locked.")
                      : t(
                          "Log a block of time or start the timer on a project.",
                        )}
                  </EmptyDescription>
                </EmptyHeader>
                {logTime ? <EmptyContent>{logTime}</EmptyContent> : null}
              </Empty>
            ) : (
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>{t("Project")}</TableHead>
                    {sheet.data.days.map((day) => (
                      <TableHead key={day.date} className="text-right">
                        {dayLabel(day.date)}
                      </TableHead>
                    ))}
                    <TableHead className="text-right">{t("Total")}</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {sheet.data.rows.map((row) => (
                    <TableRow key={row.id}>
                      <TableCell>
                        <div className="flex items-center gap-2">
                          <span className="text-sm font-medium">
                            {row.label === "" ? t("No project") : row.label}
                          </span>
                          {row.billable ? (
                            <Badge variant="outline">{t("Billable")}</Badge>
                          ) : null}
                        </div>
                      </TableCell>
                      {row.days.map((seconds, index) => (
                        <TableCell
                          key={sheet.data.days[index]?.date ?? index}
                          className="text-right tabular-nums"
                        >
                          {seconds === 0 ? "" : hours(seconds)}
                        </TableCell>
                      ))}
                      <TableCell className="text-right font-medium tabular-nums">
                        {hours(row.seconds)}
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
                <TableFooter>
                  <TableRow>
                    <TableCell>{t("Total")}</TableCell>
                    {sheet.data.days.map((day) => (
                      <TableCell
                        key={day.date}
                        className="text-right tabular-nums"
                      >
                        {hours(day.seconds)}
                      </TableCell>
                    ))}
                    <TableCell className="text-right tabular-nums">
                      {hours(sheet.data.seconds)}
                    </TableCell>
                  </TableRow>
                </TableFooter>
              </Table>
            )}
          </>
        )}
      </CardContent>
    </Card>
  );
}
```

## Prompt

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

### Web

```text
Build "Timesheet" in a React web app that uses shadcn/ui components. A week of time entries per project with a running timer and totals.

It uses the better-supabase `time-tracking` block (Time tracking). 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 time-tracking` and apply the migration.
2. Add the shadcn components the recipe uses: `npx shadcn add badge button card empty skeleton table`.
3. The recipe needs no glue: it calls the block's client on the server.
4. Write the screen. Load the data on the server with the block's client, starting from the recipe's snippet (`apps/examples/nextjs/src/features/people/components/timesheet.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/timesheet.md
- Block docs: https://bettersupabase.com/docs/blocks/time-tracking
```
