Spaces:
Configuration error
Configuration error
| // βββ Default Named Ranges ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const DEFAULT_NAMED_RANGES = [ | |
| { name: 'REVENUE', range: 'SalesData!$I:$I' }, | |
| { name: 'COGS', range: 'SalesData!$J:$J' }, | |
| { name: 'GROSSPROFIT',range: 'SalesData!$K:$K' }, | |
| { name: 'EXPENSES', range: 'SalesData!$L:$L' }, | |
| { name: 'NETPROFIT', range: 'SalesData!$M:$M' }, | |
| { name: 'MARGIN', range: 'SalesData!$N:$N' }, | |
| { name: 'UNITSSOLD', range: 'SalesData!$G:$G' }, | |
| { name: 'UNITPRICE', range: 'SalesData!$H:$H' }, | |
| { name: 'REGION', range: 'SalesData!$D:$D' }, | |
| { name: 'PRODUCT', range: 'SalesData!$C:$C' }, | |
| { name: 'MANAGER', range: 'SalesData!$E:$E' }, | |
| { name: 'QUARTER', range: 'SalesData!$F:$F' }, | |
| ]; | |
| // βββ Column name lookup βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Maps EXACT visual column position A-O to the field name. | |
| // Based on SALES_COL_DEFS order: | |
| // A=S.No(null), B=id, C=product, D=region, E=manager, F=quarter, | |
| // G=unitsSold, H=unitPrice, I=revenue, J=cogs, K=grossProfit, | |
| // L=expenses, M=netProfit, N=margin, O=status | |
| export const colNames = [ | |
| null, // A: S.No. (no data field) | |
| 'id', // B | |
| 'product', // C | |
| 'region', // D | |
| 'manager', // E | |
| 'quarter', // F | |
| 'unitsSold', // G | |
| 'unitPrice', // H | |
| 'revenue', // I | |
| 'cogs', // J | |
| 'grossProfit', // K | |
| 'expenses', // L | |
| 'netProfit', // M | |
| 'margin', // N | |
| 'status', // O | |
| ]; | |
| // Maps named-range strings to their field key | |
| const RANGE_TO_FIELD = { | |
| 'SALESDATA!$A:$A': 'sno', 'SALESDATA!$B:$B': 'id', | |
| 'SALESDATA!$C:$C': 'product', 'SALESDATA!$D:$D': 'region', | |
| 'SALESDATA!$E:$E': 'manager', 'SALESDATA!$F:$F': 'quarter', | |
| 'SALESDATA!$G:$G': 'unitsSold','SALESDATA!$H:$H': 'unitPrice', | |
| 'SALESDATA!$I:$I': 'revenue', 'SALESDATA!$J:$J': 'cogs', | |
| 'SALESDATA!$K:$K': 'grossProfit', 'SALESDATA!$L:$L': 'expenses', | |
| 'SALESDATA!$M:$M': 'netProfit', 'SALESDATA!$N:$N': 'margin', | |
| 'SALESDATA!$O:$O': 'status', | |
| // Also without $ signs | |
| 'SALESDATA!A:A': 'sno', 'SALESDATA!B:B': 'id', | |
| 'SALESDATA!C:C': 'product', 'SALESDATA!D:D': 'region', | |
| 'SALESDATA!E:E': 'manager', 'SALESDATA!F:F': 'quarter', | |
| 'SALESDATA!G:G': 'unitsSold', 'SALESDATA!H:H': 'unitPrice', | |
| 'SALESDATA!I:I': 'revenue', 'SALESDATA!J:J': 'cogs', | |
| 'SALESDATA!K:K': 'grossProfit', 'SALESDATA!L:L': 'expenses', | |
| 'SALESDATA!M:M': 'netProfit', 'SALESDATA!N:N': 'margin', | |
| 'SALESDATA!O:O': 'status', | |
| }; | |
| // βββ evaluateField ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const evaluateField = (data, field) => { | |
| if (!data) return 0; | |
| const raw = data[field]; | |
| if (typeof raw === 'string' && raw.startsWith('=')) { | |
| try { | |
| const u = Number(data.unitsSold) || 0; | |
| const p = Number(data.unitPrice) || 0; | |
| const rev = u * p; | |
| if (field === 'revenue') return rev; | |
| if (field === 'cogs') return rev * 0.4; | |
| if (field === 'grossProfit')return rev * 0.6; | |
| if (field === 'expenses') return rev * 0.1; | |
| if (field === 'netProfit') return rev * 0.5; | |
| if (field === 'margin') return rev > 0 ? 50 : 0; | |
| } catch { return 0; } | |
| } | |
| return Number(raw) || 0; | |
| }; | |
| // βββ Column letter β 0-based index βββββββββββββββββββββββββββββββββββββββββββ | |
| export const getColIdx = (letter) => { | |
| let idx = 0; | |
| for (let i = 0; i < letter.length; i++) { | |
| idx = idx * 26 + (letter.charCodeAt(i) - 64); | |
| } | |
| return idx - 1; | |
| }; | |
| // βββ Resolve a named-range string to a numeric value or numeric array βββββββββ | |
| const resolveNamedRangeToData = (rangeStr, salesData, currentCols = colNames) => { | |
| if (!rangeStr || !salesData) return null; | |
| const s = rangeStr.trim(); | |
| // Scalar number | |
| const asNum = Number(s); | |
| if (!isNaN(asNum) && s !== '') return asNum; | |
| // Is it a known full-column reference? (e.g. SalesData!$H:$H) | |
| const normKey = s.replace(/\s/g, '').toUpperCase(); | |
| if (RANGE_TO_FIELD[normKey]) { | |
| const fk = RANGE_TO_FIELD[normKey]; | |
| return salesData.map(r => Number(evaluateField(r, fk)) || 0); | |
| } | |
| // Generic full-column: Sheet!$COL:$COL or Sheet!COL:COL | |
| const fullColMatch = s.match(/(?:[a-zA-Z0-9_]+!)?\$?([A-Za-z]+):\$?([A-Za-z]+)$/); | |
| if (fullColMatch && fullColMatch[1].toUpperCase() === fullColMatch[2].toUpperCase()) { | |
| const col = getColIdx(fullColMatch[1].toUpperCase()); | |
| const fieldKey = currentCols[col]; | |
| if (fieldKey) return salesData.map(r => Number(evaluateField(r, fieldKey)) || 0); | |
| } | |
| // Range with row bounds: Sheet!$B$2:$B$5000 or B2:B500 | |
| const rangeMatch = s.match(/(?:[a-zA-Z0-9_]+!)?\$?([A-Za-z]+)\$?(\d+):\$?([A-Za-z]+)\$?(\d+)/); | |
| if (rangeMatch) { | |
| const col = getColIdx(rangeMatch[1].toUpperCase()); | |
| const rowStart = Math.max(0, parseInt(rangeMatch[2], 10) - 1); | |
| const rowEnd = Math.max(0, parseInt(rangeMatch[4], 10) - 1); | |
| const fieldKey = currentCols[col]; | |
| if (fieldKey) { | |
| const vals = []; | |
| const start = Math.min(rowStart, rowEnd); | |
| const end = Math.max(rowStart, rowEnd); | |
| for (let i = start; i <= end && i < salesData.length; i++) { | |
| vals.push(Number(evaluateField(salesData[i], fieldKey)) || 0); | |
| } | |
| return vals; | |
| } | |
| } | |
| return null; | |
| }; | |
| // βββ Resolve a named range to its STRING array (for text-based COUNTIF etc.) ββ | |
| const resolveNamedRangeToStrings = (rangeStr, salesData, currentCols = colNames) => { | |
| if (!rangeStr || !salesData) return null; | |
| const s = rangeStr.trim(); | |
| const normKey = s.replace(/\s/g, '').toUpperCase(); | |
| if (RANGE_TO_FIELD[normKey]) { | |
| const fk = RANGE_TO_FIELD[normKey]; | |
| return salesData.map(r => String(r[fk] ?? '')); | |
| } | |
| const fullColMatch = s.match(/(?:[a-zA-Z0-9_]+!)?\$?([A-Za-z]+):\$?([A-Za-z]+)$/); | |
| if (fullColMatch && fullColMatch[1].toUpperCase() === fullColMatch[2].toUpperCase()) { | |
| const col = getColIdx(fullColMatch[1].toUpperCase()); | |
| const fk = currentCols[col]; | |
| if (fk) return salesData.map(r => String(r[fk] ?? '')); | |
| } | |
| return null; | |
| }; | |
| // βββ Main formula evaluator βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const evaluateSummaryFormula = (formulaStr, salesData, metricName = '', customRanges = [], visibleCols = null) => { | |
| if (!formulaStr || !formulaStr.startsWith('=')) return formulaStr; | |
| const sd = salesData || []; | |
| const currentCols = visibleCols || colNames; | |
| let expr = formulaStr.substring(1).trim(); | |
| const rawForm = ('=' + expr).toUpperCase().replace(/\s/g, ''); | |
| // ββ Built-in keyMap (bareword β field) βββββββββββββββββββββββββββββββββββββ | |
| const keyMap = { | |
| REVENUE: 'revenue', COGS: 'cogs', GROSSPROFIT: 'grossProfit', | |
| EXPENSES: 'expenses', NETPROFIT: 'netProfit', MARGIN: 'margin', | |
| UNITSSOLD: 'unitsSold', UNITPRICE: 'unitPrice', | |
| REGION: 'region', PRODUCT: 'product', MANAGER: 'manager', | |
| QUARTER: 'quarter', STATUS: 'status', | |
| }; | |
| // ββ Merge built-in defaults + custom named ranges βββββββββββββββββββββββββ | |
| const builtinRanges = DEFAULT_NAMED_RANGES.map(r => ({ name: r.name, range: r.range })); | |
| const allRanges = [...builtinRanges, ...(customRanges || [])]; | |
| // Build resolved map: NAME β numeric(array) and string array | |
| const resolvedNum = {}; // name β number | number[] | |
| const resolvedStr = {}; // name β string[] | |
| allRanges.forEach(({ name, range }) => { | |
| if (!name || !range) return; | |
| const k = name.toUpperCase(); | |
| resolvedNum[k] = resolveNamedRangeToData(range, sd, currentCols); | |
| resolvedStr[k] = resolveNamedRangeToStrings(range, sd, currentCols); | |
| }); | |
| // ββ INDEX/MATCH shortcuts βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if (rawForm === '=INDEX(REGION,MATCH(MAX(REVENUE),REVENUE,0))') { | |
| const regionMap = {}; | |
| sd.forEach(r => { regionMap[r.region] = (regionMap[r.region] || 0) + evaluateField(r, 'revenue'); }); | |
| return Object.entries(regionMap).sort((a, b) => b[1] - a[1])[0]?.[0] || '-'; | |
| } | |
| if (rawForm === '=INDEX(PRODUCT,MATCH(MAX(UNITSSOLD),UNITSSOLD,0))') { | |
| const prodMap = {}; | |
| sd.forEach(r => { prodMap[r.product] = (prodMap[r.product] || 0) + (Number(r.unitsSold) || 0); }); | |
| return Object.entries(prodMap).sort((a, b) => b[1] - a[1])[0]?.[0] || '-'; | |
| } | |
| if (rawForm === '=INDEX(MANAGER,MATCH(MAX(REVENUE),REVENUE,0))') { | |
| const mgMap = {}; | |
| sd.forEach(r => { mgMap[r.manager] = (mgMap[r.manager] || 0) + evaluateField(r, 'revenue'); }); | |
| return Object.entries(mgMap).sort((a, b) => b[1] - a[1])[0]?.[0] || '-'; | |
| } | |
| // ββ Helper: get numeric array from arg string βββββββββββββββββββββββββββββ | |
| const getNumArray = (arg) => { | |
| const a = arg.trim(); | |
| const nameKey = a.toUpperCase(); | |
| // Custom/built-in named range | |
| if (resolvedNum.hasOwnProperty(nameKey) && resolvedNum[nameKey] !== null) { | |
| const v = resolvedNum[nameKey]; | |
| return Array.isArray(v) ? v : [v]; | |
| } | |
| // Full-column ref | |
| const fullColMatch = a.match(/(?:[a-zA-Z0-9_]+!)?\$?([A-Za-z]+):\$?([A-Za-z]+)$/); | |
| if (fullColMatch && fullColMatch[1].toUpperCase() === fullColMatch[2].toUpperCase()) { | |
| const col = getColIdx(fullColMatch[1].toUpperCase()); | |
| const fk = currentCols[col]; | |
| if (fk) return sd.map(r => Number(evaluateField(r, fk)) || 0); | |
| } | |
| // Row-bound range | |
| const rangeMatch = a.match(/(?:[a-zA-Z0-9_]+!)?\$?([A-Za-z]+)\$?(\d+):\$?([A-Za-z]+)\$?(\d+)/); | |
| if (rangeMatch) { | |
| const col = getColIdx(rangeMatch[1].toUpperCase()); | |
| const rowStart = Math.max(0, parseInt(rangeMatch[2], 10) - 2); | |
| const rowEnd = Math.max(0, parseInt(rangeMatch[4], 10) - 2); | |
| const fk = currentCols[col]; | |
| if (fk) { | |
| const vals = []; | |
| for (let i = Math.min(rowStart, rowEnd); i <= Math.max(rowStart, rowEnd) && i < sd.length; i++) { | |
| vals.push(Number(evaluateField(sd[i], fk)) || 0); | |
| } | |
| return vals; | |
| } | |
| } | |
| // Built-in keyword | |
| const key = keyMap[nameKey]; | |
| if (key) return sd.map(r => evaluateField(r, key)); | |
| // Plain number | |
| const n = Number(a); | |
| if (!isNaN(n)) return [n]; | |
| return []; | |
| }; | |
| // Helper: get string array from arg | |
| const getStrArray = (arg) => { | |
| const a = arg.trim(); | |
| const nameKey = a.toUpperCase(); | |
| if (resolvedStr.hasOwnProperty(nameKey) && resolvedStr[nameKey] !== null) { | |
| return resolvedStr[nameKey]; | |
| } | |
| const key = keyMap[nameKey]; | |
| if (key) return sd.map(r => String(r[key] ?? '')); | |
| return []; | |
| }; | |
| // ββ Aggregate functions βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const agg = { | |
| SUM: arr => arr.reduce((a, b) => a + b, 0), | |
| AVERAGE: arr => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0, | |
| MAX: arr => arr.length ? Math.max(...arr) : 0, | |
| MIN: arr => arr.length ? Math.min(...arr) : 0, | |
| COUNT: arr => arr.length, | |
| MEDIAN: arr => { | |
| if (!arr.length) return 0; | |
| const s = [...arr].sort((a, b) => a - b); | |
| const m = Math.floor(s.length / 2); | |
| return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; | |
| }, | |
| STDEV: arr => { | |
| if (arr.length < 2) return 0; | |
| const mean = arr.reduce((a, b) => a + b, 0) / arr.length; | |
| return Math.sqrt(arr.reduce((s, v) => s + (v - mean) ** 2, 0) / (arr.length - 1)); | |
| }, | |
| }; | |
| // ββ IFERROR(expr, fallback) βββββββββββββββββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/IFERROR\(([^,]+),([^)]+)\)/gi, (_, e, fallback) => { | |
| try { | |
| const inner = evaluateSummaryFormula('=' + e.trim(), sd, '', customRanges); | |
| if (inner === '#ERROR' || inner === null || inner === undefined) return fallback.trim().replace(/^"|"$/g, ''); | |
| return inner; | |
| } catch { return fallback.trim().replace(/^"|"$/g, ''); } | |
| }); | |
| // ββ IF(condition, true_val, false_val) ββββββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/\bIF\(([^,]+),([^,]+),([^)]+)\)/gi, (_, cond, tv, fv) => { | |
| try { | |
| // eslint-disable-next-line | |
| const condResult = new Function(`return ${cond}`)(); | |
| return condResult ? tv.trim() : fv.trim(); | |
| } catch { return fv.trim(); } | |
| }); | |
| // ββ SUMIF(range, criteria, [sum_range]) βββββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/SUMIF\(([^,]+),([^,)]+)(?:,([^)]+))?\)/gi, (_, rng, crit, sumRng) => { | |
| const checkArr = getStrArray(rng.trim()); | |
| const valueArr = sumRng ? getNumArray(sumRng.trim()) : getNumArray(rng.trim()); | |
| const criteria = crit.trim().replace(/^"|"$/g, ''); | |
| let total = 0; | |
| checkArr.forEach((v, i) => { | |
| if (String(v).toLowerCase() === criteria.toLowerCase()) { | |
| total += valueArr[i] || 0; | |
| } | |
| }); | |
| return total; | |
| }); | |
| // ββ COUNTIF(range, criteria) ββββββββββββββββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/COUNTIF\(([^,]+),([^)]+)\)/gi, (_, rng, crit) => { | |
| const arr = getStrArray(rng.trim()); | |
| const criteria = crit.trim().replace(/^"|"$/g, ''); | |
| return arr.filter(v => String(v).toLowerCase() === criteria.toLowerCase()).length; | |
| }); | |
| // ββ AVERAGEIF(range, criteria, avg_range) βββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/AVERAGEIF\(([^,]+),([^,]+),([^)]+)\)/gi, (_, rng, crit, avgRng) => { | |
| const checkArr = getStrArray(rng.trim()); | |
| const valueArr = getNumArray(avgRng.trim()); | |
| const criteria = crit.trim().replace(/^"|"$/g, ''); | |
| const filtered = []; | |
| checkArr.forEach((v, i) => { | |
| if (String(v).toLowerCase() === criteria.toLowerCase()) filtered.push(valueArr[i] || 0); | |
| }); | |
| return filtered.length ? filtered.reduce((a, b) => a + b, 0) / filtered.length : 0; | |
| }); | |
| // ββ Standard aggregates: SUM/AVERAGE/MAX/MIN/COUNT/MEDIAN/STDEV βββββββββ | |
| expr = expr.replace(/(SUM|AVERAGE|MAX|MIN|COUNT|MEDIAN|STDEV)\(([^)]+)\)/gi, (_, func, args) => { | |
| const f = func.toUpperCase(); | |
| const arr = getNumArray(args); | |
| return arr.length ? agg[f](arr) : 0; | |
| }); | |
| // ββ Replace remaining bare custom/builtin names with their totals βββββββββ | |
| const allNameKeys = Object.keys(resolvedNum).sort((a, b) => b.length - a.length); | |
| if (allNameKeys.length > 0) { | |
| const customRegex = new RegExp( | |
| `\\b(${allNameKeys.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\b`, 'gi' | |
| ); | |
| expr = expr.replace(customRegex, m => { | |
| const val = resolvedNum[m.toUpperCase()]; | |
| if (val === null || val === undefined) return '0'; | |
| if (Array.isArray(val)) return val.reduce((a, b) => a + b, 0); | |
| return val; | |
| }); | |
| } | |
| // ββ Fallback: replace bareword keywords ββββββββββββββββββββββββββββββββββ | |
| expr = expr.replace(/\b([a-zA-Z]+)\b/g, match => { | |
| const key = keyMap[match.toUpperCase()]; | |
| if (key) return sd.map(r => evaluateField(r, key)).reduce((a, b) => a + b, 0); | |
| return match; | |
| }); | |
| // ββ Evaluate expression βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try { | |
| if (!/^[0-9.+\-*/()%\s,e]+$/i.test(expr)) return '#ERROR'; | |
| // eslint-disable-next-line | |
| const result = new Function(`return ${expr}`)(); | |
| if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) return '#ERROR'; | |
| const m = metricName.toLowerCase(); | |
| const f = formulaStr.toLowerCase(); | |
| // Check metric name AND formula column reference for count/unit fields | |
| const isCount = m.includes('units') || m.includes('count') || m.includes('qty') || | |
| m.includes('quantity') || m.includes('rows') || m.includes('id') || | |
| /unitssold|quantity|qty/i.test(f) || | |
| // Detect direct column G references (unitsSold) with or without SalesData! prefix | |
| /(?:[^!]*!)?\bg\d+/i.test(f) || /\bg:/i.test(f); | |
| const isPercent = m.includes('margin') || m.includes('%') || m.includes('rate') || m.includes('pct') || | |
| /margin/i.test(f); | |
| const isPrice = m.includes('price') || m.includes('avg') || /unitprice/i.test(f) || /(?:[^!]*!)?\bh\d+/i.test(f) || /\bh:/i.test(f); | |
| if (isPercent) return `${result.toFixed(2)}%`; | |
| if (isCount) return result.toLocaleString('en-US'); | |
| if (isPrice) return `$${result.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| return `$${result.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| } catch { | |
| return '#ERROR'; | |
| } | |
| }; | |
| // βββ getSalesData βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const getSalesData = () => { | |
| const regions = ['North', 'South', 'East', 'West', 'Central']; | |
| const products = ['Laptop', 'Monitor', 'Keyboard', 'Mouse', 'Headset', 'Webcam', 'Tablet', 'Printer', 'Scanner', 'Router']; | |
| const managers = ['Alice Johnson', 'Bob Smith', 'Carol White', 'David Brown', 'Eva Martinez']; | |
| const statuses = ['Completed', 'Pending', 'In Progress', 'Cancelled']; | |
| const rows = []; | |
| for (let i = 1; i <= 100000; i++) { | |
| const unitsSold = Math.floor(Math.random() * 500) + 50; | |
| const unitPrice = parseFloat((Math.random() * 990 + 10).toFixed(2)); | |
| const revenue = parseFloat((unitsSold * unitPrice).toFixed(2)); | |
| const costPct = 0.4 + Math.random() * 0.2; | |
| const cogs = parseFloat((revenue * costPct).toFixed(2)); | |
| const grossProfit= parseFloat((revenue - cogs).toFixed(2)); | |
| const expenses = parseFloat((revenue * (0.05 + Math.random() * 0.1)).toFixed(2)); | |
| const netProfit = parseFloat((grossProfit - expenses).toFixed(2)); | |
| const margin = parseFloat(((netProfit / revenue) * 100).toFixed(2)); | |
| const quarter = `Q${Math.ceil((i % 12 || 12) / 3)} ${2023 + Math.floor(i / 13)}`; | |
| rows.push({ | |
| id: i, | |
| product: products[i % products.length], | |
| region: regions[i % regions.length], | |
| manager: managers[i % managers.length], | |
| quarter, | |
| unitsSold, | |
| unitPrice, | |
| revenue, | |
| cogs, | |
| grossProfit, | |
| expenses, | |
| netProfit, | |
| margin, | |
| status: statuses[i % statuses.length], | |
| }); | |
| } | |
| return rows; | |
| }; | |
| // βββ computeSummary βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const computeSummary = (salesData, existingSummaryData = null, customRanges = [], visibleCols = null) => { | |
| const sd = salesData || []; | |
| if (existingSummaryData && existingSummaryData.length > 0) { | |
| return existingSummaryData.map(row => ({ | |
| ...row, | |
| value: evaluateSummaryFormula(row.formula, sd, row.metric, customRanges, visibleCols), | |
| })); | |
| } | |
| if (sd.length === 0) { | |
| return [ | |
| { metric: 'Total Revenue', formula: '=SUM(Revenue)', value: '$0.00', note: 'Sum of all revenue' }, | |
| { metric: 'Total COGS', formula: '=SUM(COGS)', value: '$0.00', note: 'Cost of Goods Sold' }, | |
| { metric: 'Gross Profit', formula: '=Revenue - COGS', value: '$0.00', note: 'Revenue minus COGS' }, | |
| { metric: 'Total Expenses', formula: '=SUM(Expenses)', value: '$0.00', note: 'Operating expenses' }, | |
| { metric: 'Net Profit', formula: '=GrossProfit - Expenses', value: '$0.00', note: 'Gross Profit minus Expenses' }, | |
| { metric: 'Avg Margin %', formula: '=AVERAGE(Margin)', value: '0.00%', note: 'Average net margin' }, | |
| { metric: 'Top Region', formula: '=INDEX(Region,MATCH(MAX(Revenue),Revenue,0))', value: '-', note: 'Region with max revenue' }, | |
| { metric: 'Top Product', formula: '=INDEX(Product,MATCH(MAX(UnitsSold),UnitsSold,0))', value: '-', note: 'Product with max units' }, | |
| { metric: 'Total Units Sold', formula: '=SUM(UnitsSold)', value: '0', note: 'Sum of all units sold' }, | |
| { metric: 'Avg Unit Price', formula: '=AVERAGE(UnitPrice)', value: '$0.00', note: 'Average selling price' }, | |
| ]; | |
| } | |
| const totalRevenue = sd.reduce((s, r) => s + evaluateField(r, 'revenue'), 0); | |
| const totalCOGS = sd.reduce((s, r) => s + evaluateField(r, 'cogs'), 0); | |
| const grossProfit = totalRevenue - totalCOGS; | |
| const totalExpenses = sd.reduce((s, r) => s + evaluateField(r, 'expenses'), 0); | |
| const netProfit = grossProfit - totalExpenses; | |
| const avgMargin = sd.length ? sd.reduce((s, r) => s + evaluateField(r, 'margin'), 0) / sd.length : 0; | |
| const totalUnits = sd.reduce((s, r) => s + (Number(r.unitsSold) || 0), 0); | |
| const avgPrice = sd.length ? sd.reduce((s, r) => s + (Number(r.unitPrice) || 0), 0) / sd.length : 0; | |
| const regionMap = {}; | |
| sd.forEach(r => { regionMap[r.region] = (regionMap[r.region] || 0) + evaluateField(r, 'revenue'); }); | |
| const topRegion = Object.entries(regionMap).sort((a, b) => b[1] - a[1])[0]?.[0] || '-'; | |
| const prodMap = {}; | |
| sd.forEach(r => { prodMap[r.product] = (prodMap[r.product] || 0) + (Number(r.unitsSold) || 0); }); | |
| const topProduct = Object.entries(prodMap).sort((a, b) => b[1] - a[1])[0]?.[0] || '-'; | |
| const fmt = n => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; | |
| return [ | |
| { metric: 'Total Revenue', formula: '=SUM(Revenue)', value: fmt(totalRevenue), note: 'Sum of all revenue' }, | |
| { metric: 'Total COGS', formula: '=SUM(COGS)', value: fmt(totalCOGS), note: 'Cost of Goods Sold' }, | |
| { metric: 'Gross Profit', formula: '=Revenue - COGS', value: fmt(grossProfit), note: 'Revenue minus COGS' }, | |
| { metric: 'Total Expenses', formula: '=SUM(Expenses)', value: fmt(totalExpenses), note: 'Operating expenses' }, | |
| { metric: 'Net Profit', formula: '=GrossProfit - Expenses', value: fmt(netProfit), note: 'Gross Profit minus Expenses' }, | |
| { metric: 'Avg Margin %', formula: '=AVERAGE(Margin)', value: `${avgMargin.toFixed(2)}%`, note: 'Average net margin' }, | |
| { metric: 'Top Region', formula: '=INDEX(Region,MATCH(MAX(Revenue),Revenue,0))', value: topRegion, note: 'Region with max revenue' }, | |
| { metric: 'Top Product', formula: '=INDEX(Product,MATCH(MAX(UnitsSold),UnitsSold,0))', value: topProduct, note: 'Product with max units sold' }, | |
| { metric: 'Total Units Sold', formula: '=SUM(UnitsSold)', value: totalUnits.toLocaleString(), note: 'Sum of all units sold' }, | |
| { metric: 'Avg Unit Price', formula: '=AVERAGE(UnitPrice)', value: `$${avgPrice.toFixed(2)}`, note: 'Average selling price' }, | |
| ]; | |
| }; | |
| // βββ getProjectData βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const getProjectData = (project) => { | |
| const data = { | |
| FINANCE: getSalesData(), | |
| BANKING: getSalesData().map(r => ({ ...r, product: r.product + ' Loan', region: r.region })), | |
| INSURANCE: getSalesData().map(r => ({ ...r, product: r.product + ' Policy', region: r.region })), | |
| }; | |
| return data[project] || []; | |
| }; | |
| // βββ Excel column-letter map for export ββββββββββββββββββββββββββββββββββββββ | |
| // fieldName β Excel column letter (1-indexed: A=1st col of SalesData sheet) | |
| export const FIELD_TO_EXCEL_COL = { | |
| id: 'A', product: 'B', region: 'C', manager: 'D', quarter: 'E', | |
| unitsSold: 'F', unitPrice: 'G', revenue: 'H', cogs: 'I', | |
| grossProfit: 'J', expenses: 'K', netProfit: 'L', margin: 'M', status: 'N', | |
| }; |