Profit and loss
The closed month's revenue walked down through payroll, marketing, hosting and tooling to the net, as a waterfall with CSV, PNG and print exports; reads profitAndLoss().
Preview
"use client"
import {
chartSurface,
downloadBlob,
downloadText,
exportFilename,
printPage,
rowsToCsv,
svgToPng,
} from "@/lib/export"
import { formatCurrency } from "@/lib/format"
import { notify } from "@/lib/notify"
import { ChartCard } from "@/components/ui/chart-card"
import { ExportMenu, type ExportFormat } from "@/components/ui/export-menu"
import { WaterfallChart, type WaterfallMovement } from "@/components/ui/waterfall-chart"
const money = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 1 })
const CHART_ID = "profit-and-loss"
export type ProfitAndLossProps = {
movements: WaterfallMovement[]
month: string
}
/** Revenue down through each cost line to the net the month closed on. */
export function ProfitAndLoss({ movements, month }: ProfitAndLossProps) {
async function handleExport(format: ExportFormat) {
const name = `profit-and-loss-${month}`
if (format === "csv") {
const rows = movements.map((movement) => ({
line: movement.label,
amount: movement.value,
kind: movement.isTotal ? "total" : "movement",
}))
const written = downloadText(
exportFilename(name, "csv"),
rowsToCsv(rows, [
{ key: "line", label: "Line" },
{ key: "amount", label: "Amount (USD)" },
{ key: "kind", label: "Kind" },
])
)
notify[written ? "success" : "error"](
written ? "Downloaded the statement as CSV." : "This browser cannot save the file."
)
return
}
if (format === "png") {
const svg = chartSurface(document.getElementById(CHART_ID))
if (!svg) {
notify.error("The chart is still drawing.")
return
}
// The CSV branch above already reports what happened; the picture has two
// more ways to fail — drawing it, and saving it — and reported neither.
try {
const written = downloadBlob(exportFilename(name, "png"), await svgToPng(svg))
notify[written ? "success" : "error"](
written ? "Downloaded the chart as PNG." : "This browser cannot save the file."
)
} catch {
notify.error("The chart could not be drawn as a picture.")
}
return
}
printPage({ brand: "Northwind", title: "Profit and loss", range: month })
}
return (
<ChartCard
data-widget="widget-finance-overview-profit-and-loss"
id={CHART_ID}
title="Profit and loss"
description={`Revenue through to net, ${month}`}
height={280}
className="h-full"
// The card the numbers are on is the card that knows how to hand them
// over, so the menu sits in its own header rather than in the page's.
actions={
<ExportMenu size="sm" formats={["csv", "png", "pdf"]} onExport={handleExport} />
}
>
<WaterfallChart data={movements} height={280} valueFormatter={money} />
</ChartCard>
)
}Install
$
npx shadcn@latest add @vibra/widget-finance-overview-profit-and-lossNeeds the @vibra registry in your components.json — set it up once.
Source
"use client"
import {
chartSurface,
downloadBlob,
downloadText,
exportFilename,
printPage,
rowsToCsv,
svgToPng,
} from "@/lib/export"
import { formatCurrency } from "@/lib/format"
import { notify } from "@/lib/notify"
import { ChartCard } from "@/components/ui/chart-card"
import { ExportMenu, type ExportFormat } from "@/components/ui/export-menu"
import { WaterfallChart, type WaterfallMovement } from "@/components/ui/waterfall-chart"
const money = (value: number) =>
formatCurrency(value, "USD", { compact: true, maximumFractionDigits: 1 })
const CHART_ID = "profit-and-loss"
export type ProfitAndLossProps = {
movements: WaterfallMovement[]
month: string
}
/** Revenue down through each cost line to the net the month closed on. */
export function ProfitAndLoss({ movements, month }: ProfitAndLossProps) {
async function handleExport(format: ExportFormat) {
const name = `profit-and-loss-${month}`
if (format === "csv") {
const rows = movements.map((movement) => ({
line: movement.label,
amount: movement.value,
kind: movement.isTotal ? "total" : "movement",
}))
const written = downloadText(
exportFilename(name, "csv"),
rowsToCsv(rows, [
{ key: "line", label: "Line" },
{ key: "amount", label: "Amount (USD)" },
{ key: "kind", label: "Kind" },
])
)
notify[written ? "success" : "error"](
written ? "Downloaded the statement as CSV." : "This browser cannot save the file."
)
return
}
if (format === "png") {
const svg = chartSurface(document.getElementById(CHART_ID))
if (!svg) {
notify.error("The chart is still drawing.")
return
}
// The CSV branch above already reports what happened; the picture has two
// more ways to fail — drawing it, and saving it — and reported neither.
try {
const written = downloadBlob(exportFilename(name, "png"), await svgToPng(svg))
notify[written ? "success" : "error"](
written ? "Downloaded the chart as PNG." : "This browser cannot save the file."
)
} catch {
notify.error("The chart could not be drawn as a picture.")
}
return
}
printPage({ brand: "Northwind", title: "Profit and loss", range: month })
}
return (
<ChartCard
data-widget="widget-finance-overview-profit-and-loss"
id={CHART_ID}
title="Profit and loss"
description={`Revenue through to net, ${month}`}
height={280}
className="h-full"
// The card the numbers are on is the card that knows how to hand them
// over, so the menu sits in its own header rather than in the page's.
actions={
<ExportMenu size="sm" formats={["csv", "png", "pdf"]} onExport={handleExport} />
}
>
<WaterfallChart data={movements} height={280} valueFormatter={money} />
</ChartCard>
)
}Its page
On its page the card sits among the rest of the dashboard and shares its range and its data with them.
From the Finance dashboard page