diff --git a/App.tsx b/App.tsx index 637a892..7822498 100644 --- a/App.tsx +++ b/App.tsx @@ -6,7 +6,7 @@ import FilterBar from './components/FilterBar'; import AIChat from './components/AIChat'; import CrazeLogo from './components/CrazeLogo'; import { SalesRecord, FilterState, AggregatedData, AdsRecord, TrafficRecord, ForecastRecord, ProductForecastData } from './types'; -import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData, processStockExcel } from './services/dataProcessor'; +import { processCSV, filterData, filterAdsData, aggregateData, getUniqueValues, processAdsCSV, processAdsExcel, processTrafficExcel, mergeSalesAndAdsData, processForecastExcel, calculateForecastViewData, processStockExcel, processVendorStockExcel } from './services/dataProcessor'; import { queryGemini } from './services/geminiService'; import { ChartIcon, TableIcon, UploadIcon, DownloadIcon, CloseIcon, TrendingIcon, MegaphoneIcon } from './components/Icons'; import { loadSalesData, saveSalesData, clearSalesData, loadAdsData, saveAdsData, clearAdsData } from './services/storage'; @@ -49,6 +49,7 @@ const App: React.FC = () => { const [activeUrl, setActiveUrl] = useState(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL); const [lastUpdated, setLastUpdated] = useState(null); const [stockMap, setStockMap] = useState>(new Map()); + const [vendorStockMap, setVendorStockMap] = useState>(new Map()); // Modal State const [isDataModalOpen, setIsDataModalOpen] = useState(false); @@ -143,8 +144,8 @@ 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'); + console.log('[App] Fetching stock from /Item Availability.xlsx...'); + const response = await fetch('/Item Availability.xlsx'); if (!response.ok) throw new Error(`Failed to fetch stock: ${response.status}`); const buffer = await response.arrayBuffer(); @@ -156,6 +157,21 @@ const App: React.FC = () => { } }, []); + const handleVendorStockFetch = useCallback(async () => { + try { + console.log('[App] Fetching vendor stock from /Vendor Stock.xlsx...'); + const response = await fetch('/Vendor Stock.xlsx'); + if (!response.ok) throw new Error(`Failed to fetch vendor stock: ${response.status}`); + + const buffer = await response.arrayBuffer(); + const data = await processVendorStockExcel(buffer); + setVendorStockMap(data); + console.log('[App] Successfully loaded vendor stock for', data.size, 'ASINs'); + } catch (error) { + console.error("Failed to fetch/parse Vendor Stock", error); + } + }, []); + const handleForecastFetch = useCallback(async (sales: SalesRecord[], globalSales: SalesRecord[], activeFilters: FilterState) => { try { @@ -283,6 +299,7 @@ const App: React.FC = () => { // 1d. Always fetch Stock data console.log("Fetching Stock data..."); handleStockFetch(); + handleVendorStockFetch(); // 1e. Fetch Forecast data handleForecastFetch(cachedData || [], cachedData || [], filters); @@ -528,7 +545,7 @@ const App: React.FC = () => {
{/* Force Sync Action */}
@@ -88,7 +92,7 @@ const AdsRow: React.FC<{ ); }); -const AdsPerformance: React.FC = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, top50Mode }) => { +const AdsPerformance: React.FC = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' }>({ @@ -393,7 +397,7 @@ const AdsPerformance: React.FC = ({ data, filters, top50Ran {filteredAndSorted.map((p) => ( - + ))} diff --git a/components/DataGrid.tsx b/components/DataGrid.tsx index 77c9e37..9229e94 100644 --- a/components/DataGrid.tsx +++ b/components/DataGrid.tsx @@ -7,6 +7,7 @@ import { pivotSalesData, generateXLSX, aggregateForTimeSeries, aggregateForCompa import { DownloadIcon, FunnelIcon, CloseIcon, ChartIcon, TrendingIcon } from './Icons'; import { StockBadge } from './StockBadge'; import { Top50Badge } from './Top50Badge'; +import { VendorStockBadge } from './VendorStockBadge'; interface DataGridProps { data: SalesRecord[] | CombinedKPIs[]; @@ -18,6 +19,7 @@ interface DataGridProps { uk: Map; }; top50Mode: 'eu' | 'uk'; + vendorStockMap?: Map; } type SortConfig = { @@ -234,7 +236,7 @@ const ExpandableChartCard: React.FC<{ title: string; children: React.ReactNode; }; -const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap, top50Ranking, top50Mode }) => { +const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, stockMap, vendorStockMap, top50Ranking, top50Mode }) => { const [currentPage, setCurrentPage] = useState(1); const [sortConfig, setSortConfig] = useState({ key: null, direction: 'desc' }); const [showChart, setShowChart] = useState(true); @@ -1090,7 +1092,10 @@ const DataGrid: React.FC = ({ data, hasCustomerFilter, adsData, s ?
{row.title || '-'} {stockMap && ( - + <> + + + )}
: dim === 'asin' || dim === 'sku' diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index bd072f0..cd6853e 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -4,6 +4,7 @@ import { ProductForecastData, FilterState, CombinedKPIs } from '../types'; import { DownloadIcon, FunnelIcon, TrendingIcon, ChartIcon } from './Icons'; import { StockBadge } from './StockBadge'; import { Top50Badge } from './Top50Badge'; +import { VendorStockBadge } from './VendorStockBadge'; import { InColumnStockFilter } from './InColumnStockFilter'; import { PAN_EU_COUNTRIES } from '../services/dataProcessor'; import { @@ -19,6 +20,7 @@ interface ForecastViewProps { uk: Map; }; stockMap?: Map; + vendorStockMap?: Map; stockFilter: string[]; onStockFilterChange: (newFilters: string[]) => void; top50Mode: 'eu' | 'uk'; @@ -32,7 +34,8 @@ const ForecastRow: React.FC<{ top50Ranking?: { eu: Map; uk: Map }; top50Mode: 'eu' | 'uk'; stockMap?: Map; -}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap }) => { + vendorStockMap?: Map; +}> = React.memo(({ item, activeMonths, top50Ranking, top50Mode, stockMap, vendorStockMap }) => { const asin = item.asin.trim().toUpperCase(); const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; @@ -71,6 +74,7 @@ const ForecastRow: React.FC<{ {stockMap && ( )} + @@ -112,7 +116,7 @@ const ForecastRow: React.FC<{ ); }); -const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, stockFilter, onStockFilterChange, top50Mode }) => { +const ForecastView: React.FC = ({ data, filters, top50Ranking, stockMap, vendorStockMap, stockFilter, onStockFilterChange, top50Mode }) => { const [searchTerm, setSearchTerm] = useState(''); const [showOnlyTop50, setShowOnlyTop50] = useState(false); const [displayCount, setDisplayCount] = useState(50); @@ -246,6 +250,7 @@ const ForecastView: React.FC = ({ data, filters, top50Ranking top50Ranking={top50Ranking} top50Mode={top50Mode} stockMap={stockMap} + vendorStockMap={vendorStockMap} /> ))} diff --git a/components/VendorStockBadge.tsx b/components/VendorStockBadge.tsx new file mode 100644 index 0000000..b19ba46 --- /dev/null +++ b/components/VendorStockBadge.tsx @@ -0,0 +1,62 @@ + +import React from 'react'; + +interface VendorStockBadgeProps { + asin: string; + vendorStockMap?: Map; + mode: 'eu' | 'uk'; +} + +export const VendorStockBadge: React.FC = ({ asin, vendorStockMap, mode }) => { + if (!vendorStockMap || !asin) return null; + + const stockData = vendorStockMap.get(asin.toUpperCase()); + if (!stockData) return null; + + const stock = mode === 'uk' ? stockData.uk : stockData.eu; + + // As per user request: "etiqueta serĂ¡ de color gris claro" + // Using a light gray/slate theme that fits the dashboard's premium look + const themeClasses = "bg-slate-200 text-slate-800 border-slate-300 shadow-sm"; + + return ( +
+ + + + + + +
+ Vendor + {stock.toLocaleString('de-DE')} +
+
+ ); +}; diff --git a/components/WeeklyGrid.tsx b/components/WeeklyGrid.tsx index 94841ef..1705b24 100644 --- a/components/WeeklyGrid.tsx +++ b/components/WeeklyGrid.tsx @@ -5,6 +5,7 @@ import { pivotWeeklySalesData, WeeklyPivotRow, PAN_EU_COUNTRIES } from '../servi import { StockBadge } from './StockBadge'; import { InColumnStockFilter } from './InColumnStockFilter'; import { Top50Badge } from './Top50Badge'; +import { VendorStockBadge } from './VendorStockBadge'; interface WeeklyGridProps { data: CombinedKPIs[]; @@ -17,6 +18,7 @@ interface WeeklyGridProps { stockFilter: string[]; onStockFilterChange: (newFilters: string[]) => void; customerFilters: string[]; + vendorStockMap?: Map; } type SortConfig = { @@ -48,7 +50,8 @@ const WeeklyRow: React.FC<{ sortConfig: SortConfig; renderGrowth: (current: number, previous: number) => React.ReactNode; customerFilters: string[]; -}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters }) => { + vendorStockMap?: Map; +}> = React.memo(({ row, weeks, onDrillDown, stockMap, top50Ranking, top50Mode, sortConfig, renderGrowth, customerFilters, vendorStockMap }) => { const ranks: { rank: number; label: string; theme: 'amber' | 'blue' | 'indigo' }[] = []; const asin = row.asin.trim().toUpperCase(); @@ -85,6 +88,7 @@ const WeeklyRow: React.FC<{ {stockMap && ( )} + {row.line} @@ -129,7 +133,7 @@ const WeeklyRow: React.FC<{ ); }); -const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown, stockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => { +const WeeklyGrid: React.FC = ({ data, top50Ranking, onDrillDown, stockMap, vendorStockMap, stockFilter, onStockFilterChange, customerFilters, top50Mode }) => { // Pivot data - memoized const { rows, weeks: allWeeks } = useMemo(() => pivotWeeklySalesData(data), [data]); @@ -545,6 +549,7 @@ const WeeklyGrid: React.FC = ({ da weeks={weeks} onDrillDown={onDrillDown} stockMap={stockMap} + vendorStockMap={vendorStockMap} top50Ranking={top50Ranking} top50Mode={top50Mode} sortConfig={sortConfig} diff --git a/Item Availability.xlsx b/public/Item Availability.xlsx similarity index 100% rename from Item Availability.xlsx rename to public/Item Availability.xlsx diff --git a/public/Vendor Stock.xlsx b/public/Vendor Stock.xlsx new file mode 100644 index 0000000..b8a8f49 Binary files /dev/null and b/public/Vendor Stock.xlsx differ diff --git a/services/dataProcessor.ts b/services/dataProcessor.ts index 4739520..9a1e959 100644 --- a/services/dataProcessor.ts +++ b/services/dataProcessor.ts @@ -1694,6 +1694,68 @@ export const calculateForecastViewData = ( }); }; +export const processVendorStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise> => { + 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 vendorStockMap = new Map(); + + // Find header row (it contains "ASIN") + let headerRowIndex = -1; + for (let i = 0; i < Math.min(jsonData.length, 20); i++) { + if (jsonData[i] && jsonData[i].includes('ASIN')) { + headerRowIndex = i; + break; + } + } + + if (headerRowIndex === -1) { + console.warn("Could not find header row in Vendor Stock Excel"); + return vendorStockMap; + } + + // Skip headers + // A (0): ASIN + // D (3): Marketplace (Country) + // P (15): Sellable on hands units + for (let i = headerRowIndex + 1; i < jsonData.length; i++) { + const row = jsonData[i]; + if (!row || row.length < 16) continue; + + const asin = String(row[0] || '').trim().toUpperCase(); + const marketplace = String(row[3] || '').trim().toLowerCase(); + const stockValue = parseUnits(String(row[15] || '0')); + + if (!asin) continue; + + if (!vendorStockMap.has(asin)) { + vendorStockMap.set(asin, { eu: 0, uk: 0 }); + } + + const current = vendorStockMap.get(asin)!; + + // Map to UK or EU + if (marketplace.includes('uk') || marketplace.includes('kingdom') || marketplace === 'gb') { + current.uk += stockValue; + } else { + // Assume everything else is Pan-EU for now if it's not UK + current.eu += stockValue; + } + } + + return vendorStockMap; + } catch (error) { + console.error("Error processing Vendor Stock Excel:", error); + throw error; + } +}; + export const processStockExcel = async (fileOrBuffer: File | ArrayBuffer): Promise> => { try { const arrayBuffer = fileOrBuffer instanceof File