mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:45: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
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.vercel
|
||||||
|
|||||||
@@ -181,7 +181,20 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
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));
|
const allSelected = filteredOptions.every(opt => selected.includes(opt));
|
||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
// Deselect all filtered
|
// Deselect all filtered
|
||||||
|
|||||||
+32
-64
@@ -417,7 +417,7 @@ export const processAdsCSV = (file: File): Promise<AdsRecord[]> => {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
Papa.parse(file, {
|
Papa.parse(file, {
|
||||||
header: false, // Index-based mapping
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
complete: (results: any) => {
|
complete: (results: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -427,40 +427,22 @@ export const processAdsCSV = (file: File): Promise<AdsRecord[]> => {
|
|||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const row = rows[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 countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']);
|
||||||
const c0 = String(row[0]).trim().toLowerCase();
|
const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']);
|
||||||
if (c0.includes('customer') || c0.includes('country') || c0.includes('marketplace')) continue;
|
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):
|
if (!asin || !countryRaw || weekRaw === undefined || weekRaw === '') continue;
|
||||||
// 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;
|
|
||||||
|
|
||||||
const weekNum = parseInt(String(weekRaw));
|
const weekNum = parseInt(String(weekRaw));
|
||||||
if (isNaN(weekNum) || weekNum < 1 || weekNum > 53) continue;
|
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];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
// Use header: 1 to get array of arrays (row-based)
|
// Use default format (header mapping) instead of array rows
|
||||||
const jsonData: any[][] = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" });
|
const jsonData: any[] = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
|
||||||
|
|
||||||
console.log(`Processing sheet ${sheetName}: ${jsonData.length} rows`);
|
console.log(`Processing sheet ${sheetName}: ${jsonData.length} rows`);
|
||||||
|
|
||||||
// Skip header row (index 0), process data rows
|
// No need to skip index 0 since headers are mapped as keys automatically
|
||||||
for (let i = 1; i < jsonData.length; i++) {
|
for (let i = 0; i < jsonData.length; i++) {
|
||||||
const row = jsonData[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:
|
const countryRaw = getColumnValue(row, ['country', 'marketplace', 'portfolio', 'customer', 'kunde']);
|
||||||
// A (0): Country
|
const weekRaw = getColumnValue(row, ['week', 'woche', 'semana']);
|
||||||
// B (1): Week
|
const asin = getColumnValue(row, ['asin']);
|
||||||
// C (2): ASIN
|
const costRaw = getColumnValue(row, ['cost', 'spend', 'ausgaben', 'gasto', 'coste', 'ad spend']);
|
||||||
// D (3): Cost
|
const clicksRaw = getColumnValue(row, ['clicks', 'klicks', 'clics']);
|
||||||
// E (4): Clicks
|
const impressionsRaw = getColumnValue(row, ['impressions', 'impresiones', 'imp']);
|
||||||
// F (5): Impressions
|
const cpcRaw = getColumnValue(row, ['cpc', 'cost-per-click', 'coste por clic']);
|
||||||
// G (6): CPC
|
const ctrRaw = getColumnValue(row, ['ctr', 'click-through rate', 'click-through-rate', 'click through rate']);
|
||||||
// H (7): CTR %
|
const acosRaw = getColumnValue(row, ['acos', 'advertising cost of sales', 'aCOS']);
|
||||||
// I (8): ACOS %
|
const conversionsRaw = getColumnValue(row, ['conversions', 'konversionen', 'orders', 'pedidos', 'total orders']);
|
||||||
// J (9): Conversions (30d)
|
const unitsRaw = getColumnValue(row, ['units', 'einheiten', 'unidades', 'units sold', 'total units']);
|
||||||
// K (10): Units (30d)
|
const salesRaw = getColumnValue(row, ['sales', 'umsatz', 'ventas', 'ad sales', 'total sales', 'sales (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];
|
|
||||||
|
|
||||||
// Skip if missing essential data
|
// Skip if missing essential data
|
||||||
if (!asin || !countryRaw || weekRaw === undefined || weekRaw === '') continue;
|
if (!asin || !countryRaw || weekRaw === undefined || weekRaw === '') continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user