mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 15:45:24 +02:00
feat: Integrate PapaParse for CSV handling
Adds papaparse as a dependency and updates the data processing service to use it for more robust CSV file parsing. This replaces manual CSV parsing logic with a dedicated library, improving reliability and handling of various CSV formats. Also renames the `MoversIcon` to `TrendingIcon` to better reflect its usage in indicating trending performance metrics.
This commit is contained in:
+123
-156
@@ -1,13 +1,10 @@
|
||||
|
||||
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { AggregatedData, LineGrowthMetric } from '../types'; // Updated import for LineGrowthMetric
|
||||
import { AggregatedData, GrowthMetric } from '../types';
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
LineChart, Line, Legend
|
||||
} from 'recharts';
|
||||
import { DownloadIcon } from './Icons'; // Import DownloadIcon
|
||||
|
||||
interface DashboardProps {
|
||||
data: AggregatedData;
|
||||
@@ -17,13 +14,7 @@ interface DashboardProps {
|
||||
const COLORS = ['#6366f1', '#ec4899', '#10b981', '#f59e0b', '#8b5cf6', '#0ea5e9'];
|
||||
|
||||
// Reusable Expandable Card Component
|
||||
export const ExpandableCard: React.FC<{
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onExport?: () => void; // Optional export function
|
||||
exportFileName?: string; // Optional export file name
|
||||
}> = ({ title, children, className, onExport, exportFileName }) => {
|
||||
const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; className?: string }> = ({ title, children, className }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => setIsExpanded(!isExpanded);
|
||||
@@ -58,14 +49,6 @@ export const ExpandableCard: React.FC<{
|
||||
|
||||
<div className="flex justify-between items-center mb-4 border-b border-slate-800 pb-4 shrink-0">
|
||||
<h3 className="text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
|
||||
{onExport && (
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors shadow-sm"
|
||||
>
|
||||
<DownloadIcon /> Export CSV
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-6 border border-border custom-scrollbar">
|
||||
{children}
|
||||
@@ -80,26 +63,15 @@ export const ExpandableCard: React.FC<{
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{onExport && (
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-indigo-400 transition-opacity"
|
||||
title={`Export ${exportFileName || title} to CSV`}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={toggleExpand}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-primary transition-opacity"
|
||||
title="Expand to Fullscreen"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleExpand}
|
||||
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-primary transition-opacity"
|
||||
title="Expand to Fullscreen"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
|
||||
</div>
|
||||
@@ -325,11 +297,11 @@ const ComparisonTooltip = ({ active, payload, label, metric }: any) => {
|
||||
|
||||
const GrowthTable: React.FC<{
|
||||
title: string;
|
||||
data: LineGrowthMetric[]; // Updated to LineGrowthMetric
|
||||
data: GrowthMetric[];
|
||||
type: 'growth' | 'decline';
|
||||
periods: { current: string; previous: string };
|
||||
}> = ({ title, data, type, periods }) => {
|
||||
const [sortConfig, setSortConfig] = useState<{ key: keyof LineGrowthMetric | null; direction: 'asc' | 'desc' }>({ key: null, direction: 'desc' });
|
||||
const [sortConfig, setSortConfig] = useState<{ key: keyof GrowthMetric | null; direction: 'asc' | 'desc' }>({ key: null, direction: 'desc' });
|
||||
|
||||
const sortedData = useMemo(() => {
|
||||
if (!sortConfig.key) return data;
|
||||
@@ -344,7 +316,7 @@ const GrowthTable: React.FC<{
|
||||
});
|
||||
}, [data, sortConfig]);
|
||||
|
||||
const requestSort = (key: keyof LineGrowthMetric) => {
|
||||
const requestSort = (key: keyof GrowthMetric) => {
|
||||
let direction: 'asc' | 'desc' = 'desc';
|
||||
// If already sorting by this key, toggle direction
|
||||
if (sortConfig.key === key && sortConfig.direction === 'desc') {
|
||||
@@ -353,7 +325,7 @@ const GrowthTable: React.FC<{
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
const getSortIndicator = (key: keyof LineGrowthMetric) => {
|
||||
const getSortIndicator = (key: keyof GrowthMetric) => {
|
||||
if (sortConfig.key !== key) {
|
||||
return (
|
||||
<svg className="w-2.5 h-2.5 ml-1 text-slate-600 opacity-0 group-hover:opacity-50" fill="currentColor" viewBox="0 0 20 20">
|
||||
@@ -372,112 +344,113 @@ const GrowthTable: React.FC<{
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-full relative">
|
||||
<table className="w-full text-left text-sm h-full border-separate border-spacing-0">
|
||||
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500 sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th
|
||||
className="px-4 py-3 bg-slate-950 min-w-[150px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('line')}
|
||||
>
|
||||
<div className="flex items-center">Product Line {getSortIndicator('line')}</div>
|
||||
</th>
|
||||
|
||||
{/* Sell Out Columns */}
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('previousYearSellOut')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||||
onClick={() => requestSort('currentYearSellOut')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('sellOutGrowthValue')}
|
||||
>
|
||||
<div className="flex items-center justify-end">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('sellOutGrowthPercentage')}
|
||||
>
|
||||
<div className="flex items-center justify-end">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
|
||||
</th>
|
||||
<ExpandableCard title={title} className="h-full">
|
||||
<div className="overflow-auto h-full relative">
|
||||
<table className="w-full text-left text-sm h-full border-separate border-spacing-0">
|
||||
<thead className="bg-slate-950 text-xs uppercase font-semibold text-slate-500 sticky top-0 z-10 shadow-sm">
|
||||
<tr>
|
||||
<th
|
||||
className="px-4 py-3 bg-slate-950 min-w-[150px] cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('line')}
|
||||
>
|
||||
<div className="flex items-center">Product Line {getSortIndicator('line')}</div>
|
||||
</th>
|
||||
|
||||
{/* Sell Out Columns */}
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('previousYearSellOut')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Sell Out {periods.previous} {getSortIndicator('previousYearSellOut')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||||
onClick={() => requestSort('currentYearSellOut')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Sell Out {periods.current} {getSortIndicator('currentYearSellOut')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('sellOutGrowthValue')}
|
||||
>
|
||||
<div className="flex items-center justify-end">SO Diff {getSortIndicator('sellOutGrowthValue')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('sellOutGrowthPercentage')}
|
||||
>
|
||||
<div className="flex items-center justify-end">SO Growth % {getSortIndicator('sellOutGrowthPercentage')}</div>
|
||||
</th>
|
||||
|
||||
{/* Units Columns */}
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('previousYearUnits')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units {periods.previous} {getSortIndicator('previousYearUnits')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||||
onClick={() => requestSort('currentYearUnits')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units {periods.current} {getSortIndicator('currentYearUnits')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('unitsGrowthValue')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units Diff {getSortIndicator('unitsGrowthValue')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('unitsGrowthPercentage')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units Growth % {getSortIndicator('unitsGrowthPercentage')}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border text-slate-300">
|
||||
{sortedData.length > 0 ? (
|
||||
sortedData.map((item, idx) => (
|
||||
<tr key={idx} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-2 font-medium">{item.line}</td>
|
||||
|
||||
{/* 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 font-medium text-slate-200">€{item.currentYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
||||
<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.toFixed(0)} */}
|
||||
</td>
|
||||
<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'}`}>
|
||||
{item.sellOutGrowthPercentage.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
{/* Units Columns */}
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-400 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('previousYearUnits')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units {periods.previous} {getSortIndicator('previousYearUnits')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 text-slate-200 cursor-pointer hover:text-white group select-none transition-colors"
|
||||
onClick={() => requestSort('currentYearUnits')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units {periods.current} {getSortIndicator('currentYearUnits')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('unitsGrowthValue')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units Diff {getSortIndicator('unitsGrowthValue')}</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-right bg-slate-950 cursor-pointer hover:text-slate-300 group select-none transition-colors"
|
||||
onClick={() => requestSort('unitsGrowthPercentage')}
|
||||
>
|
||||
<div className="flex items-center justify-end">Units Growth % {getSortIndicator('unitsGrowthPercentage')}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border text-slate-300">
|
||||
{sortedData.length > 0 ? (
|
||||
sortedData.map((item, idx) => (
|
||||
<tr key={idx} className="hover:bg-slate-800/50">
|
||||
<td className="px-4 py-2 font-medium">{item.line}</td>
|
||||
|
||||
{/* 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 font-medium text-slate-200">€{item.currentYearSellOut.toLocaleString(undefined, {maximumFractionDigits: 0})}</td>
|
||||
<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})}
|
||||
</td>
|
||||
<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'}`}>
|
||||
{item.sellOutGrowthPercentage.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* 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 font-medium text-slate-200">{item.currentYearUnits.toLocaleString()}</td>
|
||||
<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()}
|
||||
</td>
|
||||
<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'}`}>
|
||||
{item.unitsGrowthPercentage.toFixed(1)}%
|
||||
</span>
|
||||
{/* 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 font-medium text-slate-200">{item.currentYearUnits.toLocaleString()}</td>
|
||||
<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()}
|
||||
</td>
|
||||
<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'}`}>
|
||||
{item.unitsGrowthPercentage.toFixed(1)}%
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-4 py-6 text-center text-slate-500 italic">
|
||||
Insufficient data to calculate {type}. (Select at least 2 distinct years/periods)
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-4 py-6 text-center text-slate-500 italic">
|
||||
Insufficient data to calculate {type}. (Select at least 2 distinct years/periods)
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ExpandableCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -572,30 +545,24 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
||||
</ExpandableCard>
|
||||
|
||||
{/* Growth Table */}
|
||||
<ExpandableCard
|
||||
title={contextData ? "Fastest Growing Lines (Full Line Context)" : "Fastest Growing Lines (YoY - €)"}
|
||||
className="h-[400px]" // Provide a default height for the card
|
||||
>
|
||||
<div className="h-72">
|
||||
<GrowthTable
|
||||
title={contextData ? "Fastest Growing Lines (Full Line Context)" : "Fastest Growing Lines (YoY - €)"}
|
||||
data={displayData.topMovers}
|
||||
type="growth"
|
||||
periods={displayData.comparisonPeriods}
|
||||
/>
|
||||
</ExpandableCard>
|
||||
</div>
|
||||
|
||||
{/* Decline Table */}
|
||||
<ExpandableCard
|
||||
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines (YoY - €)"}
|
||||
className="h-[400px]" // Provide a default height for the card
|
||||
>
|
||||
<div className="h-72">
|
||||
<GrowthTable
|
||||
title={contextData ? "Declining Lines (Full Line Context)" : "Declining Lines (YoY - €)"}
|
||||
data={displayData.bottomMovers}
|
||||
type="decline"
|
||||
periods={displayData.comparisonPeriods}
|
||||
/>
|
||||
</ExpandableCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
@@ -709,4 +676,4 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
|
||||
Reference in New Issue
Block a user