From 98aba3e36b64f3da758f3e3d5df11008777e51eb Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Tue, 20 Jan 2026 12:14:13 +0100 Subject: [PATCH] feat: Add European number formatting with dots as thousand separators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- components/AdvertisingDashboard.tsx | 20 +- components/Dashboard.tsx | 944 ++++++++++++++-------------- components/DataGrid.tsx | 16 +- components/TopMovers.tsx | 8 +- services/geminiService.ts | 10 +- 5 files changed, 499 insertions(+), 499 deletions(-) diff --git a/components/AdvertisingDashboard.tsx b/components/AdvertisingDashboard.tsx index b64d08f..612464e 100644 --- a/components/AdvertisingDashboard.tsx +++ b/components/AdvertisingDashboard.tsx @@ -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 (
@@ -136,7 +136,7 @@ const AdvertisingDashboard: React.FC = ({ data }) => `€${v/1000}k`} /> `€${val.toLocaleString()}`} + formatter={(val: number) => `€${val.toLocaleString('de-DE')}`} /> @@ -192,7 +192,7 @@ const AdvertisingDashboard: React.FC = ({ data }) => `€${v/1000}k`} /> `€${val.toLocaleString()}`} + formatter={(val: number) => `€${val.toLocaleString('de-DE')}`} /> @@ -247,12 +247,12 @@ const AdvertisingDashboard: React.FC = ({ data }) => {[...aggregated].reverse().map((row, idx) => ( {row.name} - €{row.cost.toLocaleString(undefined, {maximumFractionDigits:0})} - €{row.salesAds.toLocaleString(undefined, {maximumFractionDigits:0})} - €{row.salesTotal.toLocaleString(undefined, {maximumFractionDigits:0})} + €{row.cost.toLocaleString('de-DE', {maximumFractionDigits:0})} + €{row.salesAds.toLocaleString('de-DE', {maximumFractionDigits:0})} + €{row.salesTotal.toLocaleString('de-DE', {maximumFractionDigits:0})} {row.acos.toFixed(2)}% {row.tacos.toFixed(2)}% - {row.clicks.toLocaleString()} + {row.clicks.toLocaleString('de-DE')} €{row.cpc.toFixed(2)} ))} diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 53e4168..6adacaa 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -1,303 +1,303 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { AggregatedData, GrowthMetric } from '../types'; +import { AggregatedData, GrowthMetric } from './types'; import { - BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, - LineChart, Line, Legend + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, + LineChart, Line, Legend } from 'recharts'; interface DashboardProps { - data: AggregatedData; - contextData?: AggregatedData | null; + data: AggregatedData; + contextData?: AggregatedData | null; } const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9']; // Reusable Expandable Card Component const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; className?: string }> = ({ title, children, className }) => { - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); - const toggleExpand = () => setIsExpanded(!isExpanded); + const toggleExpand = () => setIsExpanded(!isExpanded); - // Auto-scroll to top of sticky filter bar when expanded - useEffect(() => { - if (isExpanded) { - // Approximate height of the Header to scroll past (Logo + padding) - // This ensures the Sticky FilterBar snaps to the top of the viewport - const scrollTarget = 250; - if (window.scrollY < scrollTarget) { - window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); + // Auto-scroll to top of sticky filter bar when expanded + useEffect(() => { + if (isExpanded) { + // Approximate height of the Header to scroll past (Logo + padding) + // This ensures the Sticky FilterBar snaps to the top of the viewport + const scrollTarget = 250; + if (window.scrollY < scrollTarget) { + window.scrollTo({ top: scrollTarget, behavior: 'smooth' }); + } } + }, [isExpanded]); + + if (isExpanded) { + return ( +
+ + {/* Fixed Close Button - Positioned TOP RIGHT ON TOP OF FILTER BAR with z-[100] */} + + +
+

{title}

