@get-asset/sdk
Profit & Loss Card
Headless compound that fetches month-by-month revenue and expense pairs for the range on screen. The compound computes a nice scale automatically; override it to align bars across cards. The series covers every calendar month the range overlaps — a whole year gives the familiar twelve — and follows the AssetProvider's shared range (driven by the DatePicker) unless dateFrom / dateTo pin it.
Anatomy
Import the primitive and compose the parts you need.
<ProfitLossCard.RootdateFrom="2026-01-01"dateTo="2026-12-31"onViewDetails={({ period, basis }) => router.push(`/reports?year=${period?.year}&basis=${basis}`)}><ProfitLossCard.ViewDetails>View details</ProfitLossCard.ViewDetails><ProfitLossCard.Success><ProfitLossCard.Period /><ProfitLossCard.Months><ProfitLossCard.Month /><ProfitLossCard.Revenue /><ProfitLossCard.RevenueBar /><ProfitLossCard.Expense /><ProfitLossCard.ExpenseBar /></ProfitLossCard.Months></ProfitLossCard.Success></ProfitLossCard.Root>
Required scopes
The access token used by AssetProvider must include these scopes for this primitive to fetch data successfully:
report:readtag:readbusiness:read
API reference
Root props
| Prop | Type | Default | Description |
|---|---|---|---|
dateFrom | CalendarDateString | — | Inclusive start (YYYY-MM-DD) of a fixed reporting range. Detaches the card from the AssetProvider's shared range entirely; must be paired with dateTo (passing one side alone throws). Use periodDateRange to derive the pair from a month, quarter or year. Omit both to follow the provider's DatePicker-driven range. |
dateTo | CalendarDateString | — | Inclusive end (YYYY-MM-DD) of the fixed reporting range. Must be paired with dateFrom. |
months | ProfitLossMonthOverride[] | — | Override fetched data — one entry per month on screen. Each entry: { month, revenue, expense }. |
scale | number | — | Override the bar chart maximum. |
tagFields | TagFilterField[] | — | Override the fetched tag filter fields ({ name, values }[]) — primarily for testing/storybook. Pass [] to hide the tag filter. |
state | "loading" | "error" | "empty" | — | Force a specific state for testing. |
locked | boolean | — | Gate the report for an unentitled viewer. Activates the Locked state and skips the API fetch. See /docs/sdk/locked-upsell. |
upsell | { title?, description?, ctaLabel?, onUpgrade? } | — | Locked-state copy + upgrade handler; merged field-by-field over the card's default copy, then the generic fallback. |
onViewDetails | (ctx: { range, period, basis }) => void | — | Callback fired when the viewer asks to see more details (e.g. to navigate to the full report). Makes ProfitLossCard.ViewDetails render. Receives the card's resolved range, the exact period it represents (or null for a custom range) and the accounting basis, so the destination can open on the same window the card shows. |
basis | "accrual" | "modified_cash" | — | Report on a specific accounting basis. Omit to inherit the AssetProvider's basis, which itself falls back to the Business's country — Canadian Businesses report on Accrual, everyone else on Cash (see Accounting basis). |
basisToggle | boolean | — | Let the end user flip between Cash and Accrual. Off by default — BasisSelect renders nothing until it's set. Flipping the control moves every component that inherits the provider's basis, so it can't leave a screen half cash and half accrual (see Accounting basis). |
childrenrequired | ReactNode | — | Compound parts. |
Parts
Container
Always rendered. Provides data and context to its children.
| Part | Description |
|---|---|
ProfitLossCard.Root | Fetches monthly P&L (filtered to the selected tags) and the business's tag fields, and provides context. |
ProfitLossCard.BasisasChild? | Renders the active basis as text ("Accrual" / "Cash") and sets data-basis. Rendered whether or not the toggle is enabled. |
ProfitLossCard.BasisSelect | Wires basis state to a Select-Root-shaped child (injects value + onValueChange). Renders nothing unless basisToggle is enabled. |
ProfitLossCard.BasisItems | Iterates the Accrual / Cash options, providing per-option context. |
ProfitLossCard.BasisItemasChild? | Renders one basis option. Injects value and the option label. |
ProfitLossCard.ViewDetailsasChild? | Button that fires Root's onViewDetails callback with the card's resolved { range, period, basis } — the viewer's way to ask for more details (e.g. the full report). Renders nothing unless onViewDetails is set. Use asChild to slot a router Link or anchor instead; the handler still fires. |
Success
Renders when monthly data is available.
| Part | Description |
|---|---|
ProfitLossCard.Success | State wrapper. Compose the chart inside. |
ProfitLossCard.PeriodasChild? | Renders the resolved period — "2026" for a whole year, "Q3 2026" for a quarter, "Aug 2026" for a month, and the range label ("7 – 14 Jan 26") for anything else. Pass format(range, period) to override. |
ProfitLossCard.Months | Iterates children once per month. |
ProfitLossCard.MonthasChild? | Short month label for the current iteration. |
ProfitLossCard.RevenueasChild? | Revenue value for the current month. |
ProfitLossCard.ExpenseasChild? | Expense value for the current month. |
ProfitLossCard.RevenueBarstyle?, className? | Proportional bar for the current month's revenue. |
ProfitLossCard.ExpenseBarstyle?, className? | Proportional bar for the current month's expense. |
Loading
Renders while the fetch is in flight.
| Part | Description |
|---|---|
ProfitLossCard.Loading | State wrapper. Render any skeleton or placeholder UI inside. |
Empty
Renders when every month in the range is zero.
| Part | Description |
|---|---|
ProfitLossCard.Empty | State wrapper. Render an empty-state message inside. |
Error
Renders on fetch error.
| Part | Description |
|---|---|
ProfitLossCard.Error | State wrapper. Compose the error UI inside. |
ProfitLossCard.ErrorMessageasChild? | Renders the error message string. |
ProfitLossCard.RetryasChild? | Button that re-runs the fetch. |
Locked
Renders when `locked` is set — the viewer isn't entitled to the report. Mutually exclusive with the other states, and no data is fetched. See the Locked / upsell guide for composing the teaser + upgrade UI.
| Part | Description |
|---|---|
ProfitLossCard.Locked | State wrapper. Compose the upgrade / upsell UI inside — see LockedUpsell. |
Unlocked
Renders whenever the card is NOT locked — the inverse of Locked.
| Part | Description |
|---|---|
ProfitLossCard.Unlocked | State wrapper. Useful for gating extras (like the insights panel) that should also disappear while locked. |
Hooks
| Part | Description |
|---|---|
useProfitLossCard() | Hook returning range, period, months, scale, basis, setBasis, onViewDetails, loading, error, locked, retry, tagFields, selectedTags, setSelectedTags, toggleTag. Selecting tags refetches the chart filtered to them. |
useProfitLossMonth() | Hook used inside a Months iteration; returns the current month. |
Accounting basis
ProfitLossCard reports on one of two accounting bases: Accrual (accrual on the wire), which counts invoices and bills when they are issued, or Cash (modified_cash), which counts them when the money moves. Cashflow components take no basis — cashflow is a cash concept.
The basis is resolved per component, first match winning:
- the card's own
basisprop; - the provider's basis —
defaultBasisonAssetProvider, or whatever the toggle was last flipped to; - the Business's country — Canadian Businesses report on Accrual, everyone else on Cash.
Only the last step costs a request — a Business read, which is why every basis-aware component's scopes includes business:read — and it is serial: the card holds its own fetch until the Business answers. One request is shared across every basis-aware component on the screen, but pass defaultBasis on AssetProvider when you already know the basis and skip it entirely. If the lookup fails the card falls back to Cash and stays there — retry() re-runs the report query, not the basis lookup.
basisToggle is off by default: ProfitLossCard.BasisSelect renders nothing and the card reports on its resolved basis. Turn it on and flipping the control moves every basis-aware component under the provider with it, so the toggle can never leave a screen half cash and half accrual. A component pinned with its own basis prop is the exception — it keeps the basis you gave it and renders no toggle of its own, which is how you show both bases side by side on purpose.
ProfitLossCard.Basisrenders the active basis as text ("Accrual" / "Cash") and sets data-basis, whether or not the toggle is enabled — use it to label the figures even on a screen that offers no control.