mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 10:05:23 +02:00
feat: bulk ASIN selection via space-separated paste in filters
Allow pasting multiple ASINs separated by spaces in any MultiSelectDropdown search field and pressing Enter to auto-select all matching options. Also switch ads CSV/Excel parsing to header-based column mapping for robustness across different file formats. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
69eafe8335
commit
487b8e105e
@@ -26,3 +26,4 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vercel
|
||||
|
||||
@@ -181,7 +181,20 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (filteredOptions.length > 0) {
|
||||
// Check if input contains multiple space-separated terms (bulk paste)
|
||||
const terms = searchTerm.trim().split(/\s+/).filter(Boolean);
|
||||
if (terms.length > 1) {
|
||||
// Bulk select: match each term against options (case-insensitive)
|
||||
const termsLower = terms.map(t => t.toLowerCase());
|
||||
const matched = options.filter(opt =>
|
||||
termsLower.some(t => opt.toLowerCase() === t || opt.toLowerCase().includes(t))
|
||||
);
|
||||
if (matched.length > 0) {
|
||||
onChange(Array.from(new Set([...selected, ...matched])));
|
||||
}
|
||||
setIsOpen(false);
|
||||
setSearchTerm('');
|
||||
} else if (filteredOptions.length > 0) {
|
||||
const allSelected = filteredOptions.every(opt => selected.includes(opt));
|
||||
if (allSelected) {
|
||||
// Deselect all filtered
|
||||
|
||||
+32
-64
@@ -417,7 +417,7 @@ export const processAdsCSV = (file: File): Promise<AdsRecord[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
Papa.parse(file, {
|
||||
header: false, // Index-based mapping
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
complete: (results: any) => {
|
||||
try {
|
||||
@@ -427,40 +427,22 @@ export const processAdsCSV = (file: File): Promise<AdsRecord[]> => {
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const row = rows[i];
|
||||
if (!Array.isArray(row) || row.length < 12) continue;
|
||||
if (!row || Object.keys(row).length < 5) continue;
|
||||
|
||||
// Check header row (Column A: Customer or Country)
|
||||
const c0 = String(row[0]).trim().toLowerCase();
|
||||
if (c0.includes('customer') || c0.includes('country') || c0.includes('marketplace')) continue;
|
||||
const countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']);
|
||||
const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']);
|
||||
const asin = getColumnValue(row, ['asin']);
|
||||
const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', 'ad spend']);
|
||||
const clicksRaw = getColumnValue(row, ['clicks', 'klicks', 'clics']);
|
||||
const impressionsRaw = getColumnValue(row, ['impressions', 'impresiones', 'imp']);
|
||||
const cpcRaw = getColumnValue(row, ['cpc', 'cost-per-click', 'coste por clic']);
|
||||
const ctrRaw = getColumnValue(row, ['ctr', 'click-through rate', 'click-through-rate', 'click through rate']);
|
||||
const acosRaw = getColumnValue(row, ['acos', 'advertising cost of sales', 'aCOS']);
|
||||
const conversionsRaw = getColumnValue(row, ['conversions', 'konversionen', 'orders', 'pedidos', 'total orders']);
|
||||
const unitsRaw = getColumnValue(row, ['units', 'einheiten', 'unidades', 'units sold', 'total units']);
|
||||
const salesRaw = getColumnValue(row, ['sales', 'umsatz', 'ventas', 'ad sales', 'total sales', 'sales (30d)']);
|
||||
|
||||
// Column mapping for CSV (same as Excel):
|
||||
// A (0): Country
|
||||
// B (1): Week
|
||||
// C (2): ASIN
|
||||
// D (3): Cost
|
||||
// E (4): Clicks
|
||||
// F (5): Impressions
|
||||
// G (6): CPC
|
||||
// H (7): CTR %
|
||||
// I (8): ACOS %
|
||||
// J (9): Conversions (30d)
|
||||
// K (10): Units (30d)
|
||||
// L (11): Sales (30d)
|
||||
|
||||
const countryRaw = row[0];
|
||||
const weekRaw = row[1];
|
||||
const asin = row[2];
|
||||
const costRaw = row[3];
|
||||
const clicksRaw = row[4];
|
||||
const impressionsRaw = row[5];
|
||||
const cpcRaw = row[6];
|
||||
const ctrRaw = row[7];
|
||||
const acosRaw = row[8];
|
||||
const conversionsRaw = row[9];
|
||||
const unitsRaw = row[10];
|
||||
const salesRaw = row[11];
|
||||
|
||||
if (!asin || !countryRaw || weekRaw === undefined) continue;
|
||||
if (!asin || !countryRaw || weekRaw === undefined || weekRaw === '') continue;
|
||||
|
||||
const weekNum = parseInt(String(weekRaw));
|
||||
if (isNaN(weekNum) || weekNum < 1 || weekNum > 53) continue;
|
||||
@@ -511,42 +493,28 @@ export const processAdsExcel = async (fileOrBuffer: File | ArrayBuffer): Promise
|
||||
}
|
||||
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
// Use header: 1 to get array of arrays (row-based)
|
||||
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" });
|
||||
// Use default format (header mapping) instead of array rows
|
||||
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
|
||||
|
||||
console.log(`Processing sheet ${sheetName}: ${jsonData.length} rows`);
|
||||
|
||||
// Skip header row (index 0), process data rows
|
||||
for (let i = 1; i < jsonData.length; i++) {
|
||||
// No need to skip index 0 since headers are mapped as keys automatically
|
||||
for (let i = 0; i < jsonData.length; i++) {
|
||||
const row = jsonData[i];
|
||||
if (!row || row.length < 12) continue;
|
||||
if (!row || Object.keys(row).length < 5) continue;
|
||||
|
||||
// Column mapping for Ads Weekly.xlsx:
|
||||
// A (0): Country
|
||||
// B (1): Week
|
||||
// C (2): ASIN
|
||||
// D (3): Cost
|
||||
// E (4): Clicks
|
||||
// F (5): Impressions
|
||||
// G (6): CPC
|
||||
// H (7): CTR %
|
||||
// I (8): ACOS %
|
||||
// J (9): Conversions (30d)
|
||||
// K (10): Units (30d)
|
||||
// L (11): Sales (30d)
|
||||
|
||||
const countryRaw = row[0];
|
||||
const weekRaw = row[1];
|
||||
const asin = row[2];
|
||||
const costRaw = row[3];
|
||||
const clicksRaw = row[4];
|
||||
const impressionsRaw = row[5];
|
||||
const cpcRaw = row[6];
|
||||
const ctrRaw = row[7];
|
||||
const acosRaw = row[8];
|
||||
const conversionsRaw = row[9];
|
||||
const unitsRaw = row[10];
|
||||
const salesRaw = row[11];
|
||||
const countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']);
|
||||
const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']);
|
||||
const asin = getColumnValue(row, ['asin']);
|
||||
const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', 'ad spend']);
|
||||
const clicksRaw = getColumnValue(row, ['clicks', 'klicks', 'clics']);
|
||||
const impressionsRaw = getColumnValue(row, ['impressions', 'impresiones', 'imp']);
|
||||
const cpcRaw = getColumnValue(row, ['cpc', 'cost-per-click', 'coste por clic']);
|
||||
const ctrRaw = getColumnValue(row, ['ctr', 'click-through rate', 'click-through-rate', 'click through rate']);
|
||||
const acosRaw = getColumnValue(row, ['acos', 'advertising cost of sales', 'aCOS']);
|
||||
const conversionsRaw = getColumnValue(row, ['conversions', 'konversionen', 'orders', 'pedidos', 'total orders']);
|
||||
const unitsRaw = getColumnValue(row, ['units', 'einheiten', 'unidades', 'units sold', 'total units']);
|
||||
const salesRaw = getColumnValue(row, ['sales', 'umsatz', 'ventas', 'ad sales', 'total sales', 'sales (30d)']);
|
||||
|
||||
// Skip if missing essential data
|
||||
if (!asin || !countryRaw || weekRaw === undefined || weekRaw === '') continue;
|
||||
|
||||
Reference in New Issue
Block a user