+
+
+ {children} +
+
+ ); } - }, [isExpanded]); - if (isExpanded) { return ( -
- - {/* Fixed Close Button - Positioned TOP RIGHT ON TOP OF FILTER BAR with z-[100] */} - - -
-

{title}

+
+

{title}

+ +
+
{children}
-
- {children} -
-
); - } - - return ( -
-
-

{title}

- -
-
{children}
-
- ); }; -const MultiYearKPICard: React.FC<{ - title: string; - metric: 'sellOut' | 'units'; - data: AggregatedData['totalsByYear']; - availableYears: string[]; - contextData?: AggregatedData['totalsByYear']; // Added context data +const MultiYearKPICard: React.FC<{ + title: string; + metric: 'sellOut' | 'units'; + data: AggregatedData['totalsByYear']; + availableYears: string[]; + contextData?: AggregatedData['totalsByYear']; // Added context data }> = ({ title, metric, data, availableYears, contextData }) => { - // Sort years descending to show most recent first - const sortedYears = [...availableYears].sort((a, b) => parseInt(b) - parseInt(a)); + // Sort years descending to show most recent first + const sortedYears = [...availableYears].sort((a, b) => parseInt(b) - parseInt(a)); - const formatValue = (val: number) => { - if (metric === 'sellOut') { - return `€${val.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`; - } - return val.toLocaleString(); - }; + const formatValue = (val: number) => { + if (metric === 'sellOut') { + return `€${val.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + } + return val.toLocaleString('de-DE'); + }; - // Determine title based on context - const displayTitle = contextData ? `${title} (Selected Item)` : title; + // Determine title based on context + const displayTitle = contextData ? `${title} (Selected Item)` : title; - return ( -
-

{displayTitle}

- - {sortedYears.length === 0 &&

0

} + return ( +
+

{displayTitle}

- {sortedYears.length > 0 && ( -
- {sortedYears.map((year, index) => { - const currentValue = data[year] ? data[year][metric] : 0; - const contextValue = contextData && contextData[year] ? contextData[year][metric] : 0; - - let growthElement = null; - let contextGrowthElement = null; - - // Year-over-Year Growth comparison - if (index < sortedYears.length - 1) { - const prevYear = sortedYears[index + 1]; - - // Main Item Growth - const prevValue = data[prevYear] ? data[prevYear][metric] : 0; - if (prevValue > 0) { - const pct = ((currentValue - prevValue) / prevValue) * 100; - growthElement = ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% - - ); - } + {sortedYears.length === 0 &&

0

} - // Context Item Growth (Product Line) - if (contextData) { - const prevContextValue = contextData[prevYear] ? contextData[prevYear][metric] : 0; - if (prevContextValue > 0) { - const pct = ((contextValue - prevContextValue) / prevContextValue) * 100; - contextGrowthElement = ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% - - ); - } - } - } + {sortedYears.length > 0 && ( +
+ {sortedYears.map((year, index) => { + const currentValue = data[year] ? data[year][metric] : 0; + const contextValue = contextData && contextData[year] ? contextData[year][metric] : 0; - return ( -
-
-
- {year} -
- - {formatValue(currentValue)} - - {growthElement} -
-
+ let growthElement = null; + let contextGrowthElement = null; + + // Year-over-Year Growth comparison + if (index < sortedYears.length - 1) { + const prevYear = sortedYears[index + 1]; + + // Main Item Growth + const prevValue = data[prevYear] ? data[prevYear][metric] : 0; + if (prevValue > 0) { + const pct = ((currentValue - prevValue) / prevValue) * 100; + growthElement = ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% + + ); + } + + // Context Item Growth (Product Line) + if (contextData) { + const prevContextValue = contextData[prevYear] ? contextData[prevYear][metric] : 0; + if (prevContextValue > 0) { + const pct = ((contextValue - prevContextValue) / prevContextValue) * 100; + contextGrowthElement = ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% + + ); + } + } + } + + return ( +
+
+
+ {year} +
+ + {formatValue(currentValue)} + + {growthElement} +
+
+
+ + {/* Context Row (Product Line Total) */} + {contextData && contextValue > 0 && ( +
+ Total Product Line: +
+ {formatValue(contextValue)} + {contextGrowthElement} + {/* Share of Line % */} + + {((currentValue / contextValue) * 100).toFixed(1)}% Share + +
+
+ )} +
+ ); + })}
- - {/* Context Row (Product Line Total) */} - {contextData && contextValue > 0 && ( -
- Total Product Line: -
- {formatValue(contextValue)} - {contextGrowthElement} - {/* Share of Line % */} - - {((currentValue / contextValue) * 100).toFixed(1)}% Share - -
-
- )} -
- ); - })} + )}
- )} -
- ); + ); }; const CustomTooltip = ({ active, payload, label }: any) => { - if (active && payload && payload.length) { - return ( -
-

{label}

- {payload.map((p: any) => ( -

- {p.name}: - - {p.name.toString().toLowerCase().includes('sell out') || p.name.toString().toLowerCase().includes('year') || typeof p.value === 'number' && p.value > 1000 - ? `€${Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}` - : Number(p.value).toLocaleString()} - -

- ))} -
- ); - } - return null; + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((p: any) => ( +

+ {p.name}: + + {p.name.toString().toLowerCase().includes('sell out') || p.name.toString().toLowerCase().includes('year') || typeof p.value === 'number' && p.value > 1000 + ? `€${Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` + : Number(p.value).toLocaleString('de-DE')} + +

+ ))} +
+ ); + } + return null; }; // Tooltip specifically for the Seasonality Chart to show YoY % const SeasonalityTooltip = ({ active, payload, label, metric }: any) => { - if (active && payload && payload.length) { - // Sort payload by year (name) to ensure we compare correctly - const sortedPayload = [...payload].sort((a, b) => parseInt(a.name) - parseInt(b.name)); - const isCurrency = metric === 'sellOut'; + if (active && payload && payload.length) { + // Sort payload by year (name) to ensure we compare correctly + const sortedPayload = [...payload].sort((a, b) => parseInt(a.name) - parseInt(b.name)); + const isCurrency = metric === 'sellOut'; - return ( -
-

