# Timesheet

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

Source: https://bettersupabase.com/docs/registry/ui-recipes/timesheet

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

It loads its data on the server through the block's client; the [time-tracking block](https://bettersupabase.com/blocks/time-tracking) must be installed (`npx better-supabase add time-tracking`). Its page on bettersupabase.com is [/recipes/timesheet](https://bettersupabase.com/recipes/timesheet).

Web, with shadcn components:

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

```tsx title="apps/examples/nextjs/src/features/people/components/timesheet.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
                          // oxlint-disable-next-line react-doctor/no-array-index-as-key -- the columns are a fixed week keyed by date; the index only covers a missing day
                          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>
  );
}
```