Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 49 additions & 29 deletions web/src/components/Kpi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,54 @@ import type { KpiDatum } from '@/mock/types'
import { Sparkline } from './Sparkline'

export function Kpi({ datum }: { datum: KpiDatum }) {
return (
<div className="card" style={{ padding: 16, flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: 11,
color: 'var(--text-3)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
marginBottom: 8,
}}
>
{datum.label}
</div>
<div className="mono tnum" style={{ fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em' }}>
{datum.value}
</div>
<div style={{ fontSize: 11, marginTop: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
{datum.sub.map((segment, i) => (
<span key={i} style={{ color: toneColor(segment.tone) }}>
{segment.text}
</span>
))}
</div>
return (
<div className="card" style={{ padding: 16, flex: 1, minWidth: 0 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 12,
}}
>
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: 11,
color: 'var(--text-3)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
marginBottom: 8,
}}
>
{datum.label}
</div>
<div
className="mono tnum"
style={{ fontSize: 22, fontWeight: 500, letterSpacing: '-0.02em' }}
>
{datum.value}
</div>
<div
style={{
fontSize: 11,
marginTop: 6,
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
{datum.sub.map((segment, i) => (
<span key={i} style={{ color: toneColor(segment.tone) }}>
{segment.text}
</span>
))}
</div>
</div>
{datum.spark && (
<Sparkline data={datum.spark} color={datum.sparkColor ?? 'var(--accent)'} />
)}
</div>
</div>
<Sparkline data={datum.spark} color={datum.sparkColor ?? 'var(--accent)'} />
</div>
</div>
)
)
}
2 changes: 1 addition & 1 deletion web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export function Sidebar({ active = 'Overview' }: { active?: string }) {
whiteSpace: 'nowrap',
}}
>
mercator-eu-prod
acme-eu-prod
</div>
</div>
<Icon name="chevDown" size={12} />
Expand Down
37 changes: 37 additions & 0 deletions web/src/features/overview/liveKpis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

import { formatNumber } from '@/lib/format'
import type { KpiDatum } from '@/mock/types'
import type { OverviewMetrics } from './useOverviewMetrics'

// Maps the truthful API counts onto the headline cards. No sparklines: a single
// count has no time series to draw.
export function liveKpis(metrics: OverviewMetrics): KpiDatum[] {
return [
{
label: 'Transactions · total',
value: formatNumber(metrics.transactions),
sub: [{ text: 'posted to the ledger', tone: 'neutral' }],
},
{
label: 'Payments · total',
value: formatNumber(metrics.payments),
sub: [{ text: 'across all statuses', tone: 'neutral' }],
},
{
label: 'Accounts · total',
value: formatNumber(metrics.accounts),
sub: [{ text: 'open in the sandbox', tone: 'neutral' }],
},
{
label: 'Compliance · open',
value: formatNumber(metrics.openCases),
sub: [
metrics.openCases === 0
? { text: 'no open cases', tone: 'credit' }
: { text: 'cases awaiting review', tone: 'amber' },
],
},
]
}
64 changes: 64 additions & 0 deletions web/src/features/overview/useOverviewMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: BUSL-1.1
// SPDX-FileCopyrightText: 2026 FinCore Engine Authors

import { useQuery } from '@tanstack/react-query'
import { apiFetch } from '@/api/client'
import type {
AccountResponse,
CaseResponse,
PageResponse,
PaymentResponse,
TransactionResponse,
} from '@/api/types'

export interface OverviewMetrics {
transactions: number
payments: number
accounts: number
openCases: number
}

// Headline counts the sandbox API can answer truthfully today. Paged endpoints
// give totals through totalElements (size=1 keeps the payload tiny); open cases
// come from the status-filtered list. Latency or "today" windows have no public
// endpoint, so they are intentionally not surfaced here.
export function useOverviewMetrics(): {
isLoading: boolean
isError: boolean
metrics: OverviewMetrics | null
} {
const transactions = useQuery({
queryKey: ['overview', 'transactions'],
queryFn: () =>
apiFetch<PageResponse<TransactionResponse>>('/v1/transactions?page=0&size=1'),
})
const payments = useQuery({
queryKey: ['overview', 'payments'],
queryFn: () => apiFetch<PageResponse<PaymentResponse>>('/v1/payments?page=0&size=1'),
})
const accounts = useQuery({
queryKey: ['overview', 'accounts'],
queryFn: () => apiFetch<PageResponse<AccountResponse>>('/v1/accounts?page=0&size=1'),
})
const openCases = useQuery({
queryKey: ['overview', 'cases', 'OPEN'],
queryFn: () => apiFetch<CaseResponse[]>('/v1/compliance/cases?status=OPEN'),
})

const queries = [transactions, payments, accounts, openCases]
const metrics: OverviewMetrics | null =
transactions.data && payments.data && accounts.data && openCases.data
? {
transactions: transactions.data.totalElements,
payments: payments.data.totalElements,
accounts: accounts.data.totalElements,
openCases: openCases.data.length,
}
: null

return {
isLoading: queries.some((query) => query.isLoading),
isError: queries.some((query) => query.isError),
metrics,
}
}
Loading
Loading