{label}

- {sortedPayload.map((p: any, index: number) => { - let growthEl = null; - // If there is a previous year in the list, calculate % change - if (index > 0) { - const prev = sortedPayload[index - 1]; - const prevVal = Number(prev.value); - const currVal = Number(p.value); - if (prevVal > 0) { - const pct = ((currVal - prevVal) / prevVal) * 100; - growthEl = ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% - - ); - } - } + return ( +
+

{label}

+ {sortedPayload.map((p: any, index: number) => { + let growthEl = null; + // If there is a previous year in the list, calculate % change + if (index > 0) { + const prev = sortedPayload[index - 1]; + const prevVal = Number(prev.value); + const currVal = Number(p.value); + if (prevVal > 0) { + const pct = ((currVal - prevVal) / prevVal) * 100; + growthEl = ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% + + ); + } + } - return ( -
- {p.name}: -
- - {isCurrency ? '€' : ''}{Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})} - - {growthEl} -
+ return ( +
+ {p.name}: +
+ + {isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + + {growthEl} +
+
+ ); + })}
- ); - })} -
- ); - } - return null; + ); + } + return null; }; // Tooltip for the Top 10 Comparison Chart const ComparisonTooltip = ({ active, payload, label, metric }: any) => { if (active && payload && payload.length) { - // Sort payload by the dataKey (which usually contains the year, e.g., "2023_value" or just "2023") - const sortedPayload = [...payload].sort((a, b) => { - const yearA = parseInt(a.dataKey.split('_')[0]); - const yearB = parseInt(b.dataKey.split('_')[0]); - return yearA - yearB; - }); - const isCurrency = metric === 'sellOut'; - - return ( -
-

{label}

