mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 11:55:22 +02:00
feat: add weekly sales tab with WoW growth
This commit is contained in:
Binary file not shown.
@@ -3,6 +3,7 @@ import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
|||||||
import FileUpload from './components/FileUpload';
|
import FileUpload from './components/FileUpload';
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
import DataGrid from './components/DataGrid';
|
import DataGrid from './components/DataGrid';
|
||||||
|
import WeeklyGrid from './components/WeeklyGrid';
|
||||||
import TopMovers from './components/TopMovers';
|
import TopMovers from './components/TopMovers';
|
||||||
// import AdvertisingDashboard from './components/AdvertisingDashboard'; // Removed
|
// import AdvertisingDashboard from './components/AdvertisingDashboard'; // Removed
|
||||||
import FilterBar from './components/FilterBar';
|
import FilterBar from './components/FilterBar';
|
||||||
@@ -31,7 +32,7 @@ const App: React.FC = () => {
|
|||||||
const [adsData, setAdsData] = useState<AdsRecord[]>([]); // New Ads State
|
const [adsData, setAdsData] = useState<AdsRecord[]>([]); // New Ads State
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
const [view, setView] = useState<'dashboard' | 'table' | 'movers' | 'ads'>('dashboard'); // Added 'ads' view
|
const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'movers' | 'ads'>('dashboard'); // Added 'weekly' view
|
||||||
const [isChatOpen, setIsChatOpen] = useState(false);
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||||
const [activeUrl, setActiveUrl] = useState<string | null>(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL);
|
const [activeUrl, setActiveUrl] = useState<string | null>(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL);
|
||||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||||
@@ -377,6 +378,13 @@ const App: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<TableIcon /> <span className="hidden sm:inline">Grid</span>
|
<TableIcon /> <span className="hidden sm:inline">Grid</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setView('weekly')}
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
|
${view === 'weekly' ? 'bg-slate-800 text-white shadow-sm ring-1 ring-white/10' : 'text-slate-400 hover:text-white hover:bg-slate-800/50'}`}
|
||||||
|
>
|
||||||
|
<TrendingIcon /> <span className="hidden sm:inline">Weekly Sales</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView('movers')}
|
onClick={() => setView('movers')}
|
||||||
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all
|
||||||
@@ -410,6 +418,7 @@ const App: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
||||||
|
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} />}
|
||||||
{view === 'movers' && <TopMovers data={filteredData} />}
|
{view === 'movers' && <TopMovers data={filteredData} />}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { CombinedKPIs } from '../types';
|
||||||
|
import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor';
|
||||||
|
|
||||||
|
interface WeeklyGridProps {
|
||||||
|
data: CombinedKPIs[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data }) => {
|
||||||
|
const { rows, weeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||||
|
|
||||||
|
// Calculate totals per week
|
||||||
|
const weekTotals = useMemo(() => {
|
||||||
|
const totals: { [weekKey: string]: number } = {};
|
||||||
|
weeks.forEach(week => {
|
||||||
|
totals[week] = rows.reduce((sum, row) => sum + (row.unitsByWeek[week] || 0), 0);
|
||||||
|
});
|
||||||
|
return totals;
|
||||||
|
}, [rows, weeks]);
|
||||||
|
|
||||||
|
const renderGrowth = (current: number, previous: number) => {
|
||||||
|
if (!previous || previous === 0) return null;
|
||||||
|
const pct = ((current - previous) / previous) * 100;
|
||||||
|
const isPositive = pct >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`text-[10px] flex items-center gap-0.5 mt-0.5 font-bold ${isPositive ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||||
|
{isPositive ? '▲' : '▼'}{Math.abs(pct).toFixed(0)}%
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-900/50 border border-white/10 rounded-xl overflow-hidden animate-fade-in shadow-2xl">
|
||||||
|
<div className="overflow-x-auto overflow-y-auto max-h-[calc(100vh-280px)]">
|
||||||
|
<table className="w-full text-left border-collapse min-w-[1000px]">
|
||||||
|
<thead className="sticky top-0 z-20">
|
||||||
|
<tr className="bg-slate-900 border-b border-white/10">
|
||||||
|
<th className="p-4 text-xs font-black text-slate-400 uppercase tracking-widest sticky left-0 z-30 bg-slate-900 border-r border-white/5 w-[300px]">Product Details</th>
|
||||||
|
{weeks.map(week => (
|
||||||
|
<th key={week} className="p-4 text-xs font-black text-slate-400 uppercase tracking-widest text-center border-r border-white/5">
|
||||||
|
{week.split('-')[1]}/{week.split('-')[0].slice(-2)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
<tr className="bg-slate-800/80 backdrop-blur-md border-b border-white/10 italic">
|
||||||
|
<th className="p-3 text-sm font-black text-white sticky left-0 z-30 bg-slate-800 border-r border-white/5">Totals</th>
|
||||||
|
{weeks.map((week, idx) => (
|
||||||
|
<th key={week} className="p-3 text-sm font-black text-white text-center border-r border-white/5">
|
||||||
|
{weekTotals[week]?.toLocaleString('de-DE')}
|
||||||
|
{renderGrowth(weekTotals[week], weekTotals[weeks[idx + 1]])}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/5">
|
||||||
|
{rows.map((row) => (
|
||||||
|
<tr key={row.id} className="hover:bg-white/[0.02] transition-colors group">
|
||||||
|
<td className="p-4 sticky left-0 z-10 bg-slate-900 md:bg-slate-900/90 backdrop-blur-sm group-hover:bg-slate-800 border-r border-white/5">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs font-black text-indigo-400 uppercase tracking-wider mb-0.5">{row.sku}</span>
|
||||||
|
<span className="text-[11px] text-white/80 line-clamp-1 group-hover:line-clamp-none transition-all">{row.title}</span>
|
||||||
|
<span className="text-[10px] text-fuchsia-400/70 font-bold mt-1 uppercase tracking-tighter">{row.line}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{weeks.map((week, idx) => {
|
||||||
|
const val = row.unitsByWeek[week] || 0;
|
||||||
|
const prevVal = row.unitsByWeek[weeks[idx + 1]] || 0;
|
||||||
|
return (
|
||||||
|
<td key={week} className="p-4 text-center border-r border-white/5 align-top">
|
||||||
|
<span className={`text-sm font-black ${val > 0 ? 'text-white' : 'text-slate-600'}`}>
|
||||||
|
{val > 0 ? val.toLocaleString('de-DE') : '-'}
|
||||||
|
</span>
|
||||||
|
{val > 0 && renderGrowth(val, prevVal)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WeeklyGrid;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browser = await puppeteer.launch({ headless: 'new' });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Set viewport for presentation
|
||||||
|
await page.setViewport({ width: 1280, height: 720 });
|
||||||
|
|
||||||
|
// Load the HTML file
|
||||||
|
const filePath = path.join(__dirname, 'presentacion.html');
|
||||||
|
await page.goto(`file://${filePath}`, { waitUntil: 'networkidle0' });
|
||||||
|
|
||||||
|
// Wait for images to load
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Generate PDF
|
||||||
|
await page.pdf({
|
||||||
|
path: path.join(__dirname, 'CrazeAnalytix_Presentation.pdf'),
|
||||||
|
format: 'A4',
|
||||||
|
landscape: true,
|
||||||
|
printBackground: true,
|
||||||
|
margin: { top: '0', right: '0', bottom: '0', left: '0' }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('PDF generated successfully!');
|
||||||
|
await browser.close();
|
||||||
|
})();
|
||||||
+1132
File diff suppressed because it is too large
Load Diff
@@ -509,6 +509,7 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
marketplace: sale.customer,
|
marketplace: sale.customer,
|
||||||
customer: sale.customer,
|
customer: sale.customer,
|
||||||
month: sale.month,
|
month: sale.month,
|
||||||
|
week: sale.week || 0, // Preserve week info
|
||||||
year: sale.year,
|
year: sale.year,
|
||||||
asin: sale.asin,
|
asin: sale.asin,
|
||||||
title: sale.title,
|
title: sale.title,
|
||||||
@@ -1253,3 +1254,57 @@ export const aggregateForComparisonTimeSeries = (data: SalesRecord[]): Compariso
|
|||||||
})
|
})
|
||||||
.sort((a, b) => a.week - b.week);
|
.sort((a, b) => a.week - b.week);
|
||||||
};
|
};
|
||||||
|
export interface WeeklyPivotRow {
|
||||||
|
id: string;
|
||||||
|
sku: string;
|
||||||
|
title: string;
|
||||||
|
asin: string;
|
||||||
|
line: string;
|
||||||
|
customer: string;
|
||||||
|
unitsByWeek: { [weekKey: string]: number }; // Key: "YYYY-WW"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pivotWeeklySalesData = (data: CombinedKPIs[]): {
|
||||||
|
rows: WeeklyPivotRow[],
|
||||||
|
weeks: string[]
|
||||||
|
} => {
|
||||||
|
// 1. Identify all unique weeks and sort descending (YYYY-WW)
|
||||||
|
const weekKeys = new Set<string>();
|
||||||
|
data.forEach(d => {
|
||||||
|
if (d.week) {
|
||||||
|
const weekKey = `${d.year}-${String(d.week).padStart(2, '0')}`;
|
||||||
|
weekKeys.add(weekKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const sortedWeeks = Array.from(weekKeys).sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
|
const map = new Map<string, WeeklyPivotRow>();
|
||||||
|
|
||||||
|
data.forEach(record => {
|
||||||
|
const key = record.sku || record.asin || `${record.title}-${record.line}`;
|
||||||
|
if (!key) return;
|
||||||
|
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, {
|
||||||
|
id: key,
|
||||||
|
sku: record.sku || '',
|
||||||
|
title: record.title || '',
|
||||||
|
asin: record.asin || '',
|
||||||
|
line: record.line || '',
|
||||||
|
customer: record.customer || record.marketplace || '',
|
||||||
|
unitsByWeek: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = map.get(key)!;
|
||||||
|
if (record.week) {
|
||||||
|
const weekKey = `${record.year}-${String(record.week).padStart(2, '0')}`;
|
||||||
|
row.unitsByWeek[weekKey] = (row.unitsByWeek[weekKey] || 0) + record.unitsTotal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: Array.from(map.values()),
|
||||||
|
weeks: sortedWeeks
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export interface CombinedKPIs {
|
|||||||
marketplace: string;
|
marketplace: string;
|
||||||
customer: string; // Added for consistency with SalesRecord
|
customer: string; // Added for consistency with SalesRecord
|
||||||
month: string;
|
month: string;
|
||||||
|
week: number; // Added for weekly analysis
|
||||||
year: number;
|
year: number;
|
||||||
asin: string;
|
asin: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user