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 (
0
} + return ( +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 && ( +{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()} - -
- ))} -{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')} + +
+ ))} +{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 ( -{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 ( +| requestSort('line')}
>
Product Line {getSortIndicator('line')}
|
-
+
{/* Sell Out Columns */}
- 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<{ | {/* Units Columns */} - 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} | - + {/* Sell Out Columns */} -€{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<{ | {/* Units Columns */} -{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)}% | @@ -455,225 +455,225 @@ const GrowthTable: React.FC<{ } const Dashboard: React.FC- {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') : '-'} | {/* 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