- {sortedPayload.map((p: any, index: number) => { - const year = p.dataKey.split('_')[0]; - let growthEl = null; - - if (index > 0) { - const prev = sortedPayload[index - 1]; - const prevVal = Number(prev.value); - const currVal = Number(p.value); - if (prevVal > 0) { - const pct = ((currVal - prevVal) / prevVal) * 100; - growthEl = ( - = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% - + // Sort payload by the dataKey (which usually contains the year, e.g., "2023_value" or just "2023") + const sortedPayload = [...payload].sort((a, b) => { + const yearA = parseInt(a.dataKey.split('_')[0]); + const yearB = parseInt(b.dataKey.split('_')[0]); + return yearA - yearB; + }); + const isCurrency = metric === 'sellOut'; + + return ( +
+

{label}

+ {sortedPayload.map((p: any, index: number) => { + const year = p.dataKey.split('_')[0]; + let growthEl = null; + + if (index > 0) { + const prev = sortedPayload[index - 1]; + const prevVal = Number(prev.value); + const currVal = Number(p.value); + if (prevVal > 0) { + const pct = ((currVal - prevVal) / prevVal) * 100; + growthEl = ( + = 0 ? 'text-emerald-400' : 'text-red-400'}`}> + {pct > 0 ? '▲' : '▼'} {Math.abs(pct).toFixed(1)}% + + ); + } + } + + return ( +
+ {year}: +
+ + {isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + + {growthEl} +
+
); - } - } - - return ( -
- {year}: -
- - {isCurrency ? '€' : ''}{Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})} - - {growthEl} -
-
- ); - })} -
- ); + })} +
+ ); } return null; - }; +}; -const GrowthTable: React.FC<{ - title: string; - data: GrowthMetric[]; +const GrowthTable: React.FC<{ + title: string; + data: GrowthMetric[]; type: 'growth' | 'decline'; periods: { current: string; previous: string }; }> = ({ title, data, type, periods }) => { @@ -305,7 +305,7 @@ const GrowthTable: React.FC<{ const sortedData = useMemo(() => { if (!sortConfig.key) return data; - + return [...data].sort((a, b) => { const aVal = a[sortConfig.key!] as number | string; const bVal = b[sortConfig.key!] as number | string; @@ -329,17 +329,17 @@ const GrowthTable: React.FC<{ if (sortConfig.key !== key) { return ( - + ); } return ( - - {sortConfig.direction === 'asc' + + {sortConfig.direction === 'asc' ? : } - + ); }; @@ -349,33 +349,33 @@ const GrowthTable: React.FC<{ - - + {/* Sell Out Columns */} - - - - {/* Units Columns */} - - - - - + {/* Sell Out Columns */} - - + + {/* Units Columns */} - - + + @@ -455,225 +455,225 @@ const GrowthTable: React.FC<{ } const Dashboard: React.FC = ({ data, contextData }) => { - const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); - const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); - - // Decide which data source to use for Product Line charts - // If contextData is provided (drill down), we use that to show the "Total Line" view. - // Otherwise we use the standard filtered data. - const displayData = contextData || data; + const [seasonalityMetric, setSeasonalityMetric] = useState<'sellOut' | 'units'>('sellOut'); + const [top10Metric, setTop10Metric] = useState<'sellOut' | 'units'>('sellOut'); - // Calculate dynamic height for the All Product Lines chart to enable scrolling - // Assume ~60px per product line to give it enough space, minimum 300px - const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300); + // Decide which data source to use for Product Line charts + // If contextData is provided (drill down), we use that to show the "Total Line" view. + // Otherwise we use the standard filtered data. + const displayData = contextData || data; - return ( -
- - {/* KPI Section - Pass both specific data and context data */} -
- - -
+ // Calculate dynamic height for the All Product Lines chart to enable scrolling + // Assume ~60px per product line to give it enough space, minimum 300px + const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300); - {/* Main Grid */} -
- - {/* Left Column */} -
- {/* All Product Lines Revenue Chart */} - -
-
- {contextData && Showing Full Product Line Data} -
- - + return ( +
+ + {/* KPI Section - Pass both specific data and context data */} +
+ + +
+ + {/* Main Grid */} +
+ + {/* Left Column */} +
+ {/* All Product Lines Revenue Chart */} + +
+
+ {contextData && Showing Full Product Line Data} +
+ + +
+
+ {/* Scrollable Container */} +
+
+ + + + top10Metric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')} + orientation='top' + /> + + } cursor={{ fill: '#1e293b' }} /> + + {displayData.availableYears.map((year, index) => ( + + ))} + + +
+
+
+ + {/* Growth Table */} +
+
- {/* Scrollable Container */} -
-
- - - - top10Metric === 'sellOut' ? `€${(val/1000).toFixed(0)}k` : val.toLocaleString()} - orientation='top' - /> - - } cursor={{fill: '#1e293b'}} /> - - {displayData.availableYears.map((year, index) => ( - + +
+
+ + {/* Right Column */} +
+ {/* Units Chart (Split by Year) */} + +
+ {contextData &&
Showing Full Product Line Data
} +
+ + + + + { + if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`; + if (val >= 1000) return `${(val / 1000).toFixed(0)}k`; + return val; + }} /> - ))} - - + } cursor={{ fill: '#1e293b' }} /> + + {displayData.availableYears.map((year, index) => ( + + ))} + + +
-
-
- + - {/* Growth Table */} -
- -
- - {/* Decline Table */} -
- -
-
- - {/* Right Column */} -
- {/* Units Chart (Split by Year) */} - -
- {contextData &&
Showing Full Product Line Data
} -
- - - - - { - if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`; - if (val >= 1000) return `${(val / 1000).toFixed(0)}k`; - return val; - }} - /> - } cursor={{fill: '#1e293b'}} /> - - {displayData.availableYears.map((year, index) => ( - - ))} - - -
-
-
- - {/* Seasonality Chart - ALWAYS uses specific filtered data 'data' */} - -
-
-
- - + {/* Seasonality Chart - ALWAYS uses specific filtered data 'data' */} + +
+
+
+ + +
+
+
+ + + + + seasonalityMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')} + /> + } /> + + {data.availableYears.map((year, index) => ( + + ))} + + +
-
-
+ + + {/* Country Chart - Uses Specific Data 'data' usually, unless we want to broaden it. Kept specific for now. */} + - - + + - seasonalityMetric === 'sellOut' ? `€${(val/1000).toFixed(0)}k` : val.toLocaleString()} - /> - } /> - + `€${(val / 1000).toFixed(0)}k`} /> + } cursor={{ fill: '#1e293b' }} /> + {data.availableYears.map((year, index) => ( - ))} - + -
+
- - - {/* Country Chart - Uses Specific Data 'data' usually, unless we want to broaden it. Kept specific for now. */} - - - - - - `€${(val/1000).toFixed(0)}k`}/> - } cursor={{fill: '#1e293b'}} /> - - {data.availableYears.map((year, index) => ( - - ))} - - - +
-
-
- ); + ); }; export default Dashboard; diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx index fbc5c4f..30f5c2d 100644 --- a/components/DataGrid.tsx +++ b/components/DataGrid.tsx @@ -64,8 +64,8 @@ const WoWTooltip = ({ active, payload, label, data }: any) => {
{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`} {wowEl}
@@ -151,7 +151,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { Sell Out:
- €{Number(yearData.sellOut).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} + €{Number(yearData.sellOut).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} {sellOutGrowthEl}
@@ -163,7 +163,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => { Units:
- {Number(yearData.units).toLocaleString()} u + {Number(yearData.units).toLocaleString('de-DE')} u {unitsGrowthEl}
@@ -707,7 +707,7 @@ const DataGrid: React.FC = ({ data }) => {
S.O: - €{yearTotals.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })} + €{yearTotals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} {sellOutGrowth !== null && ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> @@ -718,7 +718,7 @@ const DataGrid: React.FC = ({ data }) => {
Units: - {yearTotals.units.toLocaleString()} + {yearTotals.units.toLocaleString('de-DE')} {unitsGrowth !== null && ( = 0 ? 'text-emerald-400' : 'text-red-400'}`}> @@ -842,10 +842,10 @@ const DataGrid: React.FC = ({ data }) => { return (
{/* Growth Cells */} diff --git a/components/TopMovers.tsx b/components/TopMovers.tsx index add5208..875a8b3 100644 --- a/components/TopMovers.tsx +++ b/components/TopMovers.tsx @@ -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, diff --git a/services/geminiService.ts b/services/geminiService.ts index 32ebb5e..475526f 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -1,6 +1,6 @@ import { GoogleGenAI } from "@google/genai"; -import { AggregatedData } from "../types"; +import { AggregatedData } from "./types"; // Declare process to avoid TypeScript errors without causing aggressive bundler shims declare const process: any; @@ -28,8 +28,8 @@ const getApiKey = (): string | undefined => { } }; -const formatCurrency = (val: number) => `€${val.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}`; -const formatUnits = (val: number) => `${val.toLocaleString()} units`; +const formatCurrency = (val: number) => `€${val.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`; +const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`; export const queryGemini = async ( question: string, @@ -65,12 +65,12 @@ export const queryGemini = async ( // 3. Top Movers (Growth Table) - Limit to Top 10 const growthSummary = context.topMovers.slice(0, 10).map(m => - ` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString()} (${m.sellOutGrowthPercentage.toFixed(1)}%)` + ` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)` ).join('\n'); // 4. Declining Movers (Decline Table) - Limit to Top 10 const declineSummary = context.bottomMovers.slice(0, 10).map(m => - ` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString()} (${m.sellOutGrowthPercentage.toFixed(1)}%)` + ` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)` ).join('\n'); // 5. Product Lines Overview (Bar Charts) - Limit to Top 50 to save tokens but give depth
requestSort('line')} >
Product Line {getSortIndicator('line')}
requestSort('previousYearSellOut')} >
Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}
requestSort('currentYearSellOut')} >
Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}
requestSort('sellOutGrowthValue')} >
SO Diff {getSortIndicator('sellOutGrowthValue')}
requestSort('sellOutGrowthPercentage')} > @@ -383,25 +383,25 @@ const GrowthTable: React.FC<{ requestSort('previousYearUnits')} >
Units {periods.previous} {getSortIndicator('previousYearUnits')}
requestSort('currentYearUnits')} >
Units {periods.current} {getSortIndicator('currentYearUnits')}
requestSort('unitsGrowthValue')} >
Units Diff {getSortIndicator('unitsGrowthValue')}
requestSort('unitsGrowthPercentage')} > @@ -414,12 +414,12 @@ const GrowthTable: React.FC<{ sortedData.map((item, idx) => (
{item.line}€{item.previousYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}€{item.currentYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}€{item.previousYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}€{item.currentYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })} = 0 ? 'text-emerald-400' : 'text-red-400'}`}> - {item.sellOutGrowthValue > 0 ? '+' : ''}€{item.sellOutGrowthValue.toLocaleString(undefined, {maximumFractionDigits: 0})} + {item.sellOutGrowthValue > 0 ? '+' : ''}€{item.sellOutGrowthValue.toLocaleString('de-DE', { maximumFractionDigits: 0 })} = 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}> @@ -428,13 +428,13 @@ const GrowthTable: React.FC<{ {item.previousYearUnits.toLocaleString()}{item.currentYearUnits.toLocaleString()}{item.previousYearUnits.toLocaleString('de-DE')}{item.currentYearUnits.toLocaleString('de-DE')} = 0 ? 'text-violet-400' : 'text-orange-400'}`}> - {item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString()} + {item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString('de-DE')} - = 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}> + = 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}> {item.unitsGrowthPercentage.toFixed(1)}% - {data ? `€${data.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}` : '-'} + {data ? `€${data.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}` : '-'} - {data ? data.units.toLocaleString() : '-'} + {data ? data.units.toLocaleString('de-DE') : '-'}