mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:45:24 +02:00
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:
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { CombinedKPIs } from '../types';
|
import { CombinedKPIs } from './types';
|
||||||
import {
|
import {
|
||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||||
LineChart, Line, Legend, ComposedChart, Area
|
LineChart, Line, Legend, ComposedChart, Area
|
||||||
@@ -44,7 +44,7 @@ const aggregateByMonth = (data: CombinedKPIs[]) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = Array.from(map.values()).map(r => ({
|
const result = Array.from(map.values()).map(r => ({
|
||||||
...r,
|
..r,
|
||||||
acos: r.salesAds > 0 ? (r.cost / r.salesAds) * 100 : 0,
|
acos: r.salesAds > 0 ? (r.cost / r.salesAds) * 100 : 0,
|
||||||
tacos: r.salesTotal > 0 ? (r.cost / r.salesTotal) * 100 : 0,
|
tacos: r.salesTotal > 0 ? (r.cost / r.salesTotal) * 100 : 0,
|
||||||
ctr: r.impressions > 0 ? (r.clicks / r.impressions) * 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 KPICard = ({ title, value, subValue, type = 'currency' }: { title: string, value: number, subValue?: string, type?: 'currency' | 'percent' | 'number' }) => {
|
||||||
const formatted = type === 'currency'
|
const formatted = type === 'currency'
|
||||||
? `€${value.toLocaleString(undefined, { maximumFractionDigits: 0 })}`
|
? `€${value.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`
|
||||||
: type === 'percent'
|
: type === 'percent'
|
||||||
? `${value.toFixed(2)}%`
|
? `${value.toFixed(2)}%`
|
||||||
: value.toLocaleString();
|
: value.toLocaleString('de-DE');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-surface border border-border rounded-xl p-5 flex flex-col shadow-sm hover:shadow-md transition-shadow">
|
<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`} />
|
<YAxis yAxisId="left" stroke="#64748b" tickFormatter={(v) => `€${v/1000}k`} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
||||||
formatter={(val: number) => `€${val.toLocaleString()}`}
|
formatter={(val: number) => `€${val.toLocaleString('de-DE')}`}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Bar yAxisId="left" dataKey="cost" name="Ad Spend" fill="#f43f5e" radius={[4, 4, 0, 0]} />
|
<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`} />
|
<YAxis stroke="#64748b" tickFormatter={(v) => `€${v/1000}k`} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', color: '#e2e8f0' }}
|
||||||
formatter={(val: number) => `€${val.toLocaleString()}`}
|
formatter={(val: number) => `€${val.toLocaleString('de-DE')}`}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
<Area type="monotone" dataKey="salesOrganic" name="Organic Sales" stroke="#6366f1" fillOpacity={1} fill="url(#colorOrganic)" stackId="1" />
|
<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) => (
|
{[...aggregated].reverse().map((row, idx) => (
|
||||||
<tr key={idx} className="hover:bg-slate-800/50">
|
<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 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">€{row.cost.toLocaleString('de-DE', {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 text-emerald-400">€{row.salesAds.toLocaleString('de-DE', {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.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.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.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>
|
<td className="px-4 py-3 text-right">€{row.cpc.toFixed(2)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+22
-22
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { AggregatedData, GrowthMetric } from '../types';
|
import { AggregatedData, GrowthMetric } from './types';
|
||||||
import {
|
import {
|
||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||||
LineChart, Line, Legend
|
LineChart, Line, Legend
|
||||||
@@ -90,9 +90,9 @@ const MultiYearKPICard: React.FC<{
|
|||||||
|
|
||||||
const formatValue = (val: number) => {
|
const formatValue = (val: number) => {
|
||||||
if (metric === 'sellOut') {
|
if (metric === 'sellOut') {
|
||||||
return `€${val.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
|
return `€${val.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||||
}
|
}
|
||||||
return val.toLocaleString();
|
return val.toLocaleString('de-DE');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine title based on context
|
// Determine title based on context
|
||||||
@@ -189,8 +189,8 @@ const CustomTooltip = ({ active, payload, label }: any) => {
|
|||||||
<span>{p.name}:</span>
|
<span>{p.name}:</span>
|
||||||
<span className="font-mono font-semibold">
|
<span className="font-mono font-semibold">
|
||||||
{p.name.toString().toLowerCase().includes('sell out') || p.name.toString().toLowerCase().includes('year') || typeof p.value === 'number' && p.value > 1000
|
{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('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
|
||||||
: Number(p.value).toLocaleString()}
|
: Number(p.value).toLocaleString('de-DE')}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
@@ -232,7 +232,7 @@ const SeasonalityTooltip = ({ active, payload, label, metric }: any) => {
|
|||||||
<span style={{ color: p.color }}>{p.name}:</span>
|
<span style={{ color: p.color }}>{p.name}:</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-mono font-semibold text-slate-200">
|
<span className="font-mono font-semibold text-slate-200">
|
||||||
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
|
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||||
</span>
|
</span>
|
||||||
{growthEl}
|
{growthEl}
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +282,7 @@ const ComparisonTooltip = ({ active, payload, label, metric }: any) => {
|
|||||||
<span style={{ color: p.color }}>{year}:</span>
|
<span style={{ color: p.color }}>{year}:</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-mono font-semibold text-slate-200">
|
<span className="font-mono font-semibold text-slate-200">
|
||||||
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}
|
{isCurrency ? '€' : ''}{Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}
|
||||||
</span>
|
</span>
|
||||||
{growthEl}
|
{growthEl}
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +293,7 @@ const ComparisonTooltip = ({ active, payload, label, metric }: any) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GrowthTable: React.FC<{
|
const GrowthTable: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
@@ -416,10 +416,10 @@ const GrowthTable: React.FC<{
|
|||||||
<td className="px-4 py-2 font-medium">{item.line}</td>
|
<td className="px-4 py-2 font-medium">{item.line}</td>
|
||||||
|
|
||||||
{/* Sell Out Columns */}
|
{/* Sell Out Columns */}
|
||||||
<td className="px-4 py-2 text-right text-slate-400">€{item.previousYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
<td className="px-4 py-2 text-right text-slate-400">€{item.previousYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
|
||||||
<td className="px-4 py-2 text-right font-medium text-slate-200">€{item.currentYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
<td className="px-4 py-2 text-right font-medium text-slate-200">€{item.currentYearSellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}</td>
|
||||||
<td className={`px-4 py-2 text-right font-medium ${item.sellOutGrowthValue >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
<td className={`px-4 py-2 text-right font-medium ${item.sellOutGrowthValue >= 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 })}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.sellOutGrowthPercentage >= 0 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>
|
||||||
@@ -428,10 +428,10 @@ const GrowthTable: React.FC<{
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Units Columns */}
|
{/* Units Columns */}
|
||||||
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearUnits.toLocaleString()}</td>
|
<td className="px-4 py-2 text-right text-slate-400">{item.previousYearUnits.toLocaleString('de-DE')}</td>
|
||||||
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearUnits.toLocaleString()}</td>
|
<td className="px-4 py-2 text-right font-medium text-slate-200">{item.currentYearUnits.toLocaleString('de-DE')}</td>
|
||||||
<td className={`px-4 py-2 text-right font-medium ${item.unitsGrowthValue >= 0 ? 'text-violet-400' : 'text-orange-400'}`}>
|
<td className={`px-4 py-2 text-right font-medium ${item.unitsGrowthValue >= 0 ? 'text-violet-400' : 'text-orange-400'}`}>
|
||||||
{item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString()}
|
{item.unitsGrowthValue > 0 ? '+' : ''}{item.unitsGrowthValue.toLocaleString('de-DE')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.unitsGrowthPercentage >= 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}>
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${item.unitsGrowthPercentage >= 0 ? 'bg-violet-500/10 text-violet-400' : 'bg-orange-500/10 text-orange-400'}`}>
|
||||||
@@ -522,11 +522,11 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
|||||||
<XAxis
|
<XAxis
|
||||||
type="number"
|
type="number"
|
||||||
stroke="#64748b"
|
stroke="#64748b"
|
||||||
tickFormatter={(val) => top10Metric === 'sellOut' ? `€${(val/1000).toFixed(0)}k` : val.toLocaleString()}
|
tickFormatter={(val) => top10Metric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
|
||||||
orientation='top'
|
orientation='top'
|
||||||
/>
|
/>
|
||||||
<YAxis dataKey="name" type="category" width={100} stroke="#94a3b8" tick={{fontSize: 12}} />
|
<YAxis dataKey="name" type="category" width={100} stroke="#94a3b8" tick={{ fontSize: 12 }} />
|
||||||
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric={top10Metric} />} cursor={{fill: '#1e293b'}} />
|
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric={top10Metric} />} cursor={{ fill: '#1e293b' }} />
|
||||||
|
|
||||||
{displayData.availableYears.map((year, index) => (
|
{displayData.availableYears.map((year, index) => (
|
||||||
<Bar
|
<Bar
|
||||||
@@ -575,7 +575,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
|||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={displayData.byLineOverviewSplit} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
<BarChart data={displayData.byLineOverviewSplit} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
<XAxis dataKey="name" stroke="#64748b" tick={{fontSize: 10}} interval={0} angle={-15} textAnchor="end" height={60} />
|
<XAxis dataKey="name" stroke="#64748b" tick={{ fontSize: 10 }} interval={0} angle={-15} textAnchor="end" height={60} />
|
||||||
<YAxis
|
<YAxis
|
||||||
stroke="#64748b"
|
stroke="#64748b"
|
||||||
tickFormatter={(val) => {
|
tickFormatter={(val) => {
|
||||||
@@ -584,7 +584,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
|||||||
return val;
|
return val;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="units" />} cursor={{fill: '#1e293b'}} />
|
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="units" />} cursor={{ fill: '#1e293b' }} />
|
||||||
|
|
||||||
{displayData.availableYears.map((year, index) => (
|
{displayData.availableYears.map((year, index) => (
|
||||||
<Bar
|
<Bar
|
||||||
@@ -627,7 +627,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
|||||||
<XAxis dataKey="name" stroke="#64748b" />
|
<XAxis dataKey="name" stroke="#64748b" />
|
||||||
<YAxis
|
<YAxis
|
||||||
stroke="#64748b"
|
stroke="#64748b"
|
||||||
tickFormatter={(val) => seasonalityMetric === 'sellOut' ? `€${(val/1000).toFixed(0)}k` : val.toLocaleString()}
|
tickFormatter={(val) => seasonalityMetric === 'sellOut' ? `€${(val / 1000).toFixed(0)}k` : val.toLocaleString('de-DE')}
|
||||||
/>
|
/>
|
||||||
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
|
<Tooltip content={(props: any) => <SeasonalityTooltip {...props} metric={seasonalityMetric} />} />
|
||||||
<Legend />
|
<Legend />
|
||||||
@@ -655,8 +655,8 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
|||||||
<BarChart data={data.byCustomerSplit}>
|
<BarChart data={data.byCustomerSplit}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
||||||
<XAxis dataKey="name" stroke="#64748b" />
|
<XAxis dataKey="name" stroke="#64748b" />
|
||||||
<YAxis stroke="#64748b" tickFormatter={(val) => `€${(val/1000).toFixed(0)}k`}/>
|
<YAxis stroke="#64748b" tickFormatter={(val) => `€${(val / 1000).toFixed(0)}k`} />
|
||||||
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="sellOut" />} cursor={{fill: '#1e293b'}} />
|
<Tooltip content={(props: any) => <ComparisonTooltip {...props} metric="sellOut" />} cursor={{ fill: '#1e293b' }} />
|
||||||
|
|
||||||
{data.availableYears.map((year, index) => (
|
{data.availableYears.map((year, index) => (
|
||||||
<Bar
|
<Bar
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ const WoWTooltip = ({ active, payload, label, data }: any) => {
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-mono font-semibold text-slate-200">
|
<span className="font-mono font-semibold text-slate-200">
|
||||||
{p.dataKey === 'sellOut'
|
{p.dataKey === 'sellOut'
|
||||||
? `€${Number(p.value).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
|
? `€${Number(p.value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`
|
||||||
: `${Number(p.value).toLocaleString()} u`}
|
: `${Number(p.value).toLocaleString('de-DE')} u`}
|
||||||
</span>
|
</span>
|
||||||
{wowEl}
|
{wowEl}
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +151,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => {
|
|||||||
<span className="font-medium" style={{ color: yearData.color }}>Sell Out:</span>
|
<span className="font-medium" style={{ color: yearData.color }}>Sell Out:</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-mono font-semibold" style={{ color: yearData.color }}>
|
<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>
|
</span>
|
||||||
{sellOutGrowthEl}
|
{sellOutGrowthEl}
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +163,7 @@ const ComparisonTooltip = ({ active, payload, label }: any) => {
|
|||||||
<span className="font-medium" style={{ color: yearData.color }}>Units:</span>
|
<span className="font-medium" style={{ color: yearData.color }}>Units:</span>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="font-mono font-semibold" style={{ color: yearData.color }}>
|
<span className="font-mono font-semibold" style={{ color: yearData.color }}>
|
||||||
{Number(yearData.units).toLocaleString()} u
|
{Number(yearData.units).toLocaleString('de-DE')} u
|
||||||
</span>
|
</span>
|
||||||
{unitsGrowthEl}
|
{unitsGrowthEl}
|
||||||
</div>
|
</div>
|
||||||
@@ -707,7 +707,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-slate-500">S.O:</span>
|
<span className="text-xs text-slate-500">S.O:</span>
|
||||||
<span className="text-sm font-bold text-emerald-400">
|
<span className="text-sm font-bold text-emerald-400">
|
||||||
€{yearTotals.sellOut.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
€{yearTotals.sellOut.toLocaleString('de-DE', { maximumFractionDigits: 0 })}
|
||||||
</span>
|
</span>
|
||||||
{sellOutGrowth !== null && (
|
{sellOutGrowth !== null && (
|
||||||
<span className={`text-xs font-bold ${sellOutGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-slate-500">Units:</span>
|
<span className="text-xs text-slate-500">Units:</span>
|
||||||
<span className="text-sm font-bold text-blue-400">
|
<span className="text-sm font-bold text-blue-400">
|
||||||
{yearTotals.units.toLocaleString()}
|
{yearTotals.units.toLocaleString('de-DE')}
|
||||||
</span>
|
</span>
|
||||||
{unitsGrowth !== null && (
|
{unitsGrowth !== null && (
|
||||||
<span className={`text-xs font-bold ${unitsGrowth >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
<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 (
|
return (
|
||||||
<React.Fragment key={year}>
|
<React.Fragment key={year}>
|
||||||
<td className="px-4 py-3 text-right font-medium text-white group-hover:text-emerald-400 transition-colors">
|
<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>
|
||||||
<td className="px-4 py-3 text-right text-slate-400">
|
<td className="px-4 py-3 text-right text-slate-400">
|
||||||
{data ? data.units.toLocaleString() : '-'}
|
{data ? data.units.toLocaleString('de-DE') : '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{/* Growth Cells */}
|
{/* Growth Cells */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { SalesRecord } from '../types';
|
import { SalesRecord } from './types';
|
||||||
import { DownloadIcon } from './Icons';
|
import { DownloadIcon } from './Icons';
|
||||||
|
|
||||||
interface TopMoversProps {
|
interface TopMoversProps {
|
||||||
@@ -30,8 +30,8 @@ const MoversTable: React.FC<{
|
|||||||
}> = ({ title, data, metric, previousYear, currentYear, type }) => {
|
}> = ({ title, data, metric, previousYear, currentYear, type }) => {
|
||||||
|
|
||||||
const formatValue = (val: number) => {
|
const formatValue = (val: number) => {
|
||||||
if (metric === 'sellOut') return `€${val.toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
if (metric === 'sellOut') return `€${val.toLocaleString('de-DE', { maximumFractionDigits: 0 })}`;
|
||||||
return val.toLocaleString();
|
return val.toLocaleString('de-DE');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
@@ -39,7 +39,7 @@ const MoversTable: React.FC<{
|
|||||||
|
|
||||||
// Helper to force Comma as thousands separator (US Locale)
|
// Helper to force Comma as thousands separator (US Locale)
|
||||||
const formatForCSV = (val: number) => {
|
const formatForCSV = (val: number) => {
|
||||||
return val.toLocaleString('en-US', {
|
return val.toLocaleString('de-DE', {
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
minimumFractionDigits: metric === 'sellOut' ? 2 : 0,
|
minimumFractionDigits: metric === 'sellOut' ? 2 : 0,
|
||||||
maximumFractionDigits: metric === 'sellOut' ? 2 : 0,
|
maximumFractionDigits: metric === 'sellOut' ? 2 : 0,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { GoogleGenAI } from "@google/genai";
|
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 process to avoid TypeScript errors without causing aggressive bundler shims
|
||||||
declare const process: any;
|
declare const process: any;
|
||||||
@@ -28,8 +28,8 @@ const getApiKey = (): string | undefined => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (val: number) => `€${val.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}`;
|
const formatCurrency = (val: number) => `€${val.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0})}`;
|
||||||
const formatUnits = (val: number) => `${val.toLocaleString()} units`;
|
const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`;
|
||||||
|
|
||||||
export const queryGemini = async (
|
export const queryGemini = async (
|
||||||
question: string,
|
question: string,
|
||||||
@@ -65,12 +65,12 @@ export const queryGemini = async (
|
|||||||
|
|
||||||
// 3. Top Movers (Growth Table) - Limit to Top 10
|
// 3. Top Movers (Growth Table) - Limit to Top 10
|
||||||
const growthSummary = context.topMovers.slice(0, 10).map(m =>
|
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');
|
).join('\n');
|
||||||
|
|
||||||
// 4. Declining Movers (Decline Table) - Limit to Top 10
|
// 4. Declining Movers (Decline Table) - Limit to Top 10
|
||||||
const declineSummary = context.bottomMovers.slice(0, 10).map(m =>
|
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');
|
).join('\n');
|
||||||
|
|
||||||
// 5. Product Lines Overview (Bar Charts) - Limit to Top 50 to save tokens but give depth
|
// 5. Product Lines Overview (Bar Charts) - Limit to Top 50 to save tokens but give depth
|
||||||
|
|||||||
Reference in New Issue
Block a user