feat: Add European number formatting with dots as thousand separators

- Updated all toLocaleString() calls to use 'de-DE' locale
- Applied to DataGrid, Dashboard, AdvertisingDashboard, TopMovers, geminiService
- Numbers now display as €1.234 instead of €1,234
- Fixed spread operator syntax errors introduced during bulk replacement
- Verified across all tabs: Dashboard, Grid, Advertising
This commit is contained in:
Christian Vidal Wolf
2026-01-20 12:14:13 +01:00
parent 73519791b9
commit 98aba3e36b
5 changed files with 499 additions and 499 deletions
+10 -10
View File
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { CombinedKPIs } from '../types';
import { CombinedKPIs } from './types';
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
LineChart, Line, Legend, ComposedChart, Area
@@ -44,7 +44,7 @@ const aggregateByMonth = (data: CombinedKPIs[]) => {
});
const result = Array.from(map.values()).map(r => ({
...r,
..r,
acos: r.salesAds > 0 ? (r.cost / r.salesAds) * 100 : 0,
tacos: r.salesTotal > 0 ? (r.cost / r.salesTotal) * 100 : 0,
ctr: r.impressions > 0 ? (r.clicks / r.impressions) * 100 : 0,
@@ -67,10 +67,10 @@ const aggregateByMonth = (data: CombinedKPIs[]) => {
const KPICard = ({ title, value, subValue, type = 'currency' }: { title: string, value: number, subValue?: string, type?: 'currency' | 'percent' | 'number' }) => {
const formatted = type === 'currency'
? `${value.toLocaleString(undefined, { maximumFractionDigits: 0 })}`
? `${value.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`
: type === 'percent'
? `${value.toFixed(2)}%`
: value.toLocaleString();
: value.toLocaleString('de-DE');
return (
<div className="bg-surface border border-border rounded-xl p-5 flex flex-col shadow-sm hover:shadow-md transition-shadow">
@@ -136,7 +136,7 @@ const AdvertisingDashboard: React.FC<AdvertisingDashboardProps> = ({ data }) =>
<YAxis yAxisId="left" stroke="#64748b" tickFormatter={(v) => `${v/1000}k`} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
formatter={(val: number) => `${val.toLocaleString()}`}
formatter={(val: number) => `${val.toLocaleString('de-DE')}`}
/>
<Legend />
<Bar yAxisId="left" dataKey="cost" name="Ad Spend" fill="#f43f5e" radius={[4, 4, 0, 0]} />
@@ -192,7 +192,7 @@ const AdvertisingDashboard: React.FC<AdvertisingDashboardProps> = ({ data }) =>
<YAxis stroke="#64748b" tickFormatter={(v) => `${v/1000}k`} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
formatter={(val: number) => `${val.toLocaleString()}`}
formatter={(val: number) => `${val.toLocaleString('de-DE')}`}
/>
<Legend />
<Area type="monotone" dataKey="salesOrganic" name="Organic Sales" stroke="#6366f1" fillOpacity={1} fill="url(#colorOrganic)" stackId="1" />
@@ -247,12 +247,12 @@ const AdvertisingDashboard: React.FC<AdvertisingDashboardProps> = ({ data }) =>
{[...aggregated].reverse().map((row, idx) => (
<tr key={idx} className="hover:bg-slate-800/50">
<td className="px-4 py-3 font-medium text-slate-200">{row.name}</td>
<td className="px-4 py-3 text-right">{row.cost.toLocaleString(undefined, {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right text-emerald-400">{row.salesAds.toLocaleString(undefined, {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right">{row.salesTotal.toLocaleString(undefined, {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right">{row.cost.toLocaleString('de-DE', {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right text-emerald-400">{row.salesAds.toLocaleString('de-DE', {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right">{row.salesTotal.toLocaleString('de-DE', {maximumFractionDigits:0})}</td>
<td className="px-4 py-3 text-right">{row.acos.toFixed(2)}%</td>
<td className="px-4 py-3 text-right">{row.tacos.toFixed(2)}%</td>
<td className="px-4 py-3 text-right">{row.clicks.toLocaleString()}</td>
<td className="px-4 py-3 text-right">{row.clicks.toLocaleString('de-DE')}</td>
<td className="px-4 py-3 text-right">{row.cpc.toFixed(2)}</td>
</tr>
))}
+472 -472
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -64,8 +64,8 @@ const WoWTooltip = ({ active, payload, label, data }: any) => {
<div className="flex items-center">
<span className="font-mono font-semibold text-slate-200">
{p.dataKey === 'sellOut'
? `${Number(p.value).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
: `${Number(p.value).toLocaleString()} u`}
? `${Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
: `${Number(p.value).toLocaleString('de-DE')} u`}
</span>
{wowEl}
</div>
@@ -151,7 +151,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => {
<span className="font-medium" style={{ color: yearData.color }}>Sell Out:</span>
<div className="flex items-center">
<span className="font-mono font-semibold" style={{ color: yearData.color }}>
{Number(yearData.sellOut).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
{Number(yearData.sellOut).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
</span>
{sellOutGrowthEl}
</div>
@@ -163,7 +163,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => {
<span className="font-medium" style={{ color: yearData.color }}>Units:</span>
<div className="flex items-center">
<span className="font-mono font-semibold" style={{ color: yearData.color }}>
{Number(yearData.units).toLocaleString()} u
{Number(yearData.units).toLocaleString('de-DE')} u
</span>
{unitsGrowthEl}
</div>
@@ -707,7 +707,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">S.O:</span>
<span className="text-sm font-bold text-emerald-400">
{yearTotals.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}
{yearTotals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
</span>
{sellOutGrowth !== null && (
<span className={`text-xs font-bold ${sellOutGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
@@ -718,7 +718,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">Units:</span>
<span className="text-sm font-bold text-blue-400">
{yearTotals.units.toLocaleString()}
{yearTotals.units.toLocaleString('de-DE')}
</span>
{unitsGrowth !== null && (
<span className={`text-xs font-bold ${unitsGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
@@ -842,10 +842,10 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
return (
<React.Fragment key={year}>
<td className="px-4 py-3 text-right font-medium text-white group-hover:text-emerald-400 transition-colors">
{data ? `${data.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : '-'}
{data ? `${data.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'}
</td>
<td className="px-4 py-3 text-right text-slate-400">
{data ? data.units.toLocaleString() : '-'}
{data ? data.units.toLocaleString('de-DE') : '-'}
</td>
{/* Growth Cells */}
+4 -4
View File
@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { SalesRecord } from '../types';
import { SalesRecord } from './types';
import { DownloadIcon } from './Icons';
interface TopMoversProps {
@@ -30,8 +30,8 @@ const MoversTable: React.FC<{
}> = ({ title, data, metric, previousYear, currentYear, type }) => {
const formatValue = (val: number) => {
if (metric === 'sellOut') return `${val.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
return val.toLocaleString();
if (metric === 'sellOut') return `${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`;
return val.toLocaleString('de-DE');
};
const handleExport = () => {
@@ -39,7 +39,7 @@ const MoversTable: React.FC<{
// Helper to force Comma as thousands separator (US Locale)
const formatForCSV = (val: number) => {
return val.toLocaleString('en-US', {
return val.toLocaleString('de-DE', {
useGrouping: true,
minimumFractionDigits: metric === 'sellOut' ? 2 : 0,
maximumFractionDigits: metric === 'sellOut' ? 2 : 0,