Targets progress
Sales targets for the period with the progress toward each one. It loads its data on the server through the block's client.
pnpm dev:example) to see it.Add the components
terminal npx shadcn add badge card empty progress skeletonInstall the glue
Install the block with
npx better-supabase add targets. The glue items are copied into your app, so edit them as you need.None: the page calls the block's client on the server.
Write the component
React and shadcn
View in the exampletargets-progress.tsx /** Render inside `<Suspense>`: the targets running today and how far along they are. */ export async function TargetsProgress() { const [session, t, format, locale] = await Promise.all([ getSession(), getExtracted("crm"), getFormatter(), getLocale(), ]); if (!can(session, "targets.read")) { return null; } const targets = await settle(loadTargets); if (targets.kind === "no-organization") { return null; } const canManage = can(session, "targets.manage"); const amountOf = (target: TargetRow, value: bigint) => target.currency === null ? format.number(value) : formatMoney(money(value, target.currency), locale); return ( <Card data-recipe="targets-progress"> <CardHeader> <CardTitle>{t("Targets")}</CardTitle> <CardDescription> {t("Won deals against this period's goals.")} </CardDescription> {canManage && targets.kind === "ok" && targets.data.length > 0 ? ( <CardAction> <CreateTargetDialog variant="outline" /> </CardAction> ) : null} </CardHeader> <CardContent> {targets.kind === "error" ? ( <CrmLoadError title={t("The targets didn't load")} /> ) : targets.data.length === 0 ? ( <Empty className="border"> <EmptyHeader> <EmptyMedia variant="icon"> <TargetIcon /> </EmptyMedia> <EmptyTitle>{t("No targets running")}</EmptyTitle> <EmptyDescription> {canManage ? t("Set a goal for the amount or number of deals won.") : t("An admin sets the sales targets.")} </EmptyDescription> </EmptyHeader> {canManage ? ( <EmptyContent> <CreateTargetDialog /> </EmptyContent> ) : null} </Empty> ) : ( <ul className="grid gap-6 md:grid-cols-2"> {targets.data.map((target) => ( <li key={target.id} className="flex flex-col gap-2"> <Progress value={Math.min(target.attainment * 100, 100)} aria-label={target.name} > <ProgressLabel>{target.name}</ProgressLabel> {target.attainment >= 1 ? ( <Badge variant="success-light" className="ml-auto"> {t("Reached")} </Badge> ) : ( <span className="ml-auto text-sm text-muted-foreground tabular-nums"> {format.number(target.attainment, { style: "percent", maximumFractionDigits: 0, })} </span> )} </Progress> <span className="text-xs text-muted-foreground tabular-nums"> {t("{progress} of {goal}, {start} to {end}", { progress: amountOf(target, target.progress), goal: amountOf(target, target.goal), start: Temporal.PlainDate.from( target.periodStart, ).toLocaleString(locale, { dateStyle: "medium" }), end: Temporal.PlainDate.from( target.periodEnd, ).toLocaleString(locale, { dateStyle: "medium" }), })} </span> </li> ))} </ul> )} </CardContent> </Card> ); }