mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:05:24 +02:00
Integrate stock availability data and StockBadge component across all views
This commit is contained in:
@@ -6,7 +6,7 @@ import FilterBar from './components/FilterBar';
|
|||||||
import AIChat from './components/AIChat';
|
import AIChat from './components/AIChat';
|
||||||
import CrazeLogo from './components/CrazeLogo';
|
import CrazeLogo from './components/CrazeLogo';
|
||||||
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types';
|
||||||
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData } from './services/dataProcessor';
|
import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData, processStockExcel } from './services/dataProcessor';
|
||||||
import { queryGemini } from './services/geminiService';
|
import { queryGemini } from './services/geminiService';
|
||||||
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons';
|
||||||
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage';
|
||||||
@@ -48,6 +48,7 @@ const App: React.FC = () => {
|
|||||||
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);
|
||||||
|
const [stockMap, setStockMap] = useState<Map<string, number>>(new Map());
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
const [isDataModalOpen, setIsDataModalOpen] = useState(false);
|
||||||
@@ -124,18 +125,6 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleTrafficFetch = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
console.log('[App] Fetching traffic from /api/fetch-traffic...');
|
|
||||||
const response = await fetch('/api/fetch-traffic');
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch Traffic: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
const data = await processTrafficExcel(buffer);
|
|
||||||
|
|
||||||
setTrafficData(data);
|
setTrafficData(data);
|
||||||
console.log('[App] Successfully loaded', data.length, 'traffic records');
|
console.log('[App] Successfully loaded', data.length, 'traffic records');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -143,6 +132,22 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleStockFetch = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
console.log('[App] Fetching stock from /api/fetch-stock...');
|
||||||
|
const response = await fetch('/api/fetch-stock');
|
||||||
|
if (!response.ok) throw new Error(`Failed to fetch stock: ${response.status}`);
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const data = await processStockExcel(buffer);
|
||||||
|
setStockMap(data);
|
||||||
|
console.log('[App] Successfully loaded stock for', data.size, 'normalized SKUs');
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch/parse Stock", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => {
|
const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => {
|
||||||
try {
|
try {
|
||||||
const isUK = activeFilters.customer.includes('Amazon UK');
|
const isUK = activeFilters.customer.includes('Amazon UK');
|
||||||
@@ -265,7 +270,11 @@ const App: React.FC = () => {
|
|||||||
console.log("Fetching Traffic data...");
|
console.log("Fetching Traffic data...");
|
||||||
handleTrafficFetch();
|
handleTrafficFetch();
|
||||||
|
|
||||||
// 1d. Fetch Forecast data
|
// 1d. Always fetch Stock data
|
||||||
|
console.log("Fetching Stock data...");
|
||||||
|
handleStockFetch();
|
||||||
|
|
||||||
|
// 1e. Fetch Forecast data
|
||||||
handleForecastFetch(cachedData || [], cachedData || [], filters);
|
handleForecastFetch(cachedData || [], cachedData || [], filters);
|
||||||
};
|
};
|
||||||
initApp();
|
initApp();
|
||||||
@@ -575,20 +584,32 @@ const App: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
|
<FilterBar filters={filters} onFilterChange={handleFilterChange} options={filterOptions} />
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
{view === 'dashboard' && (
|
{view === 'dashboard' && <Dashboard data={aggregatedData} filterState={filters} adsData={filteredAdsData} stockMap={stockMap} />}
|
||||||
<Dashboard
|
{view === 'table' && (
|
||||||
data={aggregatedData}
|
|
||||||
contextData={contextAggregatedData}
|
|
||||||
adsData={filteredAdsData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
{view === 'table' && <DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} />}
|
<DataGrid data={combinedAdsData} hasCustomerFilter={filters.customer.length > 0} adsData={filteredAdsData} stockMap={stockMap} />
|
||||||
{view === 'weekly' && <WeeklyGrid data={combinedAdsData} top50Ranking={top50Ranking2025} onDrillDown={handleSkuDrillDown} />}
|
|
||||||
{view === 'movers' && <TopMovers data={filteredData} />}
|
|
||||||
{view === 'ads' && <AdsPerformance data={combinedAdsData} filters={filters} top50Ranking={top50Ranking2025} />}
|
|
||||||
{view === 'forecast' && <ForecastView data={forecastData} filters={filters} top50Ranking={top50Ranking2025} />}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{view === 'weekly' && (
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<WeeklyGrid data={filteredAdsData.length > 0 ? mergeSalesAndAdsData(filteredData, filteredAdsData, undefined, trafficData) : filteredData} onDrillDown={handleSkuDrillDown} stockMap={stockMap} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{view === 'movers' && (
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<TopMovers data={filteredData} stockMap={stockMap} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{view === 'ads' && (
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<AdsPerformance data={filteredAdsData} filters={filters} onSkuDrillDown={handleSkuDrillDown} stockMap={stockMap} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
{view === 'forecast' && (
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<ForecastView data={forecastData} stockMap={stockMap} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
||||||
|
// CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[fetch-stock] Reading Stock file from local storage...');
|
||||||
|
// Try multiple possible paths to be robust
|
||||||
|
const pathsToTry = [
|
||||||
|
path.join(process.cwd(), 'Item Availability.xlsx'),
|
||||||
|
path.join(process.cwd(), 'public', 'Item Availability.xlsx'),
|
||||||
|
path.join('/Users/christianvidalwolf/github/CrazeAnalytix', 'Item Availability.xlsx')
|
||||||
|
];
|
||||||
|
|
||||||
|
let buffer = null;
|
||||||
|
let foundPath = '';
|
||||||
|
|
||||||
|
for (const p of pathsToTry) {
|
||||||
|
console.log(`[fetch-stock] Checking path: ${p}`);
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
buffer = fs.readFileSync(p);
|
||||||
|
foundPath = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
throw new Error(`Stock file 'Item Availability.xlsx' not found in any of: ${pathsToTry.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[fetch-stock] Successfully read Stock Excel from ${foundPath}, size: ${buffer.byteLength}`);
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
res.status(200).send(buffer);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[fetch-stock] Error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { CombinedKPIs, FilterState } from '../types';
|
import { CombinedKPIs, FilterState } from '../types';
|
||||||
import { DownloadIcon } from './Icons';
|
import { DownloadIcon } from './Icons';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
|
|
||||||
interface AdsPerformanceProps {
|
interface AdsPerformanceProps {
|
||||||
data: CombinedKPIs[];
|
data: CombinedKPIs[];
|
||||||
@@ -10,6 +11,7 @@ interface AdsPerformanceProps {
|
|||||||
eu: Map<string, number>;
|
eu: Map<string, number>;
|
||||||
uk: Map<string, number>;
|
uk: Map<string, number>;
|
||||||
};
|
};
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
type SortKey = keyof CombinedKPIs | 'acos' | 'roas' | 'tacos' | 'ctr' | 'cpc' | 'cvrUnits';
|
||||||
@@ -34,7 +36,7 @@ const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'bl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ranking }) => {
|
const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ranking, stockMap }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||||
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
||||||
@@ -404,6 +406,9 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({ data, filters, top50Ran
|
|||||||
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
<Top50Badge key={i} rank={r.rank} label={r.label} theme={r.theme} />
|
||||||
))}
|
))}
|
||||||
<span className="text-white font-bold text-sm tracking-tight">{row.asin}</span>
|
<span className="text-white font-bold text-sm tracking-tight">{row.asin}</span>
|
||||||
|
{stockMap && (
|
||||||
|
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] uppercase font-black text-indigo-400 tracking-tighter">SKU: {row.sku}</span>
|
<span className="text-[10px] uppercase font-black text-indigo-400 tracking-tighter">SKU: {row.sku}</span>
|
||||||
<span className="text-[10px] text-slate-400 truncate max-w-[220px] leading-tight" title={row.title}>{row.title}</span>
|
<span className="text-[10px] text-slate-400 truncate max-w-[220px] leading-tight" title={row.title}>{row.title}</span>
|
||||||
|
|||||||
+10
-1
@@ -5,11 +5,13 @@ import {
|
|||||||
import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs } from '../types';
|
import { SalesRecord, PivotRow, AdsRecord, CombinedKPIs } from '../types';
|
||||||
import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForComparisonTimeSeries, applyPanEUGrouping } from '../services/dataProcessor';
|
||||||
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
|
|
||||||
interface DataGridProps {
|
interface DataGridProps {
|
||||||
data: SalesRecord[] | CombinedKPIs[];
|
data: SalesRecord[] | CombinedKPIs[];
|
||||||
hasCustomerFilter: boolean;
|
hasCustomerFilter: boolean;
|
||||||
adsData?: AdsRecord[];
|
adsData?: AdsRecord[];
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortConfig = {
|
type SortConfig = {
|
||||||
@@ -226,7 +228,7 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode;
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData = [] }) => {
|
const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData, stockMap }) => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' });
|
const [sortConfig, setSortConfig] = useState<SortConfig>({ key: null, direction: 'desc' });
|
||||||
const [showChart, setShowChart] = useState(true);
|
const [showChart, setShowChart] = useState(true);
|
||||||
@@ -1081,6 +1083,13 @@ const DataGrid: React.FC<DataGridProps> = ({ data, hasCustomerFilter, adsData =
|
|||||||
<td key={dim} className="px-4 py-3 font-medium text-slate-200 break-words max-w-xs">
|
<td key={dim} className="px-4 py-3 font-medium text-slate-200 break-words max-w-xs">
|
||||||
{dim === 'title'
|
{dim === 'title'
|
||||||
? <div className="line-clamp-2" title={row.title}>{row.title || '-'}</div>
|
? <div className="line-clamp-2" title={row.title}>{row.title || '-'}</div>
|
||||||
|
: (dim === 'sku' || dim === 'asin')
|
||||||
|
? <div className="flex items-center gap-2">
|
||||||
|
<span>{(row[dim as keyof PivotRow] as string) || '-'}</span>
|
||||||
|
{stockMap && dim === 'sku' && (
|
||||||
|
<StockBadge stock={stockMap.get((row[dim] as string)?.replace(/(DE|EN)$/i, ''))} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
: (row[dim as keyof PivotRow] as string) || '-'
|
: (row[dim as keyof PivotRow] as string) || '-'
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { ProductForecastData, FilterState } from '../types';
|
import { ProductForecastData, FilterState } from '../types';
|
||||||
import { DownloadIcon } from './Icons';
|
import { DownloadIcon } from './Icons';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
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
|
||||||
@@ -13,7 +15,9 @@ interface ForecastViewProps {
|
|||||||
top50Ranking?: {
|
top50Ranking?: {
|
||||||
eu: Map<string, number>;
|
eu: Map<string, number>;
|
||||||
uk: Map<string, number>;
|
uk: Map<string, number>;
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
};
|
};
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
@@ -29,7 +33,7 @@ const Top50Badge: React.FC<{ rank: number; label: string; theme?: 'amber' | 'ind
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking }) => {
|
const ForecastView: React.FC<ForecastViewProps> = ({ data, filters, top50Ranking, stockMap, stockMap }) => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
const [showOnlyTop50, setShowOnlyTop50] = useState(false);
|
||||||
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
const [top50Mode, setTop50Mode] = useState<'eu' | 'uk'>('eu');
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface StockBadgeProps {
|
||||||
|
stock: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StockBadge: React.FC<StockBadgeProps> = ({ stock }) => {
|
||||||
|
if (stock === undefined) return null;
|
||||||
|
|
||||||
|
const isLow = stock < 50;
|
||||||
|
const isOut = stock <= 0;
|
||||||
|
|
||||||
|
let colorClass = "bg-emerald-500/20 text-emerald-400 border-emerald-500/30";
|
||||||
|
let icon = "📦";
|
||||||
|
|
||||||
|
if (isOut) {
|
||||||
|
colorClass = "bg-rose-500/20 text-rose-400 border-rose-500/30";
|
||||||
|
icon = "❌";
|
||||||
|
} else if (isLow) {
|
||||||
|
colorClass = "bg-amber-500/20 text-amber-400 border-amber-500/30";
|
||||||
|
icon = "⚠️";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-black border uppercase tracking-tighter shadow-sm ${colorClass}`}
|
||||||
|
title={`Total Stock (GMBH): ${stock}`}
|
||||||
|
>
|
||||||
|
<span className="text-xs">{icon}</span>
|
||||||
|
<span>{stock.toLocaleString('de-DE')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { CombinedKPIs } from '../types';
|
import { CombinedKPIs } from '../types';
|
||||||
import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor';
|
import { pivotWeeklySalesData, WeeklyPivotRow } from '../services/dataProcessor';
|
||||||
|
import { StockBadge } from './StockBadge';
|
||||||
|
|
||||||
interface WeeklyGridProps {
|
interface WeeklyGridProps {
|
||||||
data: CombinedKPIs[];
|
data: CombinedKPIs[];
|
||||||
@@ -10,6 +11,7 @@ interface WeeklyGridProps {
|
|||||||
uk: Map<string, number>;
|
uk: Map<string, number>;
|
||||||
};
|
};
|
||||||
onDrillDown?: (sku: string) => void;
|
onDrillDown?: (sku: string) => void;
|
||||||
|
stockMap?: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortConfig = {
|
type SortConfig = {
|
||||||
@@ -51,7 +53,7 @@ const Top50Badge: React.FC<{ rank: number; label?: string; theme?: 'amber' | 'bl
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown }) => {
|
const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown, stockMap }) => {
|
||||||
// Pivot data - memoized
|
// Pivot data - memoized
|
||||||
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]);
|
||||||
|
|
||||||
@@ -515,6 +517,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps> = ({ data, top50Ranking, onDrillDown
|
|||||||
{row.sku || '-'}
|
{row.sku || '-'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
|
<span className="text-[10px] font-bold text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded border border-white/5">{row.asin}</span>
|
||||||
|
{stockMap && (
|
||||||
|
<StockBadge stock={stockMap.get(row.sku?.replace(/(DE|EN)$/i, ''))} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] text-white/70 truncate w-[210px] leading-tight mb-1" title={row.title}>{row.title}</span>
|
<span className="text-[11px] text-white/70 truncate w-[210px] leading-tight mb-1" title={row.title}>{row.title}</span>
|
||||||
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
<span className="text-[9px] text-fuchsia-400/80 font-bold uppercase tracking-widest">{row.line}</span>
|
||||||
|
|||||||
@@ -1619,3 +1619,40 @@ export const calculateForecastViewData = (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise<Map<string, number>> => {
|
||||||
|
try {
|
||||||
|
const arrayBuffer = fileOrBuffer instanceof File
|
||||||
|
? await fileOrBuffer.arrayBuffer()
|
||||||
|
: fileOrBuffer;
|
||||||
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
const sheetName = workbook.SheetNames[0];
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
|
||||||
|
|
||||||
|
const stockMap = new Map<string, number>();
|
||||||
|
|
||||||
|
// Skip headers (index 0)
|
||||||
|
for (let i = 1; i < jsonData.length; i++) {
|
||||||
|
const row = jsonData[i];
|
||||||
|
const rawSku = String(row[0] || '').trim();
|
||||||
|
if (!rawSku) continue;
|
||||||
|
|
||||||
|
// Normalize SKU: Remove trailing EN or DE
|
||||||
|
const normalizedSku = rawSku.replace(/(DE|EN)$/i, '');
|
||||||
|
|
||||||
|
// User requested Column I which is index 8 (After Assembly Orders GMBH)
|
||||||
|
const stockValue = Number(row[8] || 0);
|
||||||
|
|
||||||
|
if (!isNaN(stockValue)) {
|
||||||
|
const current = stockMap.get(normalizedSku) || 0;
|
||||||
|
stockMap.set(normalizedSku, current + stockValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stockMap;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing Stock Excel:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user