mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:35:24 +02:00
feat: Initialize Craze Analytix project structure
Sets up the project with Vite, React, Tailwind CSS, Gemini AI integration, and necessary dependencies for data analysis. Includes initial configuration for TypeScript, Tailwind, and project metadata.
This commit is contained in:
@@ -0,0 +1,817 @@
|
||||
import { SalesRecord, FilterState, AggregatedData, LineGrowthMetric, ItemGrowthMetric, SeasonalityPoint, YearlySplitData, PivotRow, YearlyData, TimeSeriesData, ComparisonTimeSeriesPoint } from '../types';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
// 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 => {
|
||||
if (!value) return 0;
|
||||
|
||||
// Remove currency symbol and whitespace
|
||||
let clean = value.replace(/[€\s]/g, '').trim();
|
||||
|
||||
// HEURISTIC:
|
||||
// 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)
|
||||
// But given the context (DE data), comma is usually decimal.
|
||||
|
||||
// Case A: European Format (e.g., "277.179,09" or "50,00")
|
||||
if (clean.includes(',')) {
|
||||
// If it has dots (thousands), remove them
|
||||
clean = clean.replace(/\./g, '');
|
||||
// Replace decimal comma with dot
|
||||
clean = clean.replace(',', '.');
|
||||
return parseFloat(clean);
|
||||
}
|
||||
|
||||
// Case B: Standard/US Format or Clean Number (e.g. "277179.09" or "1000")
|
||||
// Just remove any potential thousands separator commas (if any exist and we didn't catch them above)
|
||||
// and parse.
|
||||
clean = clean.replace(/,/g, '');
|
||||
const num = parseFloat(clean);
|
||||
|
||||
return isNaN(num) ? 0 : num;
|
||||
};
|
||||
|
||||
const parseUnits = (value: string): number => {
|
||||
if(!value) return 0;
|
||||
// Remove dots (thousands separators in EU) and commas (thousands in US) just to be safe for integers
|
||||
const clean = value.replace(/[\.,]/g, '');
|
||||
const num = parseInt(clean, 10);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
const MONTH_ORDER = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
// Robust Month Normalizer
|
||||
const normalizeMonth = (rawMonth: string): string => {
|
||||
if (!rawMonth) return '';
|
||||
let m = rawMonth.trim();
|
||||
|
||||
// Handle numeric months "01", "1", "01-2023" (start with digits)
|
||||
const numMatch = m.match(/^(\d{1,2})([^\d]|$)/);
|
||||
if (numMatch) {
|
||||
const num = parseInt(numMatch[1]);
|
||||
if (num >= 1 && num <= 12) return MONTH_ORDER[num - 1];
|
||||
}
|
||||
|
||||
// Handle text months "Apr-23", "Apr 23", "April"
|
||||
// Extract first sequence of letters
|
||||
const alphaMatch = m.match(/([a-zA-Z]+)/);
|
||||
if (alphaMatch) {
|
||||
m = alphaMatch[1];
|
||||
}
|
||||
|
||||
// Take first 3 characters
|
||||
if (m.length > 3) {
|
||||
m = m.substring(0, 3);
|
||||
}
|
||||
// Capitalize first letter, lowercase rest
|
||||
m = m.charAt(0).toUpperCase() + m.slice(1).toLowerCase();
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
// Robust CSV Column Value Extractor
|
||||
// Handles case-insensitivity, trimming, multiple potential header aliases, AND ignores empty values to find fallbacks.
|
||||
const getColumnValue = (row: any, aliases: string[]): string => {
|
||||
const rowKeys = Object.keys(row);
|
||||
// Create a map of normalized keys in the row to the actual keys
|
||||
const normalizedRowKeys: Record<string, string> = {};
|
||||
rowKeys.forEach(k => {
|
||||
normalizedRowKeys[k.trim().toLowerCase()] = k;
|
||||
});
|
||||
|
||||
for (const alias of aliases) {
|
||||
const lookup = alias.trim().toLowerCase();
|
||||
if (normalizedRowKeys[lookup]) {
|
||||
const actualKey = normalizedRowKeys[lookup];
|
||||
const val = row[actualKey];
|
||||
if (val !== undefined && val !== null) {
|
||||
const strVal = String(val).trim();
|
||||
// CRITICAL FIX: Only return if the value is NOT empty.
|
||||
// This allows falling back to the next alias if the first matching column exists but is empty.
|
||||
if (strVal.length > 0) {
|
||||
return strVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Extracted Mapping Function
|
||||
const mapRowToRecord = (row: any, index: number): SalesRecord => {
|
||||
const customer = getColumnValue(row, ['NEW CUSTOMER', 'Customer', 'Client', 'Account', 'Partner', 'COUNTRY', 'Country', 'Market']) || 'Unknown';
|
||||
const yearStr = getColumnValue(row, ['YEAR', 'Year', 'D']);
|
||||
const year = parseInt(yearStr) || 0;
|
||||
const monthStr = getColumnValue(row, ['MONTH', 'Month', 'Period']);
|
||||
const month = normalizeMonth(monthStr);
|
||||
const weekStr = getColumnValue(row, ['WEEK', 'Week', 'CW', 'Semana', 'KW', 'E']);
|
||||
const weekNum = weekStr ? parseInt(weekStr.replace(/cw/i, '').trim(), 10) : NaN;
|
||||
const week = isNaN(weekNum) ? undefined : weekNum;
|
||||
const line = getColumnValue(row, ['LINE', 'Line', 'Product Line']) || 'Other';
|
||||
|
||||
// Updated ASIN priority list based on user feedback
|
||||
const asin = getColumnValue(row, [
|
||||
'CUSTOMER REFERENCE',
|
||||
'AMAZON ASIN',
|
||||
'ASIN',
|
||||
'Asin',
|
||||
'PRODUCT ID',
|
||||
'ITEM IDENTIFIER',
|
||||
'ASIN NO.',
|
||||
'Product ASIN',
|
||||
'IDENTIFIER'
|
||||
]);
|
||||
|
||||
const sku = getColumnValue(row, ['RAW ARTICLE NO.', 'SKU', 'Sku', 'Item No']);
|
||||
|
||||
// Prioritize 'Title' column, fallback to 'Article Name' columns
|
||||
const title = getColumnValue(row, ['ARTICLE NAME (Craze)', 'Title', 'TITLE', 'Product Title', 'Article Name', 'ArticleName']);
|
||||
|
||||
// Legacy/Backup field
|
||||
const articleName = getColumnValue(row, ['ARTICLE NAME (Craze)', 'Article Name', 'ArticleName', 'Title']);
|
||||
|
||||
const unitsRaw = getColumnValue(row, ['UNITS', 'Units', 'Quantity', 'Qty']);
|
||||
const sellOutRaw = getColumnValue(row, ['AMOUNT', 'Sell Out', 'SellOut', 'Revenue', 'Sales', 'Turnover']);
|
||||
|
||||
return {
|
||||
id: `row-${index}`,
|
||||
customer,
|
||||
year,
|
||||
month,
|
||||
week,
|
||||
asin,
|
||||
sku,
|
||||
title,
|
||||
articleName,
|
||||
units: parseUnits(unitsRaw),
|
||||
sellOut: parseCurrency(sellOutRaw),
|
||||
line
|
||||
};
|
||||
};
|
||||
|
||||
export const processCSV = (fileOrContent: File | string): Promise<SalesRecord[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore - PapaParse is loaded globally via CDN
|
||||
Papa.parse(fileOrContent, {
|
||||
header: true,
|
||||
// delimiter: ";", // Allow auto-detect
|
||||
skipEmptyLines: true,
|
||||
complete: (results: any) => {
|
||||
try {
|
||||
const data: SalesRecord[] = results.data.map((row: any, index: number) => {
|
||||
return mapRowToRecord(row, index);
|
||||
}).filter((r: SalesRecord) => r.year !== 2022 && r.line && r.line !== 'Other'); // Validation: Exclude 2022 and require line
|
||||
resolve(data);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
error: (error: any) => {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const processExcel = async (file: File): Promise<SalesRecord[]> => {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const workbook = XLSX.read(arrayBuffer);
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
// Convert to JSON
|
||||
// raw: false attempts to format the cell (e.g. dates), but for robustness we often prefer raw values or defval
|
||||
// Using { defval: "" } ensures empty cells are present as empty strings if needed, but key logic handles missing keys.
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
|
||||
|
||||
const data: SalesRecord[] = jsonData.map((row: any, index: number) => {
|
||||
return mapRowToRecord(row, index);
|
||||
}).filter((r: SalesRecord) => r.year !== 2022 && r.line && r.line !== 'Other');
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error processing Excel file:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const filterData = (data: SalesRecord[], filters: FilterState): SalesRecord[] => {
|
||||
return data.filter(item => {
|
||||
// Item month is already normalized
|
||||
const recordMonth = item.month;
|
||||
|
||||
const customerMatch = filters.customer.length === 0 || filters.customer.includes(item.customer);
|
||||
const yearMatch = filters.year.length === 0 || filters.year.includes(item.year.toString());
|
||||
const monthMatch = filters.month.length === 0 || filters.month.includes(recordMonth);
|
||||
const lineMatch = filters.line.length === 0 || filters.line.includes(item.line);
|
||||
const asinMatch = filters.asin.length === 0 || filters.asin.includes(item.asin);
|
||||
const skuMatch = filters.sku.length === 0 || filters.sku.includes(item.sku);
|
||||
const titleMatch = filters.title.length === 0 || filters.title.includes(item.title);
|
||||
|
||||
return customerMatch && yearMatch && monthMatch && lineMatch && asinMatch && skuMatch && titleMatch;
|
||||
});
|
||||
};
|
||||
|
||||
const calculateSeasonality = (data: SalesRecord[]): { seasonality: SeasonalityPoint[], seasonalityUnits: SeasonalityPoint[], years: string[] } => {
|
||||
const seasonalityMap = new Map<string, SeasonalityPoint>();
|
||||
const seasonalityUnitsMap = new Map<string, SeasonalityPoint>();
|
||||
const yearsSet = new Set<string>();
|
||||
|
||||
// Initialize all months
|
||||
MONTH_ORDER.forEach(m => {
|
||||
seasonalityMap.set(m, { name: m });
|
||||
seasonalityUnitsMap.set(m, { name: m });
|
||||
});
|
||||
|
||||
data.forEach(record => {
|
||||
const monthName = record.month;
|
||||
const yearStr = record.year.toString();
|
||||
yearsSet.add(yearStr);
|
||||
|
||||
if (seasonalityMap.has(monthName)) {
|
||||
// Sell Out
|
||||
const entrySO = seasonalityMap.get(monthName)!;
|
||||
const currentValSO = (entrySO[yearStr] as number) || 0;
|
||||
entrySO[yearStr] = currentValSO + record.sellOut;
|
||||
|
||||
// Units
|
||||
const entryUnits = seasonalityUnitsMap.get(monthName)!;
|
||||
const currentValUnits = (entryUnits[yearStr] as number) || 0;
|
||||
entryUnits[yearStr] = currentValUnits + record.units;
|
||||
}
|
||||
});
|
||||
|
||||
const seasonality = Array.from(seasonalityMap.values());
|
||||
const seasonalityUnits = Array.from(seasonalityUnitsMap.values());
|
||||
const years = Array.from(yearsSet).sort();
|
||||
|
||||
return { seasonality, seasonalityUnits, years };
|
||||
};
|
||||
|
||||
const calculateTopLinesSplit = (data: SalesRecord[]): YearlySplitData[] => {
|
||||
// 1. Identify Lines by Sell Out (Sort desc)
|
||||
const lineTotals = new Map<string, number>();
|
||||
data.forEach(item => {
|
||||
lineTotals.set(item.line, (lineTotals.get(item.line) || 0) + item.sellOut);
|
||||
});
|
||||
|
||||
// Return ALL lines
|
||||
const topLines = Array.from(lineTotals.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([line]) => line);
|
||||
|
||||
// 2. Aggregate data by Year
|
||||
const resultMap = new Map<string, YearlySplitData>();
|
||||
|
||||
topLines.forEach(line => {
|
||||
resultMap.set(line, { name: line });
|
||||
});
|
||||
|
||||
data.forEach(item => {
|
||||
if (resultMap.has(item.line)) {
|
||||
const entry = resultMap.get(item.line)!;
|
||||
const keyVal = `${item.year}_value`;
|
||||
const keyUnits = `${item.year}_units`;
|
||||
|
||||
entry[keyVal] = ((entry[keyVal] as number) || 0) + item.sellOut;
|
||||
entry[keyUnits] = ((entry[keyUnits] as number) || 0) + item.units;
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(resultMap.values());
|
||||
};
|
||||
|
||||
const calculateGenericSplit = (data: SalesRecord[], groupField: keyof SalesRecord, valueField: 'sellOut' | 'units', limit?: number): YearlySplitData[] => {
|
||||
const totals = new Map<string, number>();
|
||||
data.forEach(item => {
|
||||
const key = String(item[groupField]);
|
||||
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]);
|
||||
if (limit) sortedKeys = sortedKeys.slice(0, limit);
|
||||
const keySet = new Set(sortedKeys);
|
||||
|
||||
const resultMap = new Map<string, YearlySplitData>();
|
||||
sortedKeys.forEach(k => resultMap.set(k, { name: k }));
|
||||
|
||||
data.forEach(item => {
|
||||
const key = String(item[groupField]);
|
||||
if (keySet.has(key)) {
|
||||
const entry = resultMap.get(key)!;
|
||||
const yearKey = item.year.toString();
|
||||
entry[yearKey] = ((entry[yearKey] as number) || 0) + item[valueField];
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(resultMap.values());
|
||||
};
|
||||
|
||||
// Renamed from calculateMovers
|
||||
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 allYears = new Set<number>();
|
||||
|
||||
data.forEach(item => {
|
||||
if (!lineYearMap.has(item.line)) {
|
||||
lineYearMap.set(item.line, new Map());
|
||||
}
|
||||
const yearMap = lineYearMap.get(item.line)!;
|
||||
const current = yearMap.get(item.year) || { sellOut: 0, units: 0 };
|
||||
yearMap.set(item.year, {
|
||||
sellOut: current.sellOut + item.sellOut,
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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() }
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const createItemKey = (record: SalesRecord) => {
|
||||
// A robust key combining all identifiers
|
||||
return `${record.sku || 'NO_SKU'}||${record.asin || 'NO_ASIN'}||${record.title || 'NO_TITLE'}`;
|
||||
}
|
||||
|
||||
export const calculateItemMovers = (
|
||||
currentFilteredData: SalesRecord[],
|
||||
selectedCustomerFromPage: string | null,
|
||||
currentComparisonYearFromPage: number | null
|
||||
): { topMovers: ItemGrowthMetric[], bottomMovers: ItemGrowthMetric[], comparisonPeriods: { current: string, previous: string } } => {
|
||||
|
||||
let dataToProcess = currentFilteredData;
|
||||
|
||||
// Apply customer filter if selected on the Top Movers page
|
||||
if (selectedCustomerFromPage) {
|
||||
dataToProcess = dataToProcess.filter(item => item.customer === selectedCustomerFromPage);
|
||||
}
|
||||
|
||||
if (dataToProcess.length === 0) {
|
||||
return { topMovers: [], bottomMovers: [], comparisonPeriods: { current: 'N/A', previous: 'N/A' } };
|
||||
}
|
||||
|
||||
// Map to store item data aggregated by year
|
||||
const itemYearMap = new Map<string, Map<number, { sellOut: number; units: number, sku: string, asin: string, title: string, line: string }>>();
|
||||
const allYearsInFilteredData = new Set<number>();
|
||||
|
||||
dataToProcess.forEach(item => {
|
||||
const itemKey = createItemKey(item);
|
||||
if (!itemYearMap.has(itemKey)) {
|
||||
itemYearMap.set(itemKey, new Map());
|
||||
}
|
||||
const yearMap = itemYearMap.get(itemKey)!;
|
||||
const current = yearMap.get(item.year) || { sellOut: 0, units: 0, sku: item.sku, asin: item.asin, title: item.title, line: item.line };
|
||||
yearMap.set(item.year, {
|
||||
sellOut: current.sellOut + item.sellOut,
|
||||
units: current.units + item.units,
|
||||
sku: item.sku,
|
||||
asin: item.asin,
|
||||
title: item.title,
|
||||
line: item.line
|
||||
});
|
||||
allYearsInFilteredData.add(item.year);
|
||||
});
|
||||
|
||||
const sortedYearsInFilteredData = Array.from(allYearsInFilteredData).sort((a, b) => b - a); // Descending (most recent first)
|
||||
|
||||
let currentYear: number;
|
||||
let prevYear: number;
|
||||
|
||||
if (currentComparisonYearFromPage) {
|
||||
// If a specific comparison year is provided by the user on the Top Movers page
|
||||
currentYear = currentComparisonYearFromPage;
|
||||
const currentYearIndex = sortedYearsInFilteredData.indexOf(currentYear);
|
||||
if (currentYearIndex === -1 || currentYearIndex === sortedYearsInFilteredData.length - 1) {
|
||||
// Specified year not found in filtered data or it's the oldest year (no previous year for comparison)
|
||||
return { topMovers: [], bottomMovers: [], comparisonPeriods: { current: currentYear.toString(), previous: 'N/A' } };
|
||||
}
|
||||
prevYear = sortedYearsInFilteredData[currentYearIndex + 1]; // The year directly before the currentComparisonYear
|
||||
} else {
|
||||
// Default to the two most recent years from the *filtered data* if no specific year is chosen
|
||||
if (sortedYearsInFilteredData.length < 2) {
|
||||
return { topMovers: [], bottomMovers: [], comparisonPeriods: { current: 'N/A', previous: 'N/A' } };
|
||||
}
|
||||
currentYear = sortedYearsInFilteredData[0]; // Most recent
|
||||
prevYear = sortedYearsInFilteredData[1]; // Second most recent
|
||||
}
|
||||
|
||||
const metrics: ItemGrowthMetric[] = [];
|
||||
|
||||
itemYearMap.forEach((yearMap) => {
|
||||
const currData = yearMap.get(currentYear) || { sellOut: 0, units: 0, sku: '', asin: '', title: '', line: '' };
|
||||
const prevData = yearMap.get(prevYear) || { sellOut: 0, units: 0, sku: '', asin: '', title: '', line: '' };
|
||||
|
||||
// Only include items that had some activity in at least one of the comparison years
|
||||
if ((currData.sellOut === 0 && currData.units === 0) && (prevData.sellOut === 0 && prevData.units === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use metadata from current year, if not available use previous (for sku/asin/title/line)
|
||||
const itemMeta = currData.sku ? currData : prevData;
|
||||
|
||||
|
||||
// Sell Out Growth
|
||||
let sellOutGrowthValue = currData.sellOut - prevData.sellOut;
|
||||
let sellOutGrowthPercentage = 0;
|
||||
if (prevData.sellOut !== 0) {
|
||||
sellOutGrowthPercentage = (sellOutGrowthValue / prevData.sellOut) * 100;
|
||||
} else if (currData.sellOut > 0) {
|
||||
sellOutGrowthPercentage = 100; // Growth from zero
|
||||
} else if (currData.sellOut === 0 && prevData.sellOut > 0) {
|
||||
sellOutGrowthPercentage = -100; // Decline to zero
|
||||
}
|
||||
|
||||
// Unit Growth
|
||||
let unitsGrowthValue = currData.units - prevData.units;
|
||||
let unitsGrowthPercentage = 0;
|
||||
if (prevData.units !== 0) {
|
||||
unitsGrowthPercentage = (unitsGrowthValue / prevData.units) * 100;
|
||||
} else if (currData.units > 0) {
|
||||
unitsGrowthPercentage = 100; // Growth from zero
|
||||
} else if (currData.units === 0 && prevData.units > 0) {
|
||||
unitsGrowthPercentage = -100; // Decline to zero
|
||||
}
|
||||
|
||||
metrics.push({
|
||||
sku: itemMeta.sku,
|
||||
asin: itemMeta.asin,
|
||||
title: itemMeta.title,
|
||||
line: itemMeta.line,
|
||||
currentYearSellOut: currData.sellOut,
|
||||
previousYearSellOut: prevData.sellOut,
|
||||
sellOutGrowthValue,
|
||||
sellOutGrowthPercentage,
|
||||
currentYearUnits: currData.units,
|
||||
previousYearUnits: prevData.units,
|
||||
unitsGrowthValue,
|
||||
unitsGrowthPercentage
|
||||
});
|
||||
});
|
||||
|
||||
const topMovers = metrics
|
||||
.sort((a, b) => b.unitsGrowthValue - a.unitsGrowthValue) // Sort by unitsGrowthValue
|
||||
.slice(0, 20); // Top 20 Gainers
|
||||
|
||||
const bottomMovers = metrics
|
||||
.sort((a, b) => a.unitsGrowthValue - b.unitsGrowthValue) // Sort by unitsGrowthValue
|
||||
.slice(0, 20); // Top 20 Losers
|
||||
|
||||
return {
|
||||
topMovers,
|
||||
bottomMovers,
|
||||
comparisonPeriods: { current: currentYear.toString(), previous: prevYear.toString() }
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export const aggregateData = (data: SalesRecord[]): AggregatedData => {
|
||||
const totalSellOut = data.reduce((acc, curr) => acc + curr.sellOut, 0);
|
||||
const totalUnits = data.reduce((acc, curr) => acc + curr.units, 0);
|
||||
|
||||
const totalsByYear: Record<string, { sellOut: number; units: number }> = {};
|
||||
data.forEach(item => {
|
||||
const y = item.year.toString();
|
||||
if (!totalsByYear[y]) totalsByYear[y] = { sellOut: 0, units: 0 };
|
||||
totalsByYear[y].sellOut += item.sellOut;
|
||||
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>();
|
||||
data.forEach(item => {
|
||||
customerMap.set(item.customer, (customerMap.get(item.customer) || 0) + item.sellOut);
|
||||
});
|
||||
const byCustomer = Array.from(customerMap.entries())
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const { seasonality, seasonalityUnits, years } = calculateSeasonality(data);
|
||||
const { topMovers, bottomMovers, comparisonPeriods } = calculateLineMovers(data); // Use calculateLineMovers
|
||||
const topLinesSplit = calculateTopLinesSplit(data);
|
||||
const byCustomerSplit = calculateGenericSplit(data, 'customer', 'sellOut');
|
||||
const byLineOverviewSplit = calculateGenericSplit(data, 'line', 'units', 10);
|
||||
|
||||
return {
|
||||
totalSellOut,
|
||||
totalUnits,
|
||||
totalsByYear,
|
||||
byLine,
|
||||
byCustomer,
|
||||
seasonality,
|
||||
seasonalityUnits,
|
||||
availableYears: years,
|
||||
topMovers,
|
||||
bottomMovers,
|
||||
comparisonPeriods,
|
||||
topLinesSplit,
|
||||
byCustomerSplit,
|
||||
byLineOverviewSplit
|
||||
};
|
||||
};
|
||||
|
||||
export const getUniqueValues = (data: SalesRecord[], field: keyof SalesRecord): string[] => {
|
||||
const values = new Set(data.map(item => String(item[field])));
|
||||
return Array.from(values).sort();
|
||||
};
|
||||
|
||||
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
|
||||
const yearsSet = new Set(data.map(d => d.year));
|
||||
const years = Array.from(yearsSet).sort((a,b) => b-a).map(String);
|
||||
|
||||
const map = new Map<string, PivotRow>();
|
||||
|
||||
data.forEach(record => {
|
||||
// Group by Dynamic Dimensions
|
||||
const keyParts = dimensions.map(dim => String(record[dim as keyof SalesRecord] || ''));
|
||||
const key = keyParts.join('||');
|
||||
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
id: key,
|
||||
customer: dimensions.includes('customer') ? record.customer : '',
|
||||
line: dimensions.includes('line') ? record.line : '',
|
||||
title: dimensions.includes('title') ? record.title : '',
|
||||
articleName: dimensions.includes('articleName') ? record.articleName : '',
|
||||
sku: dimensions.includes('sku') ? record.sku : '',
|
||||
asin: dimensions.includes('asin') ? record.asin : '',
|
||||
// Initialize 12 months with empty year maps
|
||||
months: Array(12).fill(null).map((_, i) => ({
|
||||
monthIndex: i,
|
||||
byYear: {}
|
||||
})),
|
||||
totalsByYear: {}
|
||||
});
|
||||
}
|
||||
|
||||
const row = map.get(key)!;
|
||||
const monthPart = record.month;
|
||||
const monthIdx = MONTH_ORDER.indexOf(monthPart);
|
||||
const yearStr = record.year.toString();
|
||||
|
||||
// 1. Update Row Totals for Year
|
||||
if (!row.totalsByYear[yearStr]) {
|
||||
row.totalsByYear[yearStr] = { sellOut: 0, units: 0 };
|
||||
}
|
||||
row.totalsByYear[yearStr].sellOut += record.sellOut;
|
||||
row.totalsByYear[yearStr].units += record.units;
|
||||
|
||||
// 2. Update Monthly Data
|
||||
if (monthIdx !== -1) {
|
||||
const m = row.months[monthIdx];
|
||||
if (!m.byYear[yearStr]) {
|
||||
m.byYear[yearStr] = { sellOut: 0, units: 0 };
|
||||
}
|
||||
m.byYear[yearStr].sellOut += record.sellOut;
|
||||
m.byYear[yearStr].units += record.units;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
rows: Array.from(map.values()),
|
||||
years
|
||||
};
|
||||
};
|
||||
|
||||
export const generateCSV = (rows: PivotRow[], dimensions: string[], years: string[]) => {
|
||||
// Flatten PivotRows into CSV-friendly objects
|
||||
const flatData = rows.map(row => {
|
||||
const flatRow: any = {};
|
||||
|
||||
// Add Dimension Columns
|
||||
dimensions.forEach(dim => {
|
||||
// Map internal key to nicer Header if needed
|
||||
let header = dim;
|
||||
if (dim === 'line') header = 'Product Line';
|
||||
if (dim === 'title') header = 'Title';
|
||||
if (dim === 'customer') header = 'Customer';
|
||||
|
||||
flatRow[header] = row[dim as keyof PivotRow];
|
||||
});
|
||||
|
||||
// Add Yearly Totals
|
||||
years.forEach(year => {
|
||||
const data = row.totalsByYear[year];
|
||||
flatRow[`Total Sell Out ${year}`] = data?.sellOut || 0;
|
||||
flatRow[`Total Units ${year}`] = data?.units || 0;
|
||||
});
|
||||
|
||||
// Add Monthly Data
|
||||
row.months.forEach(m => {
|
||||
const monthName = MONTH_ORDER[m.monthIndex];
|
||||
years.forEach(year => {
|
||||
const data = m.byYear[year];
|
||||
flatRow[`${monthName} ${year} Sell Out`] = data?.sellOut || 0;
|
||||
flatRow[`${monthName} ${year} Units`] = data?.units || 0;
|
||||
});
|
||||
});
|
||||
|
||||
return flatRow;
|
||||
});
|
||||
|
||||
// Generate CSV string
|
||||
// @ts-ignore
|
||||
const csv = Papa.unparse(flatData);
|
||||
|
||||
// Trigger Download
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `sales_export_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
export const generateItemMoversCSV = (
|
||||
data: ItemGrowthMetric[],
|
||||
periods: { current: string; previous: string },
|
||||
type: 'Gainers' | 'Losers'
|
||||
) => {
|
||||
const flatData = data.map(item => ({
|
||||
SKU: item.sku || '-',
|
||||
ASIN: item.asin || '-',
|
||||
'Product Title': item.title || '-',
|
||||
'Product Line': item.line || '-',
|
||||
[`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}),
|
||||
'SO Diff': item.sellOutGrowthValue.toLocaleString('de-DE', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
|
||||
'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.current}`]: item.currentYearUnits.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}) + '%',
|
||||
}));
|
||||
|
||||
// @ts-ignore
|
||||
const csv = Papa.unparse(flatData);
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${type}_${periods.current}_vs_${periods.previous}_${new Date().toISOString().split('T')[0]}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
|
||||
export const aggregateForTimeSeries = (data: SalesRecord[]): TimeSeriesData[] => {
|
||||
const map = new Map<string, { sellOut: number; units: number }>();
|
||||
const recordsWithWeek = data.filter(r => r.week != null && r.year != null && r.week >= 1 && r.week <= 53);
|
||||
|
||||
if (recordsWithWeek.length === 0) return []; // No weekly data to process
|
||||
|
||||
recordsWithWeek.forEach(record => {
|
||||
// Create a sortable key YYYY-WW
|
||||
const weekStr = record.week!.toString().padStart(2, '0');
|
||||
const key = `${record.year}-${weekStr}`;
|
||||
|
||||
const current = map.get(key) || { sellOut: 0, units: 0 };
|
||||
current.sellOut += record.sellOut;
|
||||
current.units += record.units;
|
||||
map.set(key, current);
|
||||
});
|
||||
|
||||
// Convert map to array and sort chronologically
|
||||
return Array.from(map.entries())
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, values]) => {
|
||||
const [year, weekNum] = key.split('-');
|
||||
const yearShort = year.substring(2);
|
||||
|
||||
return {
|
||||
name: `W${weekNum} '${yearShort}`,
|
||||
sellOut: values.sellOut,
|
||||
units: values.units
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const aggregateForComparisonTimeSeries = (data: SalesRecord[]): ComparisonTimeSeriesPoint[] => {
|
||||
const map = new Map<number, { [key: string]: number }>(); // Key is week number
|
||||
const years = Array.from(new Set(data.map(d => d.year)));
|
||||
|
||||
// Initialize map for all 53 possible weeks to ensure a consistent X-axis
|
||||
for (let i = 1; i <= 53; i++) {
|
||||
const initialWeekData: { [key: string]: number } = {};
|
||||
years.forEach(year => {
|
||||
initialWeekData[`${year}_sellOut`] = 0;
|
||||
initialWeekData[`${year}_units`] = 0;
|
||||
});
|
||||
map.set(i, initialWeekData);
|
||||
}
|
||||
|
||||
data.forEach(record => {
|
||||
if (record.week != null && record.year != null && record.week >= 1 && record.week <= 53) {
|
||||
const weekData = map.get(record.week)!;
|
||||
|
||||
const sellOutKey = `${record.year}_sellOut`;
|
||||
const unitsKey = `${record.year}_units`;
|
||||
|
||||
weekData[sellOutKey] = (weekData[sellOutKey] || 0) + record.sellOut;
|
||||
weekData[unitsKey] = (weekData[unitsKey] || 0) + record.units;
|
||||
|
||||
map.set(record.week, weekData);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map to array, filter out weeks with no data across all years, and sort
|
||||
return Array.from(map.entries())
|
||||
.map(([week, values]) => ({
|
||||
week,
|
||||
name: `W${week}`,
|
||||
...values,
|
||||
}))
|
||||
.filter(d => {
|
||||
// Check if there is any non-zero value for this week
|
||||
return Object.values(d).some(val => typeof val === 'number' && val > 0);
|
||||
})
|
||||
.sort((a, b) => a.week - b.week);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { AggregatedData } from "../types";
|
||||
|
||||
const SYSTEM_INSTRUCTION = `
|
||||
You are an expert Data Analyst Assistant for "Craze Analytix".
|
||||
You have access to a structured dataset of sales performance including Revenue (Sell Out), Units, Product Lines, and Seasonality.
|
||||
|
||||
Your Capabilities:
|
||||
1. **Analyze Trends**: Use the provided Seasonality and Yearly Breakdown data.
|
||||
2. **Perform Calculations**: You have access to detailed Product Line totals. You MUST calculate growth percentages, market shares, and sums dynamically if the user asks.
|
||||
3. **Compare**: Compare performance between years (e.g., 2024 vs 2025).
|
||||
|
||||
Rules:
|
||||
- If the user asks for a calculation (e.g., "What is the % share of Line X?"), perform the math using the provided numbers.
|
||||
- Always format currency as € (e.g., €1,200) and units with 'u' or 'units' (e.g., 500 units).
|
||||
- Be concise but insightful. Point out significant growth or decline.
|
||||
- If data is missing for a specific query, state clearly that it is not in the current filtered view.
|
||||
`;
|
||||
|
||||
const formatCurrency = (val: number) => `€${val.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}`;
|
||||
const formatUnits = (val: number) => `${val.toLocaleString()} units`;
|
||||
|
||||
export const queryGemini = async (
|
||||
apiKey: string,
|
||||
question: string,
|
||||
context: AggregatedData,
|
||||
filteredRecordCount: number
|
||||
): Promise<string> => {
|
||||
|
||||
if (!apiKey) {
|
||||
return "Please provide your Gemini API Key in the settings to enable the AI assistant.";
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure the key is clean of whitespace
|
||||
const ai = new GoogleGenAI({ apiKey: apiKey.trim() });
|
||||
|
||||
// --- CONTEXT GENERATION ---
|
||||
// We construct a structured report mirroring the dashboard charts
|
||||
|
||||
// 1. Totals by Year (KPI Cards)
|
||||
const yearlySummary = Object.entries(context.totalsByYear)
|
||||
.sort((a, b) => parseInt(b[0]) - parseInt(a[0])) // Descending years
|
||||
.map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`)
|
||||
.join('\n');
|
||||
|
||||
// 2. Seasonality (Line Chart Data)
|
||||
const seasonalitySummary = context.seasonality.map(p => {
|
||||
const yearValues = context.availableYears.map(y => `${y}: ${formatCurrency(p[y] as number || 0)}`).join(', ');
|
||||
return ` - ${p.name}: [${yearValues}]`;
|
||||
}).join('\n');
|
||||
|
||||
// 3. Growth/Decline
|
||||
const growthSummary = context.topMovers.slice(0, 10).map(m =>
|
||||
` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString()} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
|
||||
).join('\n');
|
||||
|
||||
const declineSummary = context.bottomMovers.slice(0, 10).map(m =>
|
||||
` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString()} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
|
||||
).join('\n');
|
||||
|
||||
// 4. DETAILED BREAKDOWN (For Calculations)
|
||||
// We provide a JSON-like structure of ALL top product lines so the AI can compute shares/totals.
|
||||
// We limit this to top 100 to avoid token limits, which covers most relevant data.
|
||||
const detailedLines = context.byLine.slice(0, 100).map(l => ({
|
||||
name: l.name,
|
||||
revenue: l.value,
|
||||
units: l.units
|
||||
}));
|
||||
|
||||
const fullReport = `
|
||||
REPORT CONTEXT (Based on Current Filters):
|
||||
------------------------------------------
|
||||
GLOBAL METRICS:
|
||||
Total Sell Out: ${formatCurrency(context.totalSellOut)}
|
||||
Total Units: ${formatUnits(context.totalUnits)}
|
||||
Records Analyzed: ${filteredRecordCount}
|
||||
Years Available: ${context.availableYears.join(', ')}
|
||||
|
||||
YEARLY TOTALS:
|
||||
${yearlySummary}
|
||||
|
||||
MONTHLY TRENDS (Seasonality):
|
||||
${seasonalitySummary}
|
||||
|
||||
TOP PERFORMERS (Growth YoY):
|
||||
${growthSummary}
|
||||
|
||||
WORST PERFORMERS (Decline YoY):
|
||||
${declineSummary}
|
||||
|
||||
DETAILED PRODUCT LINE DATA (Use this for specific calculations):
|
||||
${JSON.stringify(detailedLines, null, 2)}
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview', // Updated to the latest capable model for complex reasoning
|
||||
contents: [
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: `Context Data:\n${fullReport}\n\nUser Question: ${question}` }]
|
||||
}
|
||||
],
|
||||
config: {
|
||||
systemInstruction: SYSTEM_INSTRUCTION,
|
||||
}
|
||||
});
|
||||
|
||||
return response.text || "I couldn't generate a response based on the data provided.";
|
||||
} catch (error: any) {
|
||||
console.error("Gemini API Error:", error);
|
||||
|
||||
if (error.message && error.message.includes("403")) {
|
||||
return "Error 403: Invalid API Key. Please check your key in the settings.";
|
||||
}
|
||||
if (error.message && error.message.includes("429")) {
|
||||
return "Error 429: Quota exceeded. You are sending too many requests.";
|
||||
}
|
||||
|
||||
return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
|
||||
import { SalesRecord } from '../types';
|
||||
|
||||
const DB_NAME = 'CrazeAnalytixDB';
|
||||
const STORE_NAME = 'salesData';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
const initDB = (): Promise<IDBDatabase> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
};
|
||||
|
||||
export const saveSalesData = async (data: SalesRecord[]): Promise<void> => {
|
||||
try {
|
||||
const db = await initDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
// Store the data array
|
||||
store.put(data, 'currentData');
|
||||
// Store the timestamp
|
||||
store.put(new Date().toISOString(), 'lastUpdated');
|
||||
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving to IndexedDB:", error);
|
||||
// Fallback or silence error (data just won't be cached)
|
||||
}
|
||||
};
|
||||
|
||||
export const loadSalesData = async (): Promise<{ data: SalesRecord[]; lastUpdated: string | null }> => {
|
||||
try {
|
||||
const db = await initDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
const dataReq = store.get('currentData');
|
||||
const dateReq = store.get('lastUpdated');
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
resolve({
|
||||
data: dataReq.result || [],
|
||||
lastUpdated: dateReq.result || null
|
||||
});
|
||||
};
|
||||
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error loading from IndexedDB:", error);
|
||||
return { data: [], lastUpdated: null };
|
||||
}
|
||||
};
|
||||
|
||||
export const clearSalesData = async (): Promise<void> => {
|
||||
try {
|
||||
const db = await initDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
store.clear();
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user