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;