From 6704093a426c656490ab5229438082bf177213cf Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Wed, 17 Jun 2026 16:10:00 +0200 Subject: [PATCH] feat: add multi-product search by SKU/ASIN/title to Forecast view --- App.tsx | 2 +- components/ForecastView.tsx | 77 +++++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/App.tsx b/App.tsx index 1af60f5..1186cf9 100644 --- a/App.tsx +++ b/App.tsx @@ -898,7 +898,7 @@ const App: React.FC = () => { }>
- +
diff --git a/components/ForecastView.tsx b/components/ForecastView.tsx index 6b5b4f0..e8e9a76 100644 --- a/components/ForecastView.tsx +++ b/components/ForecastView.tsx @@ -23,6 +23,8 @@ interface BudgetRow { interface ComparisonRow { sku: string; description: string; + asins: string[]; + title: string; actualUnits: number; periodForecast: number; budgetYearTotal: number; @@ -182,7 +184,11 @@ const ComparisonTableRow: React.FC<{ row: ComparisonRow }> = React.memo(({ row } // ── Main Component ─────────────────────────────────────────────────────────── -const ForecastView: React.FC = () => { +interface ForecastViewProps { + asinMetadata?: Map; +} + +const ForecastView: React.FC = ({ asinMetadata }) => { const [sellInData, setSellInData] = useState | null>(null); const [budgetData, setBudgetData] = useState | null>(null); const [sellInFileName, setSellInFileName] = useState(''); @@ -191,6 +197,7 @@ const ForecastView: React.FC = () => { const [cutoffMonth, setCutoffMonth] = useState(4); const [sortAsc, setSortAsc] = useState(true); const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); const sellInInputRef = useRef(null); const budgetInputRef = useRef(null); @@ -217,6 +224,23 @@ const ForecastView: React.FC = () => { reader.readAsArrayBuffer(file); }, []); + const skuMetadata = useMemo(() => { + const map = new Map(); + if (!asinMetadata) return map; + asinMetadata.forEach((meta, asin) => { + const existing = map.get(meta.sku); + if (existing) { + existing.asins.push(asin); + if (meta.title && meta.title.length > (existing.title?.length || 0)) { + existing.title = meta.title; + } + } else { + map.set(meta.sku, { asins: [asin], title: meta.title || '' }); + } + }); + return map; + }, [asinMetadata]); + const comparisonRows = useMemo((): ComparisonRow[] => { if (!budgetData) return []; const rows: ComparisonRow[] = []; @@ -231,9 +255,12 @@ const ForecastView: React.FC = () => { const budgetDiscrepancy = sellIn != null && Math.abs(budgetYearTotal - sellInBudget) > 1; const pctPeriod = periodForecast > 0 ? (actualUnits / periodForecast) * 100 : 0; const pctAnnual = budgetYearTotal > 0 ? (actualUnits / budgetYearTotal) * 100 : 0; + const meta = skuMetadata.get(budgetRow.sku); rows.push({ sku: budgetRow.sku, description: budgetRow.description, + asins: meta?.asins ?? [], + title: meta?.title ?? '', actualUnits, periodForecast, budgetYearTotal, @@ -244,7 +271,25 @@ const ForecastView: React.FC = () => { }); rows.sort((a, b) => sortAsc ? a.pctPeriod - b.pctPeriod : b.pctPeriod - a.pctPeriod); return rows; - }, [budgetData, sellInData, cutoffMonth, sortAsc]); + }, [budgetData, sellInData, cutoffMonth, sortAsc, skuMetadata]); + + const filteredRows = useMemo(() => { + if (!searchQuery.trim()) return comparisonRows; + const terms = searchQuery + .split(/[,;|\n]+/) + .map(t => t.trim().toLowerCase()) + .filter(Boolean); + if (terms.length === 0) return comparisonRows; + return comparisonRows.filter(row => { + return terms.some(term => { + if (row.sku.toLowerCase().includes(term)) return true; + if (row.description.toLowerCase().includes(term)) return true; + if (row.title.toLowerCase().includes(term)) return true; + if (row.asins.some(asin => asin.toLowerCase().includes(term))) return true; + return false; + }); + }); + }, [comparisonRows, searchQuery]); const kpis = useMemo(() => { const totalActual = comparisonRows.reduce((s, r) => s + r.actualUnits, 0); @@ -405,6 +450,17 @@ const ForecastView: React.FC = () => { {/* ── Comparison Table ── */} {isReady && (
+
+
+ setSearchQuery(e.target.value)} + placeholder="Buscar por SKU, ASIN, o título... (separar varios con coma)" + className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" + /> +
+
@@ -436,16 +492,21 @@ const ForecastView: React.FC = () => { - {comparisonRows.map(row => ( + {filteredRows.map(row => ( ))} + {filteredRows.length === 0 && ( +
+ {searchQuery.trim() ? 'No se encontraron productos con esos criterios' : 'Sin datos de comparación'} +
+ )} + {searchQuery.trim() && filteredRows.length > 0 && ( +
+ {filteredRows.length} de {comparisonRows.length} SKUs +
+ )}
- {comparisonRows.length === 0 && ( -
- Sin datos de comparación -
- )}
)}