"use client"; import { useMemo } from "react"; import { trpc } from "~/lib/trpc/client.js"; import type { WidgetProps } from "~/components/dashboard/widget-registry.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { WidgetFilterBar, type WidgetFilter } from "~/components/dashboard/WidgetFilterBar.js"; import { useWidgetFilterOptions } from "~/hooks/useWidgetFilterOptions.js"; function colorClass(pct: number): string { if (pct > 90) return "bg-red-500"; if (pct > 70) return "bg-amber-400"; return "bg-green-500"; } function textColorClass(pct: number): string { if (pct > 90) return "text-red-700"; if (pct > 70) return "text-amber-700"; return "text-green-700"; } type BudgetForecastLocation = { countryCode?: string | null; countryName?: string | null; federalState?: string | null; metroCityName?: string | null; activeAssignmentCount?: number; burnRateCents?: number; }; type BudgetForecastRow = { projectId?: string; projectName: string; shortCode: string; clientId: string | null; clientName: string | null; budgetCents: number; spentCents: number; remainingCents?: number; burnRate: number; estimatedExhaustionDate: string | null; pctUsed: number; activeAssignmentCount?: number; calendarLocations?: BudgetForecastLocation[]; }; function formatCurrency(cents: number | undefined): string { if (cents === undefined) return "—"; return `${(cents / 100).toLocaleString("de-DE", { maximumFractionDigits: 0 })} €`; } function formatLocation(location: BudgetForecastLocation): string { const parts = [ location.countryCode ?? location.countryName ?? null, location.federalState ?? null, location.metroCityName ?? null, ].filter((part): part is string => Boolean(part)); return parts.length > 0 ? parts.join(" / ") : "No calendar context"; } function SummaryCard({ label, value, helper, }: { label: string; value: string; helper: string; }) { return (
|
Project |
Budget Usage |
Burn/mo |
Exhaustion |
|---|---|---|---|
|
{row.shortCode}
{row.projectName}
{row.clientName ?? "No client"}
{!showDetails && row.calendarLocations && row.calendarLocations.length > 0
? ` · ${formatLocation(row.calendarLocations[0]!)}`
: ""}
{showDetails ? (
{row.activeAssignmentCount ?? 0} active assignments
Remaining {formatCurrency(row.remainingCents ?? Math.max(0, row.budgetCents - row.spentCents))}
{row.calendarLocations && row.calendarLocations.length > 0 ? (
row.calendarLocations.slice(0, 4).map((location) => (
{formatLocation(location)} · {location.activeAssignmentCount ?? 0}x · {formatCurrency(location.burnRateCents)}
))
) : (
No active calendar basis in the current month
)}
|
{formatCurrency(row.spentCents)} / {formatCurrency(row.budgetCents)}
{showDetails ? (
Remaining {formatCurrency(row.remainingCents ?? Math.max(0, row.budgetCents - row.spentCents))}
) : null}
|
{row.burnRate > 0 ? formatCurrency(row.burnRate) : "\u2014"}
{showDetails ? (
{row.activeAssignmentCount ?? 0} active assignments
{(row.calendarLocations ?? []).slice(0, 3).map((location) => (
{formatLocation(location)} · {location.activeAssignmentCount ?? 0}x · {formatCurrency(location.burnRateCents)}
))}
|
{row.estimatedExhaustionDate ?? "\u2014"}
{showDetails ? (
at {formatCurrency(row.burnRate)} / month
) : null}
|