mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:45:23 +02:00
feat: restrict data to Amazon customers only (DE/FR/ES/IT/UK/SC)
This commit is contained in:
+371
-363
@@ -4,43 +4,43 @@ import Papa from 'papaparse';
|
|||||||
|
|
||||||
// Helper to parse currency values handling both EU (1.234,56) and US/Standard (1,234.56 or 1234.56) formats
|
// Helper to parse currency values handling both EU (1.234,56) and US/Standard (1,234.56 or 1234.56) formats
|
||||||
const parseCurrency = (value: string): number => {
|
const parseCurrency = (value: string): number => {
|
||||||
if (!value) return 0;
|
if (!value) return 0;
|
||||||
|
|
||||||
// Remove currency symbol and whitespace
|
// Remove currency symbol and whitespace
|
||||||
let clean = value.replace(/[€$£\s]/g, '').trim();
|
let clean = value.replace(/[€$£\s]/g, '').trim();
|
||||||
|
|
||||||
// HEURISTIC:
|
// HEURISTIC:
|
||||||
// If it contains a comma, we assume it's likely European format (Decimal separator)
|
// If it contains a comma, we assume it's likely European format (Decimal separator)
|
||||||
// UNLESS it also contains a dot and the comma is before the dot (e.g. 1,000.50 - US format)
|
// UNLESS it also contains a dot and the comma is before the dot (e.g. 1,000.50 - US format)
|
||||||
// But given the context (DE data), comma is usually decimal.
|
// But given the context (DE data), comma is usually decimal.
|
||||||
|
|
||||||
// Case A: European Format (e.g., "277.179,09" or "50,00" or "263,83")
|
// Case A: European Format (e.g., "277.179,09" or "50,00" or "263,83")
|
||||||
if (clean.includes(',') && !clean.includes('.')) {
|
if (clean.includes(',') && !clean.includes('.')) {
|
||||||
// Likely EU decimal without thousands or with thousands implicitly handled
|
// Likely EU decimal without thousands or with thousands implicitly handled
|
||||||
// e.g. "263,83" -> "263.83"
|
// e.g. "263,83" -> "263.83"
|
||||||
clean = clean.replace(',', '.');
|
clean = clean.replace(',', '.');
|
||||||
return parseFloat(clean);
|
return parseFloat(clean);
|
||||||
}
|
}
|
||||||
else if (clean.includes(',') && clean.includes('.')) {
|
else if (clean.includes(',') && clean.includes('.')) {
|
||||||
// Mixed: 1.234,56 -> EU
|
// Mixed: 1.234,56 -> EU
|
||||||
if (clean.indexOf(',') > clean.indexOf('.')) {
|
if (clean.indexOf(',') > clean.indexOf('.')) {
|
||||||
clean = clean.replace(/\./g, '').replace(',', '.');
|
clean = clean.replace(/\./g, '').replace(',', '.');
|
||||||
} else {
|
} else {
|
||||||
// 1,234.56 -> US
|
// 1,234.56 -> US
|
||||||
clean = clean.replace(/,/g, '');
|
clean = clean.replace(/,/g, '');
|
||||||
}
|
}
|
||||||
return parseFloat(clean);
|
return parseFloat(clean);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case B: Standard/US Format or Clean Number (e.g. "277179.09" or "1000")
|
// Case B: Standard/US Format or Clean Number (e.g. "277179.09" or "1000")
|
||||||
clean = clean.replace(/,/g, ''); // Remove commas just in case
|
clean = clean.replace(/,/g, ''); // Remove commas just in case
|
||||||
const num = parseFloat(clean);
|
const num = parseFloat(clean);
|
||||||
|
|
||||||
return isNaN(num) ? 0 : num;
|
return isNaN(num) ? 0 : num;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseUnits = (value: string): number => {
|
const parseUnits = (value: string): number => {
|
||||||
if(!value) return 0;
|
if (!value) return 0;
|
||||||
// Remove dots (thousands separators in EU) and commas (thousands in US) just to be safe for integers
|
// Remove dots (thousands separators in EU) and commas (thousands in US) just to be safe for integers
|
||||||
const clean = value.replace(/[\.,]/g, '');
|
const clean = value.replace(/[\.,]/g, '');
|
||||||
const num = parseInt(clean, 10);
|
const num = parseInt(clean, 10);
|
||||||
@@ -98,8 +98,8 @@ const normalizeMonth = (rawMonth: string): string => {
|
|||||||
|
|
||||||
const numMatch = m.match(/^(\d{1,2})([^\d]|$)/);
|
const numMatch = m.match(/^(\d{1,2})([^\d]|$)/);
|
||||||
if (numMatch) {
|
if (numMatch) {
|
||||||
const num = parseInt(numMatch[1]);
|
const num = parseInt(numMatch[1]);
|
||||||
if (num >= 1 && num <= 12) return MONTH_ORDER[num - 1];
|
if (num >= 1 && num <= 12) return MONTH_ORDER[num - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fallback: Extract first 3 letters and capitalize
|
// 3. Fallback: Extract first 3 letters and capitalize
|
||||||
@@ -121,7 +121,7 @@ const normalizeMonth = (rawMonth: string): string => {
|
|||||||
// This part is likely fallback for Sales Data records
|
// This part is likely fallback for Sales Data records
|
||||||
const letters = m.replace(/[^a-z]/g, '');
|
const letters = m.replace(/[^a-z]/g, '');
|
||||||
if (letters && MONTH_MAP[letters]) {
|
if (letters && MONTH_MAP[letters]) {
|
||||||
return `${MONTH_MAP[letters]}-${y}`;
|
return `${MONTH_MAP[letters]}-${y}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +151,14 @@ const getColumnValue = (row: any, aliases: string[]): string => {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
// Allowed Customers Whitelist
|
||||||
|
const ALLOWED_CUSTOMERS = ['Amazon DE', 'Amazon FR', 'Amazon ES', 'Amazon IT', 'Amazon UK', 'Amazon SC'];
|
||||||
|
|
||||||
|
const isAllowedCustomer = (customer: string): boolean => {
|
||||||
|
if (!customer) return false;
|
||||||
|
const normCustomer = customer.trim().toLowerCase();
|
||||||
|
return ALLOWED_CUSTOMERS.some(allowed => allowed.toLowerCase() === normCustomer);
|
||||||
|
};
|
||||||
|
|
||||||
// --- SALES / SELL OUT MAPPING ---
|
// --- SALES / SELL OUT MAPPING ---
|
||||||
|
|
||||||
@@ -190,43 +198,43 @@ const mapRowToRecord = (row: any, index: number): SalesRecord => {
|
|||||||
const sellOutRaw = getColumnValue(row, ['AMOUNT', 'Sell Out', 'SellOut', 'Revenue', 'Sales', 'Turnover']);
|
const sellOutRaw = getColumnValue(row, ['AMOUNT', 'Sell Out', 'SellOut', 'Revenue', 'Sales', 'Turnover']);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `row-${index}`,
|
id: `row-${index}`,
|
||||||
customer,
|
customer,
|
||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
week,
|
week,
|
||||||
asin,
|
asin,
|
||||||
sku,
|
sku,
|
||||||
title,
|
title,
|
||||||
articleName,
|
articleName,
|
||||||
units: parseUnits(unitsRaw),
|
units: parseUnits(unitsRaw),
|
||||||
sellOut: parseCurrency(sellOutRaw),
|
sellOut: parseCurrency(sellOutRaw),
|
||||||
line
|
line
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processCSV = (fileOrContent: File | string): Promise<SalesRecord[]> => {
|
export const processCSV = (fileOrContent: File | string): Promise<SalesRecord[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
Papa.parse(fileOrContent, {
|
Papa.parse(fileOrContent, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
complete: (results: any) => {
|
complete: (results: any) => {
|
||||||
try {
|
try {
|
||||||
const data: SalesRecord[] = results.data.map((row: any, index: number) => {
|
const data: SalesRecord[] = results.data.map((row: any, index: number) => {
|
||||||
return mapRowToRecord(row, index);
|
return mapRowToRecord(row, index);
|
||||||
})
|
})
|
||||||
// Relaxed filtering: Only exclude rows with absolutely no year info even after backfill
|
// Filter: Valid Year AND Allowed Customer
|
||||||
.filter((r: SalesRecord) => r.year > 0);
|
.filter((r: SalesRecord) => r.year > 0 && isAllowedCustomer(r.customer));
|
||||||
|
|
||||||
resolve(data);
|
resolve(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error: any) => reject(error)
|
error: (error: any) => reject(error)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processExcel = async (file: File): Promise<SalesRecord[]> => {
|
export const processExcel = async (file: File): Promise<SalesRecord[]> => {
|
||||||
@@ -240,8 +248,8 @@ export const processExcel = async (file: File): Promise<SalesRecord[]> => {
|
|||||||
const data: SalesRecord[] = jsonData.map((row: any, index: number) => {
|
const data: SalesRecord[] = jsonData.map((row: any, index: number) => {
|
||||||
return mapRowToRecord(row, index);
|
return mapRowToRecord(row, index);
|
||||||
})
|
})
|
||||||
// Relaxed filtering
|
// Filter: Valid Year AND Allowed Customer
|
||||||
.filter((r: SalesRecord) => r.year > 0);
|
.filter((r: SalesRecord) => r.year > 0 && isAllowedCustomer(r.customer));
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -264,81 +272,81 @@ const mapCountryToMarketplace = (country: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const processAdsCSV = (file: File): Promise<AdsRecord[]> => {
|
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: false, // Index-based mapping
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
complete: (results: any) => {
|
complete: (results: any) => {
|
||||||
try {
|
try {
|
||||||
const data: AdsRecord[] = [];
|
const data: AdsRecord[] = [];
|
||||||
const rows = results.data;
|
const rows = results.data;
|
||||||
const len = rows.length;
|
const len = rows.length;
|
||||||
|
|
||||||
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 < 13) continue;
|
if (!Array.isArray(row) || row.length < 13) continue;
|
||||||
|
|
||||||
// Check header row (Column A: Customer or Country)
|
// Check header row (Column A: Customer or Country)
|
||||||
const c0 = String(row[0]).trim().toLowerCase();
|
const c0 = String(row[0]).trim().toLowerCase();
|
||||||
if (c0.includes('customer') || c0.includes('country') || c0.includes('marketplace')) continue;
|
if (c0.includes('customer') || c0.includes('country') || c0.includes('marketplace')) continue;
|
||||||
|
|
||||||
// A (0): Customer/Marketplace
|
// A (0): Customer/Marketplace
|
||||||
// B (1): Month (Can be "may", "01", or Excel serial "45544")
|
// B (1): Month (Can be "may", "01", or Excel serial "45544")
|
||||||
// C (2): Year
|
// C (2): Year
|
||||||
// D (3): ASIN
|
// D (3): ASIN
|
||||||
// E (4): Ad Spend
|
// E (4): Ad Spend
|
||||||
// F (5): Clicks
|
// F (5): Clicks
|
||||||
// G (6): Impressions
|
// G (6): Impressions
|
||||||
// ...
|
// ...
|
||||||
// L (11): Units (Attributed)
|
// L (11): Units (Attributed)
|
||||||
// M (12): Sales (Sell Out)
|
// M (12): Sales (Sell Out)
|
||||||
|
|
||||||
const countryRaw = row[0];
|
const countryRaw = row[0];
|
||||||
const monthRaw = row[1];
|
const monthRaw = row[1];
|
||||||
const yearRaw = row[2];
|
const yearRaw = row[2];
|
||||||
const asin = row[3];
|
const asin = row[3];
|
||||||
const costRaw = row[4];
|
const costRaw = row[4];
|
||||||
const clicksRaw = row[5];
|
const clicksRaw = row[5];
|
||||||
const impressionsRaw = row[6];
|
const impressionsRaw = row[6];
|
||||||
|
|
||||||
const unitsRaw = row[11]; // L
|
const unitsRaw = row[11]; // L
|
||||||
const salesRaw = row[12]; // M
|
const salesRaw = row[12]; // M
|
||||||
|
|
||||||
if (!asin || !countryRaw) continue;
|
if (!asin || !countryRaw) continue;
|
||||||
|
|
||||||
// Construct Normalized Month-Year String (e.g., "May-24")
|
// Construct Normalized Month-Year String (e.g., "May-24")
|
||||||
const pureMonth = normalizeMonth(String(monthRaw)); // Returns "May"
|
const pureMonth = normalizeMonth(String(monthRaw)); // Returns "May"
|
||||||
let yearShort = '';
|
let yearShort = '';
|
||||||
if (yearRaw) {
|
if (yearRaw) {
|
||||||
yearShort = String(yearRaw).trim().replace(/[,.]/g, '').slice(-2); // "2024" -> "24", handle "2,024"
|
yearShort = String(yearRaw).trim().replace(/[,.]/g, '').slice(-2); // "2024" -> "24", handle "2,024"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid double year if normalizeMonth already extracted it (rare for serial numbers but possible for strings)
|
||||||
|
let finalMonthStr = pureMonth;
|
||||||
|
if (!finalMonthStr.includes('-') && yearShort) {
|
||||||
|
finalMonthStr = `${pureMonth}-${yearShort}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({
|
||||||
|
country: mapCountryToMarketplace(String(countryRaw)),
|
||||||
|
month: finalMonthStr,
|
||||||
|
asin: String(asin).trim(),
|
||||||
|
cost: parseCurrency(String(costRaw)),
|
||||||
|
clicks: parseUnits(String(clicksRaw)),
|
||||||
|
impressions: parseUnits(String(impressionsRaw)),
|
||||||
|
attributedSales30d: parseCurrency(String(salesRaw)),
|
||||||
|
attributedUnits30d: parseUnits(String(unitsRaw)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Avoid double year if normalizeMonth already extracted it (rare for serial numbers but possible for strings)
|
error: (error: any) => reject(error)
|
||||||
let finalMonthStr = pureMonth;
|
});
|
||||||
if (!finalMonthStr.includes('-') && yearShort) {
|
|
||||||
finalMonthStr = `${pureMonth}-${yearShort}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.push({
|
|
||||||
country: mapCountryToMarketplace(String(countryRaw)),
|
|
||||||
month: finalMonthStr,
|
|
||||||
asin: String(asin).trim(),
|
|
||||||
cost: parseCurrency(String(costRaw)),
|
|
||||||
clicks: parseUnits(String(clicksRaw)),
|
|
||||||
impressions: parseUnits(String(impressionsRaw)),
|
|
||||||
attributedSales30d: parseCurrency(String(salesRaw)),
|
|
||||||
attributedUnits30d: parseUnits(String(unitsRaw)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
resolve(data);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error: any) => reject(error)
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processAdsExcel = async (file: File): Promise<AdsRecord[]> => {
|
export const processAdsExcel = async (file: File): Promise<AdsRecord[]> => {
|
||||||
@@ -379,11 +387,11 @@ export const processAdsExcel = async (file: File): Promise<AdsRecord[]> => {
|
|||||||
|
|
||||||
if (!asin || !countryRaw) return null;
|
if (!asin || !countryRaw) return null;
|
||||||
|
|
||||||
// Construct Normalized Month-Year String
|
// Construct Normalized Month-Year String
|
||||||
const pureMonth = normalizeMonth(String(monthRaw));
|
const pureMonth = normalizeMonth(String(monthRaw));
|
||||||
let yearShort = '';
|
let yearShort = '';
|
||||||
if (yearRaw) {
|
if (yearRaw) {
|
||||||
yearShort = String(yearRaw).trim().slice(-2);
|
yearShort = String(yearRaw).trim().slice(-2);
|
||||||
}
|
}
|
||||||
const finalMonthStr = yearShort ? `${pureMonth}-${yearShort}` : pureMonth;
|
const finalMonthStr = yearShort ? `${pureMonth}-${yearShort}` : pureMonth;
|
||||||
|
|
||||||
@@ -506,99 +514,99 @@ export const mergeSalesAndAdsData = (salesData: SalesRecord[], adsData: AdsRecor
|
|||||||
// --- EXISTING HELPERS ---
|
// --- EXISTING HELPERS ---
|
||||||
|
|
||||||
export const filterData = (data: SalesRecord[], filters: FilterState): SalesRecord[] => {
|
export const filterData = (data: SalesRecord[], filters: FilterState): SalesRecord[] => {
|
||||||
return data.filter(item => {
|
return data.filter(item => {
|
||||||
// 1. Month Logic: Handle "Apr-23" matching "Apr" filter
|
// 1. Month Logic: Handle "Apr-23" matching "Apr" filter
|
||||||
const recordMonth = item.month; // e.g. "Apr-23"
|
const recordMonth = item.month; // e.g. "Apr-23"
|
||||||
const pureMonth = recordMonth.split('-')[0]; // "Apr"
|
const pureMonth = recordMonth.split('-')[0]; // "Apr"
|
||||||
|
|
||||||
// 2. Filter Checks
|
// 2. Filter Checks
|
||||||
const customerMatch = filters.customer.length === 0 || filters.customer.includes(item.customer);
|
const customerMatch = filters.customer.length === 0 || filters.customer.includes(item.customer);
|
||||||
const yearMatch = filters.year.length === 0 || filters.year.includes(item.year.toString());
|
const yearMatch = filters.year.length === 0 || filters.year.includes(item.year.toString());
|
||||||
|
|
||||||
// Check match against pure month ("Apr") OR full month ("Apr-23") just in case filters evolve
|
// Check match against pure month ("Apr") OR full month ("Apr-23") just in case filters evolve
|
||||||
const monthMatch = filters.month.length === 0 || filters.month.includes(pureMonth) || filters.month.includes(recordMonth);
|
const monthMatch = filters.month.length === 0 || filters.month.includes(pureMonth) || filters.month.includes(recordMonth);
|
||||||
|
|
||||||
const lineMatch = filters.line.length === 0 || filters.line.includes(item.line);
|
const lineMatch = filters.line.length === 0 || filters.line.includes(item.line);
|
||||||
const asinMatch = filters.asin.length === 0 || filters.asin.includes(item.asin);
|
const asinMatch = filters.asin.length === 0 || filters.asin.includes(item.asin);
|
||||||
const skuMatch = filters.sku.length === 0 || filters.sku.includes(item.sku);
|
const skuMatch = filters.sku.length === 0 || filters.sku.includes(item.sku);
|
||||||
const titleMatch = filters.title.length === 0 || filters.title.includes(item.title);
|
const titleMatch = filters.title.length === 0 || filters.title.includes(item.title);
|
||||||
|
|
||||||
return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch;
|
return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateSeasonality = (data: SalesRecord[]): { seasonality: SeasonalityPoint[], seasonalityUnits: SeasonalityPoint[], years: string[] } => {
|
const calculateSeasonality = (data: SalesRecord[]): { seasonality: SeasonalityPoint[], seasonalityUnits: SeasonalityPoint[], years: string[] } => {
|
||||||
const seasonalityMap = new Map<string, SeasonalityPoint>();
|
const seasonalityMap = new Map<string, SeasonalityPoint>();
|
||||||
const seasonalityUnitsMap = new Map<string, SeasonalityPoint>();
|
const seasonalityUnitsMap = new Map<string, SeasonalityPoint>();
|
||||||
const yearsSet = new Set<string>();
|
const yearsSet = new Set<string>();
|
||||||
|
|
||||||
// Initialize all months
|
// Initialize all months
|
||||||
MONTH_ORDER.forEach(m => {
|
MONTH_ORDER.forEach(m => {
|
||||||
seasonalityMap.set(m, { name: m });
|
seasonalityMap.set(m, { name: m });
|
||||||
seasonalityUnitsMap.set(m, { name: m });
|
seasonalityUnitsMap.set(m, { name: m });
|
||||||
});
|
});
|
||||||
|
|
||||||
data.forEach(record => {
|
data.forEach(record => {
|
||||||
const monthName = record.month;
|
const monthName = record.month;
|
||||||
// Extract year from record.month if it's in Format "Mon-YY", else use record.year
|
// Extract year from record.month if it's in Format "Mon-YY", else use record.year
|
||||||
// record.year is numeric, record.month is "Apr-23".
|
// record.year is numeric, record.month is "Apr-23".
|
||||||
const yearStr = record.year.toString();
|
const yearStr = record.year.toString();
|
||||||
yearsSet.add(yearStr);
|
yearsSet.add(yearStr);
|
||||||
|
|
||||||
// We need to match month name purely (Jan, Feb) for the X Axis, ignoring year
|
// We need to match month name purely (Jan, Feb) for the X Axis, ignoring year
|
||||||
const pureMonth = monthName.split('-')[0];
|
const pureMonth = monthName.split('-')[0];
|
||||||
|
|
||||||
if (seasonalityMap.has(pureMonth)) {
|
if (seasonalityMap.has(pureMonth)) {
|
||||||
// Sell Out
|
// Sell Out
|
||||||
const entrySO = seasonalityMap.get(pureMonth)!;
|
const entrySO = seasonalityMap.get(pureMonth)!;
|
||||||
const currentValSO = (entrySO[yearStr] as number) || 0;
|
const currentValSO = (entrySO[yearStr] as number) || 0;
|
||||||
entrySO[yearStr] = currentValSO + record.sellOut;
|
entrySO[yearStr] = currentValSO + record.sellOut;
|
||||||
|
|
||||||
// Units
|
// Units
|
||||||
const entryUnits = seasonalityUnitsMap.get(pureMonth)!;
|
const entryUnits = seasonalityUnitsMap.get(pureMonth)!;
|
||||||
const currentValUnits = (entryUnits[yearStr] as number) || 0;
|
const currentValUnits = (entryUnits[yearStr] as number) || 0;
|
||||||
entryUnits[yearStr] = currentValUnits + record.units;
|
entryUnits[yearStr] = currentValUnits + record.units;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const seasonality = Array.from(seasonalityMap.values());
|
const seasonality = Array.from(seasonalityMap.values());
|
||||||
const seasonalityUnits = Array.from(seasonalityUnitsMap.values());
|
const seasonalityUnits = Array.from(seasonalityUnitsMap.values());
|
||||||
const years = Array.from(yearsSet).sort();
|
const years = Array.from(yearsSet).sort();
|
||||||
|
|
||||||
return { seasonality, seasonalityUnits, years };
|
return { seasonality, seasonalityUnits, years };
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateTopLinesSplit = (data: SalesRecord[]): YearlySplitData[] => {
|
const calculateTopLinesSplit = (data: SalesRecord[]): YearlySplitData[] => {
|
||||||
// 1. Identify Lines by Sell Out (Sort desc)
|
// 1. Identify Lines by Sell Out (Sort desc)
|
||||||
const lineTotals = new Map<string, number>();
|
const lineTotals = new Map<string, number>();
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
lineTotals.set(item.line, (lineTotals.get(item.line) || 0) + item.sellOut);
|
lineTotals.set(item.line, (lineTotals.get(item.line) || 0) + item.sellOut);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return ALL lines
|
// Return ALL lines
|
||||||
const topLines = Array.from(lineTotals.entries())
|
const topLines = Array.from(lineTotals.entries())
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.map(([line]) => line);
|
.map(([line]) => line);
|
||||||
|
|
||||||
// 2. Aggregate data by Year
|
// 2. Aggregate data by Year
|
||||||
const resultMap = new Map<string, YearlySplitData>();
|
const resultMap = new Map<string, YearlySplitData>();
|
||||||
|
|
||||||
topLines.forEach(line => {
|
topLines.forEach(line => {
|
||||||
resultMap.set(line, { name: line });
|
resultMap.set(line, { name: line });
|
||||||
});
|
});
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
if (resultMap.has(item.line)) {
|
if (resultMap.has(item.line)) {
|
||||||
const entry = resultMap.get(item.line)!;
|
const entry = resultMap.get(item.line)!;
|
||||||
const keyVal = `${item.year}_value`;
|
const keyVal = `${item.year}_value`;
|
||||||
const keyUnits = `${item.year}_units`;
|
const keyUnits = `${item.year}_units`;
|
||||||
|
|
||||||
entry[keyVal] = ((entry[keyVal] as number) || 0) + item.sellOut;
|
entry[keyVal] = ((entry[keyVal] as number) || 0) + item.sellOut;
|
||||||
entry[keyUnits] = ((entry[keyUnits] as number) || 0) + item.units;
|
entry[keyUnits] = ((entry[keyUnits] as number) || 0) + item.units;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(resultMap.values());
|
return Array.from(resultMap.values());
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateGenericSplit = (data: SalesRecord[], groupField: keyof SalesRecord, valueField: 'sellOut' | 'units', limit?: number): YearlySplitData[] => {
|
const calculateGenericSplit = (data: SalesRecord[], groupField: keyof SalesRecord, valueField: 'sellOut' | 'units', limit?: number): YearlySplitData[] => {
|
||||||
@@ -608,7 +616,7 @@ const calculateGenericSplit = (data: SalesRecord[], groupField: keyof SalesRecor
|
|||||||
totals.set(key, (totals.get(key) || 0) + item[valueField]);
|
totals.set(key, (totals.get(key) || 0) + item[valueField]);
|
||||||
});
|
});
|
||||||
|
|
||||||
let sortedKeys = Array.from(totals.entries()).sort((a,b) => b[1] - a[1]).map(e => e[0]);
|
let sortedKeys = Array.from(totals.entries()).sort((a, b) => b[1] - a[1]).map(e => e[0]);
|
||||||
if (limit) sortedKeys = sortedKeys.slice(0, limit);
|
if (limit) sortedKeys = sortedKeys.slice(0, limit);
|
||||||
const keySet = new Set(sortedKeys);
|
const keySet = new Set(sortedKeys);
|
||||||
|
|
||||||
@@ -629,93 +637,93 @@ const calculateGenericSplit = (data: SalesRecord[], groupField: keyof SalesRecor
|
|||||||
|
|
||||||
// Renamed from calculateMovers
|
// Renamed from calculateMovers
|
||||||
export const calculateLineMovers = (data: SalesRecord[]): { topMovers: LineGrowthMetric[], bottomMovers: LineGrowthMetric[], comparisonPeriods: { current: string, previous: string } } => {
|
export const calculateLineMovers = (data: SalesRecord[]): { topMovers: LineGrowthMetric[], bottomMovers: LineGrowthMetric[], comparisonPeriods: { current: string, previous: string } } => {
|
||||||
const lineYearMap = new Map<string, Map<number, { sellOut: number; units: number }>>();
|
const lineYearMap = new Map<string, Map<number, { sellOut: number; units: number }>>();
|
||||||
const allYears = new Set<number>();
|
const allYears = new Set<number>();
|
||||||
|
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
if (!lineYearMap.has(item.line)) {
|
if (!lineYearMap.has(item.line)) {
|
||||||
lineYearMap.set(item.line, new Map());
|
lineYearMap.set(item.line, new Map());
|
||||||
}
|
}
|
||||||
const yearMap = lineYearMap.get(item.line)!;
|
const yearMap = lineYearMap.get(item.line)!;
|
||||||
const current = yearMap.get(item.year) || { sellOut: 0, units: 0 };
|
const current = yearMap.get(item.year) || { sellOut: 0, units: 0 };
|
||||||
yearMap.set(item.year, {
|
yearMap.set(item.year, {
|
||||||
sellOut: current.sellOut + item.sellOut,
|
sellOut: current.sellOut + item.sellOut,
|
||||||
units: current.units + item.units
|
units: current.units + item.units
|
||||||
});
|
|
||||||
allYears.add(item.year);
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedYears = Array.from(allYears).sort((a, b) => b - a);
|
|
||||||
|
|
||||||
if (sortedYears.length < 2) {
|
|
||||||
return { topMovers: [], bottomMovers: [], comparisonPeriods: { current: 'N/A', previous: 'N/A' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentYear = sortedYears[0];
|
|
||||||
const prevYear = sortedYears[1];
|
|
||||||
|
|
||||||
const metrics: LineGrowthMetric[] = [];
|
|
||||||
|
|
||||||
lineYearMap.forEach((yearMap, line) => {
|
|
||||||
const currData = yearMap.get(currentYear) || { sellOut: 0, units: 0 };
|
|
||||||
const prevData = yearMap.get(prevYear) || { sellOut: 0, units: 0 };
|
|
||||||
|
|
||||||
// Sell Out Growth
|
|
||||||
let sellOutGrowthValue = 0;
|
|
||||||
let sellOutGrowthPercentage = 0;
|
|
||||||
if (prevData.sellOut > 0) {
|
|
||||||
sellOutGrowthValue = currData.sellOut - prevData.sellOut;
|
|
||||||
sellOutGrowthPercentage = (sellOutGrowthValue / prevData.sellOut) * 100;
|
|
||||||
} else if (currData.sellOut > 0) {
|
|
||||||
sellOutGrowthValue = currData.sellOut;
|
|
||||||
sellOutGrowthPercentage = 100;
|
|
||||||
} else if (currData.sellOut === 0 && prevData.sellOut > 0) {
|
|
||||||
sellOutGrowthValue = -prevData.sellOut;
|
|
||||||
sellOutGrowthPercentage = -100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unit Growth
|
|
||||||
let unitsGrowthValue = 0;
|
|
||||||
let unitsGrowthPercentage = 0;
|
|
||||||
if (prevData.units > 0) {
|
|
||||||
unitsGrowthValue = currData.units - prevData.units;
|
|
||||||
unitsGrowthPercentage = (unitsGrowthValue / prevData.units) * 100;
|
|
||||||
} else if (currData.units > 0) {
|
|
||||||
unitsGrowthValue = currData.units;
|
|
||||||
unitsGrowthPercentage = 100;
|
|
||||||
} else if (currData.units === 0 && prevData.units > 0) {
|
|
||||||
unitsGrowthValue = -prevData.units;
|
|
||||||
unitsGrowthPercentage = -100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currData.sellOut > 0 || prevData.sellOut > 0) {
|
|
||||||
metrics.push({
|
|
||||||
line,
|
|
||||||
currentYearSellOut: currData.sellOut,
|
|
||||||
previousYearSellOut: prevData.sellOut,
|
|
||||||
sellOutGrowthValue,
|
|
||||||
sellOutGrowthPercentage,
|
|
||||||
currentYearUnits: currData.units,
|
|
||||||
previousYearUnits: prevData.units,
|
|
||||||
unitsGrowthValue,
|
|
||||||
unitsGrowthPercentage
|
|
||||||
});
|
});
|
||||||
|
allYears.add(item.year);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedYears = Array.from(allYears).sort((a, b) => b - a);
|
||||||
|
|
||||||
|
if (sortedYears.length < 2) {
|
||||||
|
return { topMovers: [], bottomMovers: [], comparisonPeriods: { current: 'N/A', previous: 'N/A' } };
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const topMovers = metrics
|
const currentYear = sortedYears[0];
|
||||||
.filter(m => m.sellOutGrowthValue > 0)
|
const prevYear = sortedYears[1];
|
||||||
.sort((a, b) => b.sellOutGrowthValue - a.sellOutGrowthValue);
|
|
||||||
|
|
||||||
const bottomMovers = metrics
|
const metrics: LineGrowthMetric[] = [];
|
||||||
.filter(m => m.sellOutGrowthValue < 0)
|
|
||||||
.sort((a, b) => a.sellOutGrowthValue - b.sellOutGrowthValue);
|
|
||||||
|
|
||||||
return {
|
lineYearMap.forEach((yearMap, line) => {
|
||||||
topMovers,
|
const currData = yearMap.get(currentYear) || { sellOut: 0, units: 0 };
|
||||||
bottomMovers,
|
const prevData = yearMap.get(prevYear) || { sellOut: 0, units: 0 };
|
||||||
comparisonPeriods: { current: currentYear.toString(), previous: prevYear.toString() }
|
|
||||||
};
|
// Sell Out Growth
|
||||||
|
let sellOutGrowthValue = 0;
|
||||||
|
let sellOutGrowthPercentage = 0;
|
||||||
|
if (prevData.sellOut > 0) {
|
||||||
|
sellOutGrowthValue = currData.sellOut - prevData.sellOut;
|
||||||
|
sellOutGrowthPercentage = (sellOutGrowthValue / prevData.sellOut) * 100;
|
||||||
|
} else if (currData.sellOut > 0) {
|
||||||
|
sellOutGrowthValue = currData.sellOut;
|
||||||
|
sellOutGrowthPercentage = 100;
|
||||||
|
} else if (currData.sellOut === 0 && prevData.sellOut > 0) {
|
||||||
|
sellOutGrowthValue = -prevData.sellOut;
|
||||||
|
sellOutGrowthPercentage = -100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unit Growth
|
||||||
|
let unitsGrowthValue = 0;
|
||||||
|
let unitsGrowthPercentage = 0;
|
||||||
|
if (prevData.units > 0) {
|
||||||
|
unitsGrowthValue = currData.units - prevData.units;
|
||||||
|
unitsGrowthPercentage = (unitsGrowthValue / prevData.units) * 100;
|
||||||
|
} else if (currData.units > 0) {
|
||||||
|
unitsGrowthValue = currData.units;
|
||||||
|
unitsGrowthPercentage = 100;
|
||||||
|
} else if (currData.units === 0 && prevData.units > 0) {
|
||||||
|
unitsGrowthValue = -prevData.units;
|
||||||
|
unitsGrowthPercentage = -100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currData.sellOut > 0 || prevData.sellOut > 0) {
|
||||||
|
metrics.push({
|
||||||
|
line,
|
||||||
|
currentYearSellOut: currData.sellOut,
|
||||||
|
previousYearSellOut: prevData.sellOut,
|
||||||
|
sellOutGrowthValue,
|
||||||
|
sellOutGrowthPercentage,
|
||||||
|
currentYearUnits: currData.units,
|
||||||
|
previousYearUnits: prevData.units,
|
||||||
|
unitsGrowthValue,
|
||||||
|
unitsGrowthPercentage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const topMovers = metrics
|
||||||
|
.filter(m => m.sellOutGrowthValue > 0)
|
||||||
|
.sort((a, b) => b.sellOutGrowthValue - a.sellOutGrowthValue);
|
||||||
|
|
||||||
|
const bottomMovers = metrics
|
||||||
|
.filter(m => m.sellOutGrowthValue < 0)
|
||||||
|
.sort((a, b) => a.sellOutGrowthValue - b.sellOutGrowthValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
topMovers,
|
||||||
|
bottomMovers,
|
||||||
|
comparisonPeriods: { current: currentYear.toString(), previous: prevYear.toString() }
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -856,70 +864,70 @@ export const calculateItemMovers = (
|
|||||||
|
|
||||||
|
|
||||||
export const aggregateData = (data: SalesRecord[]): AggregatedData => {
|
export const aggregateData = (data: SalesRecord[]): AggregatedData => {
|
||||||
const totalSellOut = data.reduce((acc, curr) => acc + curr.sellOut, 0);
|
const totalSellOut = data.reduce((acc, curr) => acc + curr.sellOut, 0);
|
||||||
const totalUnits = data.reduce((acc, curr) => acc + curr.units, 0);
|
const totalUnits = data.reduce((acc, curr) => acc + curr.units, 0);
|
||||||
|
|
||||||
const totalsByYear: Record<string, { sellOut: number; units: number }> = {};
|
const totalsByYear: Record<string, { sellOut: number; units: number }> = {};
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
const y = item.year.toString();
|
const y = item.year.toString();
|
||||||
if (!totalsByYear[y]) totalsByYear[y] = { sellOut: 0, units: 0 };
|
if (!totalsByYear[y]) totalsByYear[y] = { sellOut: 0, units: 0 };
|
||||||
totalsByYear[y].sellOut += item.sellOut;
|
totalsByYear[y].sellOut += item.sellOut;
|
||||||
totalsByYear[y].units += item.units;
|
totalsByYear[y].units += item.units;
|
||||||
});
|
|
||||||
|
|
||||||
const lineMap = new Map<string, { value: number; units: number }>();
|
|
||||||
data.forEach(item => {
|
|
||||||
const current = lineMap.get(item.line) || { value: 0, units: 0 };
|
|
||||||
lineMap.set(item.line, {
|
|
||||||
value: current.value + item.sellOut,
|
|
||||||
units: current.units + item.units
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
const byLine = Array.from(lineMap.entries())
|
|
||||||
.map(([name, data]) => ({ name, value: data.value, units: data.units }))
|
|
||||||
.sort((a, b) => b.value - a.value);
|
|
||||||
|
|
||||||
const customerMap = new Map<string, number>();
|
const lineMap = new Map<string, { value: number; units: number }>();
|
||||||
data.forEach(item => {
|
data.forEach(item => {
|
||||||
customerMap.set(item.customer, (customerMap.get(item.customer) || 0) + item.sellOut);
|
const current = lineMap.get(item.line) || { value: 0, units: 0 };
|
||||||
});
|
lineMap.set(item.line, {
|
||||||
const byCustomer = Array.from(customerMap.entries())
|
value: current.value + item.sellOut,
|
||||||
.map(([name, value]) => ({ name, value }))
|
units: current.units + item.units
|
||||||
.sort((a, b) => b.value - a.value);
|
});
|
||||||
|
});
|
||||||
|
const byLine = Array.from(lineMap.entries())
|
||||||
|
.map(([name, data]) => ({ name, value: data.value, units: data.units }))
|
||||||
|
.sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
const { seasonality, seasonalityUnits, years } = calculateSeasonality(data);
|
const customerMap = new Map<string, number>();
|
||||||
const { topMovers, bottomMovers, comparisonPeriods } = calculateLineMovers(data); // Use calculateLineMovers
|
data.forEach(item => {
|
||||||
const topLinesSplit = calculateTopLinesSplit(data);
|
customerMap.set(item.customer, (customerMap.get(item.customer) || 0) + item.sellOut);
|
||||||
const byCustomerSplit = calculateGenericSplit(data, 'customer', 'sellOut');
|
});
|
||||||
const byLineOverviewSplit = calculateGenericSplit(data, 'line', 'units', 10);
|
const byCustomer = Array.from(customerMap.entries())
|
||||||
|
.map(([name, value]) => ({ name, value }))
|
||||||
|
.sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
return {
|
const { seasonality, seasonalityUnits, years } = calculateSeasonality(data);
|
||||||
totalSellOut,
|
const { topMovers, bottomMovers, comparisonPeriods } = calculateLineMovers(data); // Use calculateLineMovers
|
||||||
totalUnits,
|
const topLinesSplit = calculateTopLinesSplit(data);
|
||||||
totalsByYear,
|
const byCustomerSplit = calculateGenericSplit(data, 'customer', 'sellOut');
|
||||||
byLine,
|
const byLineOverviewSplit = calculateGenericSplit(data, 'line', 'units', 10);
|
||||||
byCustomer,
|
|
||||||
seasonality,
|
return {
|
||||||
seasonalityUnits,
|
totalSellOut,
|
||||||
availableYears: years,
|
totalUnits,
|
||||||
topMovers,
|
totalsByYear,
|
||||||
bottomMovers,
|
byLine,
|
||||||
comparisonPeriods,
|
byCustomer,
|
||||||
topLinesSplit,
|
seasonality,
|
||||||
byCustomerSplit,
|
seasonalityUnits,
|
||||||
byLineOverviewSplit
|
availableYears: years,
|
||||||
};
|
topMovers,
|
||||||
|
bottomMovers,
|
||||||
|
comparisonPeriods,
|
||||||
|
topLinesSplit,
|
||||||
|
byCustomerSplit,
|
||||||
|
byLineOverviewSplit
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUniqueValues = (data: SalesRecord[], field: keyof SalesRecord): string[] => {
|
export const getUniqueValues = (data: SalesRecord[], field: keyof SalesRecord): string[] => {
|
||||||
const values = new Set(data.map(item => String(item[field])));
|
const values = new Set(data.map(item => String(item[field])));
|
||||||
return Array.from(values).sort();
|
return Array.from(values).sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['title', 'customer', 'line', 'sku']): { rows: PivotRow[], years: string[] } => {
|
export const pivotSalesData = (data: SalesRecord[], dimensions: string[] = ['title', 'customer', 'line', 'sku']): { rows: PivotRow[], years: string[] } => {
|
||||||
// 1. Determine all years present in the data for columns
|
// 1. Determine all years present in the data for columns
|
||||||
const yearsSet = new Set(data.map(d => d.year));
|
const yearsSet = new Set(data.map(d => d.year));
|
||||||
const years = Array.from(yearsSet).sort((a,b) => b-a).map(String);
|
const years = Array.from(yearsSet).sort((a, b) => b - a).map(String);
|
||||||
|
|
||||||
const map = new Map<string, PivotRow>();
|
const map = new Map<string, PivotRow>();
|
||||||
|
|
||||||
@@ -1027,36 +1035,36 @@ export const generateCSV = (rows: PivotRow[], dimensions: string[], years: strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateItemMoversCSV = (
|
export const generateItemMoversCSV = (
|
||||||
data: ItemGrowthMetric[],
|
data: ItemGrowthMetric[],
|
||||||
periods: { current: string; previous: string },
|
periods: { current: string; previous: string },
|
||||||
type: 'Gainers' | 'Losers'
|
type: 'Gainers' | 'Losers'
|
||||||
) => {
|
) => {
|
||||||
const flatData = data.map(item => ({
|
const flatData = data.map(item => ({
|
||||||
SKU: item.sku || '-',
|
SKU: item.sku || '-',
|
||||||
ASIN: item.asin || '-',
|
ASIN: item.asin || '-',
|
||||||
'Product Title': item.title || '-',
|
'Product Title': item.title || '-',
|
||||||
'Product Line': item.line || '-',
|
'Product Line': item.line || '-',
|
||||||
[`Sell Out ${periods.previous}`]: item.previousYearSellOut.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
[`Sell Out ${periods.previous}`]: item.previousYearSellOut.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
[`Sell Out ${periods.current}`]: item.currentYearSellOut.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
[`Sell Out ${periods.current}`]: item.currentYearSellOut.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
'SO Diff': item.sellOutGrowthValue.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
'SO Diff': item.sellOutGrowthValue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
'SO Growth %': item.sellOutGrowthPercentage.toLocaleString('de-DE', {minimumFractionDigits: 1, maximumFractionDigits: 1}) + '%',
|
'SO Growth %': item.sellOutGrowthPercentage.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%',
|
||||||
[`Units ${periods.previous}`]: item.previousYearUnits.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
[`Units ${periods.previous}`]: item.previousYearUnits.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
[`Units ${periods.current}`]: item.currentYearUnits.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
[`Units ${periods.current}`]: item.currentYearUnits.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
'Units Diff': item.unitsGrowthValue.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
'Units Diff': item.unitsGrowthValue.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 }),
|
||||||
'Units Growth %': item.unitsGrowthPercentage.toLocaleString('de-DE', {minimumFractionDigits: 1, maximumFractionDigits: 1}) + '%',
|
'Units Growth %': item.unitsGrowthPercentage.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const csv = Papa.unparse(flatData);
|
const csv = Papa.unparse(flatData);
|
||||||
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.setAttribute('download', `${type}_${periods.current}_vs_${periods.previous}_${new Date().toISOString().split('T')[0]}.csv`);
|
link.setAttribute('download', `${type}_${periods.current}_vs_${periods.previous}_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user