Financial reporting workspace
https://fve.meiermade.com/components/page-examples/financial-reporting?state=error Workspace
Northwind Studio
This view could not be loaded
Your selection is preserved. Try loading it again.
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open Acme.Components.Documentation
open type Html
open type Svg
open type Datastar
type ExamplePage =
| DependencyGraph
| ExecutionDetail
| FinancialReporting
| Messaging
| Operations
| Scheduling
| MediaManagement
type ReviewState =
| Setup
| Ready
| Loading
| Empty
| Failed
type ExampleQuery =
{ state: ReviewState
item: string
view: string
range: string }
let defaultQuery =
{ state = Ready
item = ""
view = ""
range = "" }
let slug =
function
| DependencyGraph -> "dependency-graph"
| ExecutionDetail -> "execution-detail"
| FinancialReporting -> "financial-reporting"
| Messaging -> "messaging"
| Operations -> "operations-dashboard"
| Scheduling -> "scheduling"
| MediaManagement -> "media-management"
let title =
function
| DependencyGraph -> "Dependency graph"
| ExecutionDetail -> "Execution detail"
| FinancialReporting -> "Financial reporting"
| Messaging -> "Messaging"
| Operations -> "Operations dashboard"
| Scheduling -> "Scheduling"
| MediaManagement -> "Media management"
let url page =
"/components/page-examples/" + slug page
let stateKey =
function
| Setup -> "setup"
| Ready -> "ready"
| Loading -> "loading"
| Empty -> "empty"
| Failed -> "error"
let queryUrl page query =
let pairs =
[ "state", stateKey query.state
"item", query.item
"view", query.view
"range", query.range ]
url page
+ "?"
+ (pairs
|> List.filter (snd >> String.IsNullOrEmpty >> not)
|> List.map (fun (key, value) -> key + "=" + Uri.EscapeDataString value)
|> String.concat "&")
let link (href: string) (label: string) : HtmlElement =
a {
_href href
_class
"font-medium text-[var(--fve-brand-text)] underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-[var(--fve-brand-ring)]"
label
}
let section (heading: string) (content: HtmlElement) =
Section.create (SectionHeader.create heading |> SectionHeader.withDivider) content
|> Section.render id
let details (fields: (string * string) list) =
DescriptionList.create [ for label, value in fields -> DetailField.text label value ]
|> DescriptionList.withColumns DescriptionListColumns.Three
|> DescriptionList.render
let reviewStates page =
(if page = Operations then [ Setup, "Setup" ] else [])
@ [ Ready, "Populated"; Loading, "Loading"; Empty, "Empty"; Failed, "Error" ]
let stateContent page query content =
match query.state with
| Setup
| Ready -> content
| Loading ->
div {
_class "p-8"
LoadingIndicator.create ("Loading " + (title page).ToLowerInvariant())
|> LoadingIndicator.withVisibleLabel
|> LoadingIndicator.render
}
| Empty ->
EmptyState.create "Nothing to show yet" "Change the current selection or return to the populated workspace."
|> EmptyState.withActions (link (queryUrl page { query with state = Ready }) "Return to workspace")
|> EmptyState.render
| Failed ->
Notice.create
"workspace-error"
"This view could not be loaded"
(p { "Your selection is preserved. Try loading it again." })
|> Notice.withTone Tone.Critical
|> Notice.withActions (link (queryUrl page { query with state = Ready }) "Try again")
|> Notice.render
/// Place durable workspace identity above the scrollable destination list.
let sideNavWorkspace (name: string) =
div {
p {
_class "text-xs font-semibold uppercase tracking-wide text-[var(--fve-muted-text)]"
"Workspace"
}
p {
_class "mt-1 truncate text-sm font-semibold"
name
}
}
let workspace page heading subtitle canvas actions content =
let product, workspaceName, destinations =
match page with
| DependencyGraph
| ExecutionDetail ->
"Relay", "Customer analytics", [ DependencyGraph, "Dependencies"; ExecutionDetail, "Executions" ]
| FinancialReporting -> "Ledger", "Northwind Studio", [ FinancialReporting, "Overview" ]
| Messaging -> "Gather", "Northwind Outdoor", [ Messaging, "Messages" ]
| _ ->
"Fieldwork", "Northwind Outdoor", [ Operations, "Overview"; Scheduling, "Schedule"; MediaManagement, "Photos" ]
let items =
[ for destination, label in destinations -> SideNavItem.create destination label ]
let navigation =
SideNav.create
(slug page + "-nav")
(product + " navigation")
(SideNavHeader.create product)
[ SideNavSection.ungrouped items ]
|> SideNav.withCurrent page
|> SideNav.withWidth SideNavWidth.Narrow
|> SideNav.withContext (sideNavWorkspace workspaceName)
|> SideNav.withMobileContext (sideNavWorkspace workspaceName)
|> SideNav.withFooter (
div {
_class "flex min-w-0 items-center gap-3"
Avatar.create "Andy Meier" "AM" |> Avatar.render
p {
_class "truncate text-sm font-medium"
"Andy Meier"
}
}
)
let header =
PageHeader.create heading
|> PageHeader.withSubtitle subtitle
|> PageHeader.withActions (ActionCluster.create (slug page + "-actions") actions)
let topBar =
PageTopBar.create ()
|> PageTopBar.withContent (
div {
_class "flex min-h-[var(--fve-shell-bar-min-height)] items-center px-4 sm:px-6 lg:px-8"
Breadcrumbs.create
(slug page + "-breadcrumbs")
(product + " breadcrumb")
[ BreadcrumbItem.create page product; BreadcrumbItem.create page heading ]
|> Breadcrumbs.render url
}
)
let body =
Page.create header content
|> Page.withTopBar topBar
|> Page.withWidth PageWidth.Full
|> Page.withBodyLayout (
if canvas then
PageBodyLayout.Canvas
else
PageBodyLayout.Padded
)
|> Page.render id
AppShell.create (slug page + "-shell") navigation body
|> AppShell.withTheme (
ComponentsTheme.sky
|> ComponentsTheme.withDensity Density.Compact
|> ComponentsTheme.withControlSize ControlSize.Small)
|> AppShell.withBoundary AppShellBoundary.Container
|> AppShell.asPreview (product + " workspace")
|> AppShell.render url
type BalancePoint =
{ month: string
actual: decimal
planned: decimal }
let balances =
[ { month = "Apr"
actual = 24200M
planned = 24200M }
{ month = "May"
actual = 28100M
planned = 27000M }
{ month = "Jun"
actual = 26400M
planned = 30000M }
{ month = "Jul"
actual = 33200M
planned = 33000M }
{ month = "Aug"
actual = 35800M
planned = 36000M }
{ month = "Sep"
actual = 38442.11M
planned = 40000M } ]
let currency (value: decimal) =
value.ToString("C2", Globalization.CultureInfo.GetCultureInfo("en-US"))
let balanceChart query =
let points =
if query.range = "3m" then
balances |> List.skip 3
else
balances
let xPercent index =
float index * 100. / float (points.Length - 1)
let yPercent amount =
100. - float amount / 50000. * 100.
let chartX index =
xPercent index * 6.
let chartY amount =
yPercent amount * 1.8
let linePoints select =
points
|> List.mapi (fun index point -> sprintf "%.2f,%.2f" (chartX index) (chartY (select point)))
|> String.concat " "
div {
_class "grid gap-4"
div {
_class "flex flex-wrap items-center justify-between gap-3"
div {
h2 {
_class "text-base font-semibold"
"Operating checking"
}
p {
_class "mt-1 text-sm text-[var(--fve-muted-text)]"
if query.range = "3m" then
"Closing balance · USD · July–September 2026"
else
"Closing balance · USD · April–September 2026"
}
}
nav {
_ariaLabel "Balance period"
_class "flex gap-3 text-sm"
for value, label in [ "3m", "3 months"; "6m", "6 months" ] do
a {
_href (queryUrl FinancialReporting { query with range = value })
if (if query.range = "3m" then "3m" else "6m") = value then
_ariaCurrent "page"
_class
"inline-flex min-h-[var(--fve-control-min-height)] items-center rounded-[var(--fve-radius-control)] px-3 py-[var(--fve-control-padding-block)] text-[length:var(--fve-control-font-size)] leading-[var(--fve-control-line-height)] ring-1 ring-[var(--fve-border)] aria-[current=page]:bg-[var(--fve-brand-subtle)] aria-[current=page]:text-[var(--fve-brand-text)]"
label
}
}
}
div {
_class "flex flex-wrap gap-5 text-xs text-[var(--fve-muted-text)]"
span {
_class "flex items-center gap-2"
span {
_ariaHidden "true"
_class "h-1 w-5 rounded bg-[var(--fve-brand-solid)]"
}
"Actual"
}
span {
_class "flex items-center gap-2"
span {
_ariaHidden "true"
_class "w-5 border-t-2 border-dashed border-[var(--fve-muted-text)]"
}
"Plan"
}
}
div {
_role "region"
_ariaLabel "Account balance chart"
_tabindex 0
_class "overflow-x-auto"
div {
_class "grid min-w-[36rem] grid-cols-[3.5rem_minmax(0,1fr)] gap-x-3"
div {
_ariaHidden true
_class "flex h-52 flex-col justify-between text-right text-xs leading-none text-[var(--fve-muted-text)]"
for value in [ 50000M; 40000M; 30000M; 20000M; 10000M; 0M ] do
span { "$" + string (value / 1000M) + "k" }
}
div {
_class "relative h-52"
svg {
_viewBox "0 0 600 180"
_attr ("preserveAspectRatio", "none")
_role "img"
_ariaLabel
"Operating checking: actual and planned closing balances in USD. Values are available in the data table below."
_class "absolute inset-0 h-full w-full overflow-visible"
for value in [ 0M; 10000M; 20000M; 30000M; 40000M; 50000M ] do
line {
_x1 0
_x2 600
_y1 (chartY value)
_y2 (chartY value)
_stroke "var(--fve-border)"
_vectorEffect "non-scaling-stroke"
}
polyline {
_points (linePoints _.planned)
_fill "none"
_stroke "var(--fve-muted-text)"
_strokeWidth 2
_attr ("stroke-dasharray", "6 5")
_vectorEffect "non-scaling-stroke"
}
polyline {
_points (linePoints _.actual)
_fill "none"
_stroke "var(--fve-brand-solid)"
_strokeWidth 3
_vectorEffect "non-scaling-stroke"
}
}
for index, point in List.indexed points do
span {
_ariaHidden true
_class "absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--fve-brand-solid)]"
_style (sprintf "left:%.2f%%;top:%.2f%%" (xPercent index) (yPercent point.actual))
}
}
span { _ariaHidden true }
div {
_ariaHidden true
_class "flex justify-between pt-3 text-xs leading-none text-[var(--fve-muted-text)]"
for point in points do
span { point.month }
}
}
}
Html.details {
summary {
_class "cursor-pointer text-sm font-medium text-[var(--fve-brand-text)]"
"View balance data"
}
Table.create
"Monthly closing balances (USD)"
[ Table.column "Month" (fun (point: BalancePoint) -> text point.month)
|> Table.asRowHeader
|> Table.asMobilePrimary
Table.column "Actual" (fun point -> text (currency point.actual))
|> Table.alignEnd
Table.column "Plan" (fun point -> text (currency point.planned))
|> Table.alignEnd ]
points
|> Table.withMobileLayout TableMobileLayout.Records
|> Table.render
}
}
let financialReporting query =
let content =
div {
_class "grid gap-8"
div {
_class "grid gap-5 border-b border-[var(--fve-border)] pb-6 sm:grid-cols-3"
for label, value, description in
[ "Checking balance", "$38,442.11", "As of September 17"
"Change since April", "+$14,242.11", "58.9% increase"
"Against September plan", "−$1,557.89", "3.9% below plan" ] do
Metric.text label value |> Metric.withDescription description |> Metric.render
}
balanceChart query
section
"Recent transactions"
(Table.create
"Checking transactions"
[ Table.column "Date" (fun (date, _, _, _) -> text date)
Table.column "Description" (fun (_, name, id, _) ->
link
("/components/page-examples/account-management?destination=ledger-transaction-"
+ string id)
name)
|> Table.asRowHeader
|> Table.asMobilePrimary
Table.column "Amount" (fun (_, _, _, amount) -> text (currency amount))
|> Table.alignEnd ]
[ "Jul 28", "Northwind payment", 201, 4800M
"Jul 27", "Cloud hosting", 202, 386.42M
"Jul 26", "ACH withdrawal", 203, 1240M ]
|> Table.withMobileLayout TableMobileLayout.Records
|> Table.render)
}
workspace
FinancialReporting
"Financial overview"
"Northwind Studio · Operating checking · USD"
false
[ ApplicationAction.link
"/components/page-examples/account-management?destination=ledger-account-2048"
"View account"
ApplicationAction.link "/components/page-examples/account-management" "All accounts" ]
(stateContent FinancialReporting query content)
div { _class "h-[44rem]"; financialReporting defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/financial-reporting?state=ready"
|> Fixture.browser "page-workspace" "Workspace" "/components/page-examples/financial-reporting?state=ready"
|> Fixture.render