diff --git a/src/App.tsx b/src/App.tsx index 771812d4..163c09d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -89,6 +89,7 @@ const PersonalIbanScreen = lazy(() => import('./screens/personal-iban.screen')); const BuyCryptoUpdateScreen = lazy(() => import('./screens/buy-crypto-update.screen')); const DashboardScreen = lazy(() => import('./screens/dashboard.screen')); const DashboardFinancialScreen = lazy(() => import('./screens/dashboard-financial.screen')); +const DashboardFinancialOverviewScreen = lazy(() => import('./screens/dashboard-financial-overview.screen')); const DashboardFinancialHistoryScreen = lazy(() => import('./screens/dashboard-financial-history.screen')); const DashboardFinancialLiveScreen = lazy(() => import('./screens/dashboard-financial-live.screen')); const DashboardFinancialExpensesScreen = lazy(() => import('./screens/dashboard-financial-expenses.screen')); @@ -515,6 +516,10 @@ export const Routes = [ index: true, element: withSuspense(), }, + { + path: 'overview', + element: withSuspense(), + }, { path: 'live', element: withSuspense(), diff --git a/src/__tests__/chart.test.ts b/src/__tests__/chart.test.ts index 0168cf50..973a7bff 100644 --- a/src/__tests__/chart.test.ts +++ b/src/__tests__/chart.test.ts @@ -1,4 +1,4 @@ -import { Timeframe, getFromDateByTimeframe } from '../util/chart'; +import { getFromDateByTimeframe, isDailySample, Timeframe } from '../util/chart'; describe('chart utils', () => { const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; @@ -55,6 +55,19 @@ describe('chart utils', () => { expect(getFromDateByTimeframe('unknown' as Timeframe)).toBe(0); }); + it('should return hourly sampling for DAY and THREE_DAYS', () => { + expect(isDailySample(Timeframe.DAY)).toBe(false); + expect(isDailySample(Timeframe.THREE_DAYS)).toBe(false); + }); + + it('should return daily sampling for WEEK and longer', () => { + expect(isDailySample(Timeframe.WEEK)).toBe(true); + expect(isDailySample(Timeframe.MONTH)).toBe(true); + expect(isDailySample(Timeframe.QUARTER)).toBe(true); + expect(isDailySample(Timeframe.YEAR)).toBe(true); + expect(isDailySample(Timeframe.ALL)).toBe(true); + }); + it('should return increasing values for longer timeframes', () => { const week = getFromDateByTimeframe(Timeframe.WEEK); const month = getFromDateByTimeframe(Timeframe.MONTH); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 374cbc1e..4b5afbe1 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -28,6 +28,8 @@ import { formatUnits, filenameDateFormat, extractFilename, + formatChf, + formatChfOrDash, formatCurrency, FormatType, deepEqual, @@ -195,6 +197,36 @@ describe('utils', () => { }); }); + describe('formatChf', () => { + it('should format integer values with Swiss thousands separator', () => { + const result = formatChf(48515); + expect(result).toContain('48'); + expect(result).toContain('515'); + }); + + it('should round to zero decimals', () => { + const result = formatChf(1234.78); + expect(result).not.toContain(','); + expect(result).not.toContain('.'); + }); + }); + + describe('formatChfOrDash', () => { + it('should return dash for undefined', () => { + expect(formatChfOrDash(undefined)).toBe('-'); + }); + + it('should format zero as "0 CHF"', () => { + expect(formatChfOrDash(0)).toBe('0 CHF'); + }); + + it('should append CHF suffix to formatted value', () => { + const result = formatChfOrDash(48515); + expect(result.endsWith(' CHF')).toBe(true); + expect(result).toContain('48'); + }); + }); + describe('deepEqual', () => { it('should return true for identical primitives', () => { expect(deepEqual(1, 1)).toBe(true); diff --git a/src/components/dashboard/age-badge.tsx b/src/components/dashboard/age-badge.tsx new file mode 100644 index 00000000..821860e3 --- /dev/null +++ b/src/components/dashboard/age-badge.tsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from 'react'; + +function formatAge(ms: number): string { + const s = Math.max(0, Math.floor(ms / 1000)); + if (s < 60) return `${s}s ago`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ${s % 60}s ago`; + const h = Math.floor(m / 60); + return `${h}h ${m % 60}m ago`; +} + +interface AgeBadgeProps { + timestamp?: string; +} + +export function AgeBadge({ timestamp }: AgeBadgeProps): JSX.Element { + const [now, setNow] = useState(Date.now()); + + useEffect(() => { + if (!timestamp) return; + const id = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(id); + }, [timestamp]); + + if (!timestamp) return <>-; + const parsed = new Date(timestamp).getTime(); + if (Number.isNaN(parsed)) return <>-; + return <>{formatAge(now - parsed)}; +} diff --git a/src/components/dashboard/balance-by-type-area-chart.tsx b/src/components/dashboard/balance-by-type-area-chart.tsx index 0f82ba13..a103eeb9 100644 --- a/src/components/dashboard/balance-by-type-area-chart.tsx +++ b/src/components/dashboard/balance-by-type-area-chart.tsx @@ -2,7 +2,7 @@ import { ApexOptions } from 'apexcharts'; import { useMemo } from 'react'; import Chart from 'react-apexcharts'; import { FinancialLogEntry } from 'src/dto/dashboard.dto'; -import { TimeRange } from 'src/screens/dashboard-financial-history.screen'; +import { TimeRange } from 'src/util/chart'; interface Props { entries: FinancialLogEntry[]; diff --git a/src/components/dashboard/financial-changes-chart.tsx b/src/components/dashboard/financial-changes-chart.tsx index e84ce59b..1a9de844 100644 --- a/src/components/dashboard/financial-changes-chart.tsx +++ b/src/components/dashboard/financial-changes-chart.tsx @@ -2,7 +2,7 @@ import { ApexOptions } from 'apexcharts'; import { useMemo } from 'react'; import Chart from 'react-apexcharts'; import { FinancialChangesEntry } from 'src/dto/dashboard.dto'; -import { TimeRange } from 'src/screens/dashboard-financial-history.screen'; +import { TimeRange } from 'src/util/chart'; interface FinancialChangesChartProps { entries: FinancialChangesEntry[]; diff --git a/src/components/dashboard/latest-balance-bar-chart.tsx b/src/components/dashboard/latest-balance-bar-chart.tsx index 80512fd9..3574d02b 100644 --- a/src/components/dashboard/latest-balance-bar-chart.tsx +++ b/src/components/dashboard/latest-balance-bar-chart.tsx @@ -6,15 +6,30 @@ import { BalanceByGroup } from 'src/dto/dashboard.dto'; interface BalanceBarChartProps { title: string; data: BalanceByGroup[]; + dark?: boolean; } -const ASSET_COLORS = [ - '#3b82f6', '#22c55e', '#f97316', '#8b5cf6', '#ef4444', - '#06b6d4', '#f59e0b', '#ec4899', '#14b8a6', '#6366f1', +const FALLBACK_ASSET_COLORS = [ + '#8b5cf6', '#06b6d4', '#f59e0b', '#ec4899', '#14b8a6', '#6366f1', '#84cc16', '#e11d48', '#0ea5e9', '#a855f7', '#64748b', ]; -export function BalanceBarChart({ title, data }: BalanceBarChartProps) { +const ASSET_COLOR_RULES: Array<{ test: (upper: string) => boolean; color: string }> = [ + { test: (u) => u.includes('BTC'), color: '#f97316' }, // orange — BTC family + { test: (u) => u.includes('USD'), color: '#22c55e' }, // green — USD family + { test: (u) => u.includes('EUR'), color: '#3b82f6' }, // blue — EUR family + { test: (u) => u.includes('CHF'), color: '#ef4444' }, // red — CHF family +]; + +function assetColor(name: string, fallbackIndex: number): string { + const upper = name.toUpperCase(); + for (const rule of ASSET_COLOR_RULES) { + if (rule.test(upper)) return rule.color; + } + return FALLBACK_ASSET_COLORS[fallbackIndex % FALLBACK_ASSET_COLORS.length]; +} + +export function BalanceBarChart({ title, data, dark }: BalanceBarChartProps) { const hasAssets = data.some((d) => d.assets && Object.keys(d.assets).length > 0); const sorted = useMemo(() => { @@ -25,21 +40,22 @@ export function BalanceBarChart({ title, data }: BalanceBarChartProps) { if (data.length === 0) return null; - if (hasAssets) return ; - return ; + if (hasAssets) return ; + return ; } -function SimpleBarChart({ title, data }: { title: string; data: BalanceByGroup[] }) { +function SimpleBarChart({ title, data, dark }: { title: string; data: BalanceByGroup[]; dark?: boolean }) { const categories = data.map((i) => i.name); const values = data.map((i) => Math.round(i.netBalanceChf)); const colors = data.map((i) => (i.netBalanceChf >= 0 ? '#22c55e' : '#ef4444')); const options = useMemo((): ApexOptions => ({ chart: { type: 'bar', toolbar: { show: false }, background: '0' }, + theme: { mode: dark ? 'dark' : 'light' }, plotOptions: { bar: { distributed: true, borderRadius: 4 } }, colors, dataLabels: { enabled: false }, - grid: { borderColor: '#e5e7eb' }, + grid: { borderColor: dark ? '#0A355C' : '#e5e7eb' }, xaxis: { categories }, yaxis: { title: { text: 'Net Balance (CHF)' }, @@ -47,7 +63,7 @@ function SimpleBarChart({ title, data }: { title: string; data: BalanceByGroup[] }, tooltip: { y: { formatter: (val: number) => `${val.toLocaleString('de-CH')} CHF` } }, legend: { show: false }, - }), [categories, colors]); + }), [categories, colors, dark]); const series = useMemo(() => [{ name: 'Net Balance', data: values }], [values]); @@ -59,7 +75,7 @@ function SimpleBarChart({ title, data }: { title: string; data: BalanceByGroup[] ); } -function StackedBarChart({ title, data }: { title: string; data: BalanceByGroup[] }) { +function StackedBarChart({ title, data, dark }: { title: string; data: BalanceByGroup[]; dark?: boolean }) { const { categories, assetNames, series } = useMemo(() => { const cats = data.map((d) => d.name); @@ -87,10 +103,11 @@ function StackedBarChart({ title, data }: { title: string; data: BalanceByGroup[ const options = useMemo((): ApexOptions => ({ chart: { type: 'bar', stacked: true, toolbar: { show: false }, background: '0' }, + theme: { mode: dark ? 'dark' : 'light' }, plotOptions: { bar: { borderRadius: 4, borderRadiusWhenStacked: 'last' as any } }, - colors: assetNames.map((_, i) => ASSET_COLORS[i % ASSET_COLORS.length]), + colors: assetNames.map((name, i) => assetColor(name, i)), dataLabels: { enabled: false }, - grid: { borderColor: '#e5e7eb' }, + grid: { borderColor: dark ? '#0A355C' : '#e5e7eb' }, xaxis: { categories }, yaxis: { title: { text: 'Balance (CHF)' }, @@ -98,7 +115,7 @@ function StackedBarChart({ title, data }: { title: string; data: BalanceByGroup[ }, tooltip: { y: { formatter: (val: number) => `${val.toLocaleString('de-CH')} CHF` } }, legend: { position: 'bottom' }, - }), [categories, assetNames]); + }), [categories, assetNames, dark]); return (
diff --git a/src/components/dashboard/summary-card.tsx b/src/components/dashboard/summary-card.tsx new file mode 100644 index 00000000..7a3487c1 --- /dev/null +++ b/src/components/dashboard/summary-card.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; + +interface SummaryCardProps { + label: string; + value: ReactNode; + color?: string; + dark?: boolean; +} + +export function SummaryCard({ label, value, color, dark }: SummaryCardProps): JSX.Element { + const cardBg = dark ? 'bg-dfxBlue-700' : 'bg-white'; + const labelColor = dark ? '#9AA5B8' : '#6b7280'; + const defaultValueColor = dark ? '#ffffff' : '#111827'; + return ( +
+
+ {label} +
+
+ {value} +
+
+ ); +} diff --git a/src/components/dashboard/total-balance-long-chart.tsx b/src/components/dashboard/total-balance-long-chart.tsx index a46a878a..cdd1648c 100644 --- a/src/components/dashboard/total-balance-long-chart.tsx +++ b/src/components/dashboard/total-balance-long-chart.tsx @@ -2,14 +2,15 @@ import { ApexOptions } from 'apexcharts'; import { useMemo } from 'react'; import Chart from 'react-apexcharts'; import { FinancialLogEntry } from 'src/dto/dashboard.dto'; -import { TimeRange } from 'src/screens/dashboard-financial-history.screen'; +import { TimeRange } from 'src/util/chart'; interface TotalBalanceLongChartProps { entries: FinancialLogEntry[]; timeRange?: TimeRange; + dark?: boolean; } -export function TotalBalanceLongChart({ entries, timeRange }: TotalBalanceLongChartProps) { +export function TotalBalanceLongChart({ entries, timeRange, dark }: TotalBalanceLongChartProps) { const chartOptions = useMemo((): ApexOptions => { return { chart: { @@ -18,10 +19,11 @@ export function TotalBalanceLongChart({ entries, timeRange }: TotalBalanceLongCh zoom: { enabled: true }, background: '0', }, + theme: { mode: dark ? 'dark' : 'light' }, stroke: { width: [3, 3], curve: 'smooth' }, colors: ['#22c55e', '#f97316'], dataLabels: { enabled: false }, - grid: { borderColor: '#e5e7eb' }, + grid: { borderColor: dark ? '#0A355C' : '#e5e7eb' }, xaxis: { type: 'datetime', labels: { datetimeUTC: false, format: 'dd MMM yy' }, @@ -48,7 +50,7 @@ export function TotalBalanceLongChart({ entries, timeRange }: TotalBalanceLongCh }, legend: { position: 'bottom' }, }; - }, [timeRange]); + }, [timeRange, dark]); const chartSeries = useMemo(() => { return [ diff --git a/src/components/dashboard/total-balance-short-chart.tsx b/src/components/dashboard/total-balance-short-chart.tsx index 28d7ed2a..2e0dfa80 100644 --- a/src/components/dashboard/total-balance-short-chart.tsx +++ b/src/components/dashboard/total-balance-short-chart.tsx @@ -2,7 +2,7 @@ import { ApexOptions } from 'apexcharts'; import { useMemo } from 'react'; import Chart from 'react-apexcharts'; import { FinancialLogEntry } from 'src/dto/dashboard.dto'; -import { TimeRange } from 'src/screens/dashboard-financial-history.screen'; +import { TimeRange } from 'src/util/chart'; interface Props { entries: FinancialLogEntry[]; diff --git a/src/hooks/layout-config.hook.tsx b/src/hooks/layout-config.hook.tsx index 177bf0c3..4ffaaa3a 100644 --- a/src/hooks/layout-config.hook.tsx +++ b/src/hooks/layout-config.hook.tsx @@ -1,5 +1,5 @@ import { isEqual } from 'lodash'; -import { useEffect, useRef } from 'react'; +import { useLayoutEffect, useRef } from 'react'; import { LayoutConfig, useLayoutConfigContext } from '../contexts/layout-config.context'; export function useLayoutOptions({ @@ -14,7 +14,7 @@ export function useLayoutOptions({ const { setConfig } = useLayoutConfigContext(); const prevConfig = useRef(); - useEffect(() => { + useLayoutEffect(() => { const newConfig: LayoutConfig = { title, backButton, @@ -36,7 +36,7 @@ export function useLayoutOptions({ } }, [title, backButton, textStart, noPadding, smallMenu, onBack, setConfig]); - useEffect(() => { + useLayoutEffect(() => { return () => setConfig({}); }, [setConfig]); } diff --git a/src/screens/dashboard-financial-history.screen.tsx b/src/screens/dashboard-financial-history.screen.tsx index f4948dc7..1468880b 100644 --- a/src/screens/dashboard-financial-history.screen.tsx +++ b/src/screens/dashboard-financial-history.screen.tsx @@ -10,19 +10,10 @@ import { useDashboard } from 'src/hooks/dashboard.hook'; import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; import { useNavigation } from 'src/hooks/navigation.hook'; -import { getFromDateByTimeframe, Timeframe } from 'src/util/chart'; - -export interface TimeRange { - min: number; - max: number; -} +import { getFromDateByTimeframe, isDailySample, Timeframe, TimeRange } from 'src/util/chart'; const TIMEFRAME_OPTIONS = [Timeframe.DAY, Timeframe.THREE_DAYS, Timeframe.WEEK, Timeframe.MONTH] as const; -function isDailySample(timeframe: Timeframe): boolean { - return timeframe === Timeframe.WEEK || timeframe === Timeframe.MONTH; -} - export default function DashboardFinancialLogScreen(): JSX.Element { useAdminGuard(); useLayoutOptions({ title: 'Financial Log', noMaxWidth: true }); diff --git a/src/screens/dashboard-financial-liquidity.screen.tsx b/src/screens/dashboard-financial-liquidity.screen.tsx index e47c04ac..f35a0c6f 100644 --- a/src/screens/dashboard-financial-liquidity.screen.tsx +++ b/src/screens/dashboard-financial-liquidity.screen.tsx @@ -1,11 +1,13 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { BalanceBarChart } from 'src/components/dashboard/latest-balance-bar-chart'; +import { SummaryCard } from 'src/components/dashboard/summary-card'; import { LatestBalanceResponse } from 'src/dto/dashboard.dto'; import { useDashboard } from 'src/hooks/dashboard.hook'; import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; +import { formatChfOrDash } from 'src/util/utils'; export default function DashboardFinancialLiquidityScreen(): JSX.Element { useAdminGuard(); @@ -22,9 +24,17 @@ export default function DashboardFinancialLiquidityScreen(): JSX.Element { getLatestBalance() .then(setLatestBalance) + .catch(() => undefined) .finally(() => setIsLoading(false)); }, [isLoggedIn]); + const totalBalance = useMemo(() => { + if (!latestBalance) return undefined; + let net = 0; + for (const t of latestBalance.byType) net += t.plusBalanceChf - t.minusBalanceChf; + return net; + }, [latestBalance]); + if (isLoading) { return (
@@ -35,6 +45,10 @@ export default function DashboardFinancialLiquidityScreen(): JSX.Element { return (
+
+ +
+
diff --git a/src/screens/dashboard-financial-live.screen.tsx b/src/screens/dashboard-financial-live.screen.tsx index c7efa7de..7d996d14 100644 --- a/src/screens/dashboard-financial-live.screen.tsx +++ b/src/screens/dashboard-financial-live.screen.tsx @@ -1,41 +1,20 @@ import { useSessionContext } from '@dfx.swiss/react'; import { SpinnerSize, StyledLoadingSpinner } from '@dfx.swiss/react-components'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { BalanceBarChart } from 'src/components/dashboard/latest-balance-bar-chart'; +import { SummaryCard } from 'src/components/dashboard/summary-card'; import { LatestBalanceResponse } from 'src/dto/dashboard.dto'; import { useDashboard } from 'src/hooks/dashboard.hook'; import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; - -function formatChf(value: number): string { - return value.toLocaleString('de-CH', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); -} - -interface SummaryCardProps { - label: string; - value: string; - color?: string; -} - -function SummaryCard({ label, value, color }: SummaryCardProps) { - return ( -
-
- {label} -
-
- {value} -
-
- ); -} +import { formatChfOrDash } from 'src/util/utils'; export default function DashboardFinancialLiveScreen(): JSX.Element { useAdminGuard(); useLayoutOptions({ title: 'Financial Live', noMaxWidth: true }); const { isLoggedIn } = useSessionContext(); - const { getLatestChanges, getLatestBalance } = useDashboard(); + const { getLatestBalance } = useDashboard(); const [latestBalance, setLatestBalance] = useState(); const [isLoading, setIsLoading] = useState(true); @@ -43,13 +22,23 @@ export default function DashboardFinancialLiveScreen(): JSX.Element { useEffect(() => { if (!isLoggedIn) return; - Promise.all([getLatestBalance(), getLatestChanges()]) - .then(([balanceData]) => { - setLatestBalance(balanceData); - }) + getLatestBalance() + .then(setLatestBalance) + .catch(() => undefined) .finally(() => setIsLoading(false)); }, [isLoggedIn]); + const { totalPlus, totalMinus, totalBalance } = useMemo(() => { + if (!latestBalance) return { totalPlus: undefined, totalMinus: undefined, totalBalance: undefined }; + let plus = 0; + let minus = 0; + for (const t of latestBalance.byType) { + plus += t.plusBalanceChf; + minus += t.minusBalanceChf; + } + return { totalPlus: plus, totalMinus: minus, totalBalance: plus - minus }; + }, [latestBalance]); + if (isLoading) { return (
@@ -58,16 +47,12 @@ export default function DashboardFinancialLiveScreen(): JSX.Element { ); } - const totalPlus = latestBalance?.byType.reduce((s, t) => s + t.plusBalanceChf, 0) ?? 0; - const totalMinus = latestBalance?.byType.reduce((s, t) => s + t.minusBalanceChf, 0) ?? 0; - return (
- {/* Summary Cards */}
- - - + + + (Timeframe.THREE_DAYS); + const [latestBalance, setLatestBalance] = useState(); + const [logEntries, setLogEntries] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (!isLoggedIn) return; + + const fromTimestamp = getFromDateByTimeframe(timeframe); + const from = fromTimestamp > 0 ? new Date(fromTimestamp).toISOString() : undefined; + const dailySample = isDailySample(timeframe); + + function load(initial: boolean) { + if (initial) setIsLoading(true); + getLatestBalance() + .then(setLatestBalance) + .catch(() => undefined); + getFinancialLog(from, dailySample) + .then((logData) => setLogEntries(logData.entries)) + .catch(() => undefined) + .finally(() => { + if (initial) setIsLoading(false); + }); + } + + load(true); + const interval = setInterval(() => load(false), REFRESH_INTERVAL_MS); + return () => clearInterval(interval); + }, [isLoggedIn, timeframe]); + + const timeRange = useMemo((): TimeRange | undefined => { + if (logEntries.length === 0) return undefined; + let min = Infinity; + let max = -Infinity; + for (const e of logEntries) { + const t = new Date(e.timestamp).getTime(); + if (t < min) min = t; + if (t > max) max = t; + } + return { min, max }; + }, [logEntries]); + + const { totalPlus, totalMinus, totalBalance } = useMemo(() => { + if (!latestBalance) return { totalPlus: undefined, totalMinus: undefined, totalBalance: undefined }; + let plus = 0; + let minus = 0; + for (const t of latestBalance.byType) { + plus += t.plusBalanceChf; + minus += t.minusBalanceChf; + } + return { totalPlus: plus, totalMinus: minus, totalBalance: plus - minus }; + }, [latestBalance]); + + return ( +
+
+ + + + } dark /> +
+ +
+ {TIMEFRAME_OPTIONS.map((tf) => ( + + ))} +
+ + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ +
+ +
+ +
+ + )} +
+ ); +} diff --git a/src/screens/dashboard-financial.screen.tsx b/src/screens/dashboard-financial.screen.tsx index 4c558a05..84769b10 100644 --- a/src/screens/dashboard-financial.screen.tsx +++ b/src/screens/dashboard-financial.screen.tsx @@ -3,6 +3,7 @@ import { useAdminGuard } from 'src/hooks/guard.hook'; import { useLayoutOptions } from 'src/hooks/layout-config.hook'; const pages = [ + { path: '/dashboard/financial/overview', title: 'Overview', description: 'Summary cards, balance history and liquidity' }, { path: '/dashboard/financial/live', title: 'Live', description: 'Current balances and latest changes' }, { path: '/dashboard/financial/history', title: 'History', description: 'Balance history over time' }, { path: '/dashboard/financial/liquidity', title: 'Liquidity', description: 'Balance breakdown by type' }, diff --git a/src/util/chart.ts b/src/util/chart.ts index e5b0f604..95b6b09f 100644 --- a/src/util/chart.ts +++ b/src/util/chart.ts @@ -1,3 +1,8 @@ +export interface TimeRange { + min: number; + max: number; +} + export enum Timeframe { DAY = '24h', THREE_DAYS = '3D', @@ -10,6 +15,10 @@ export enum Timeframe { const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; +export function isDailySample(timeframe: Timeframe): boolean { + return timeframe !== Timeframe.DAY && timeframe !== Timeframe.THREE_DAYS; +} + export function getFromDateByTimeframe(timeframe: Timeframe): number { switch (timeframe) { case Timeframe.ALL: diff --git a/src/util/utils.ts b/src/util/utils.ts index de06ff66..301f2134 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -312,6 +312,14 @@ export function formatAmountForDisplay(amount?: number): string { return Utils.formatAmount(amount).replace('.00', '.-').replace(' ', "'"); } +export function formatChf(value: number): string { + return value.toLocaleString('de-CH', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); +} + +export function formatChfOrDash(value?: number): string { + return value !== undefined ? `${formatChf(value)} CHF` : '-'; +} + export function deepEqual(a: any, b: any): boolean { if (a === b) return true; if (a == null || b == null) return false;