Rapport_React/src/views/ivareport/exportXlsx.js

244 lines
9.1 KiB
JavaScript

import { COLOR_ID, COLOR_META, COLOR_METRIC, COLOR_SKU, excelNumber, metricsByAttribute } from './helpers';
function excelCol(index) {
let letter = '';
let value = index;
while (value > 0) {
value -= 1;
letter = String.fromCharCode(65 + (value % 26)) + letter;
value = Math.floor(value / 26);
}
return letter;
}
function colIndex(letters) {
return letters.split('').reduce((sum, char) => sum * 26 + (char.charCodeAt(0) - 64), 0);
}
function parseCell(ref) {
const match = /^([A-Z]+)(\d+)$/.exec(ref);
return { col: colIndex(match[1]), row: Number(match[2]) };
}
function parseRange(range) {
const [from, to = from] = range.split(':');
const start = parseCell(from);
const end = parseCell(to);
return { top: start.row, left: start.col, bottom: end.row, right: end.col };
}
// Laravel Excel (strict_null_comparison false) skips values loosely equal to null, so 0 and '' stay blank.
// PhpSpreadsheet's default binder stores numeric text as numbers, except values with a leading zero.
function cellValue(value) {
if (value === null || value === undefined || value === '' || value === 0 || value === false) return null;
if (typeof value !== 'string' || !/^[+-]?(\d+\.?\d*|\d*\.?\d+)([Ee][-+]?[0-2]?\d{1,3})?$/.test(value)) return value;
const digits = value.replace(/^[+-]+/, '');
if (digits.length > 1 && digits[0] === '0' && digits[1] !== '.') return value;
if (!value.includes('.') && !Number.isSafeInteger(Number(value))) return value;
return Number(value);
}
function plannerExport(table) {
const { brandGroups, skuColumns, rows } = table;
const row1 = ['Territory', 'KAM Emp Code', 'KAM Name'];
const row2 = ['', '', ''];
const merges = ['A1:A2', 'B1:B2', 'C1:C2'];
const fills = [['A1:C2', COLOR_ID]];
let col = 4;
brandGroups.forEach((group) => {
const skus = skuColumns[group.name] ?? [];
const span = 3 + skus.length * 3;
const start = excelCol(col);
const end = excelCol(col + span - 1);
merges.push(`${start}1:${end}1`);
fills.push([`${start}1:${end}1`, group.style]);
fills.push([`${start}2:${end}2`, COLOR_METRIC]);
row1.push(group.name, ...Array(span - 1).fill(''));
row2.push('POB Plan (Val)', 'POB Done (Val)', '% POB');
skus.forEach((sku) => row2.push(sku.planAttr, sku.doneAttr, sku.pctLabel));
col += span;
});
const data = [row1, row2];
rows.forEach((row) => {
const line = [row.territory, row.kamEmpCode, row.kamName];
brandGroups.forEach((group) => {
const brand = row.brands[group.name] ?? {};
line.push(excelNumber(brand.plan ?? 0), excelNumber(brand.done ?? 0), excelNumber(brand.pct ?? 0, true));
const planMap = metricsByAttribute(brand.planMetrics);
const doneMap = metricsByAttribute(brand.doneMetrics);
(skuColumns[group.name] ?? []).forEach((sku) => {
const planVal = planMap[sku.planAttr] ?? 0;
const doneVal = doneMap[sku.doneAttr] ?? 0;
const pct = Number(planVal) > 0 ? Math.round((Number(doneVal) / Number(planVal)) * 100 * 100) / 100 : 0;
line.push(excelNumber(planVal), excelNumber(doneVal), excelNumber(pct, true));
});
});
data.push(line);
});
return { data, merges, fills, headerRows: 2, freeze: 'D3' };
}
function kamExport(table) {
const { brandColumns, skuColumns, rows } = table;
const headers = ['Territory', 'KAM Emp Code', 'KAM Name', 'Total Brand POB Val'];
const fills = [['A1:C1', COLOR_ID], ['D1:D1', COLOR_METRIC]];
let col = 5;
brandColumns.forEach(({ key }) => {
headers.push(key);
fills.push([`${excelCol(col)}1`, COLOR_METRIC]);
col += 1;
(skuColumns[key] ?? []).forEach((attr) => {
headers.push(attr);
fills.push([`${excelCol(col)}1`, COLOR_SKU]);
col += 1;
});
});
const data = [headers];
rows.forEach((row) => {
const line = [row.territory, row.kamEmpCode, row.kamName, excelNumber(row.total ?? 0)];
brandColumns.forEach(({ key }) => {
line.push(excelNumber(row.values[key] ?? 0));
const metricMap = metricsByAttribute(row.metrics[key]);
(skuColumns[key] ?? []).forEach((attr) => line.push(excelNumber(metricMap[attr] ?? 0)));
});
data.push(line);
});
return { data, merges: [], fills, headerRows: 1, freeze: 'D2' };
}
function drBrandExport(table) {
const { brandColumns, rows } = table;
const headers = ['Dr Name', 'Dr UID', 'Dr SBU', 'Total Brand POB Val', ...brandColumns.map((col) => col.key)];
const fills = [['A1:A1', COLOR_ID], ['B1:C1', COLOR_META], [`D1:${excelCol(headers.length)}1`, COLOR_METRIC]];
const data = [headers];
rows.forEach((row) => {
data.push([
row.drName,
row.drUid,
row.drSbu,
excelNumber(row.total ?? 0),
...brandColumns.map((col) => excelNumber(row.values[col.key] ?? 0)),
]);
});
return { data, merges: [], fills, headerRows: 1, freeze: 'B2' };
}
function skuExport(table) {
const { skuGroups, rows } = table;
const row1 = ['Dr Name'];
const row2 = [''];
const merges = ['A1:A2'];
const fills = [['A1:A2', COLOR_ID]];
let col = 2;
skuGroups.forEach((group) => {
const span = Math.max(1, group.columns.length);
const start = excelCol(col);
const end = excelCol(col + span - 1);
merges.push(`${start}1:${end}1`);
fills.push([`${start}1:${end}1`, group.style ?? COLOR_METRIC]);
fills.push([`${start}2:${end}2`, COLOR_METRIC]);
row1.push(group.name, ...Array(span - 1).fill(''));
group.columns.forEach((sku) => row2.push(sku.key));
col += span;
});
const data = [row1, row2];
rows.forEach((row) => {
const line = [row.drName];
skuGroups.forEach((group) => group.columns.forEach((sku) => line.push(excelNumber(row.values[sku.key] ?? 0, true))));
data.push(line);
});
return { data, merges, fills, headerRows: 2, freeze: 'B3' };
}
const BUILDERS = {
planner: plannerExport,
kamwise: kamExport,
drbrandwise: drBrandExport,
skuwise: skuExport,
};
function eachInRange(sheet, range, visit) {
for (let row = range.top; row <= range.bottom; row += 1) {
for (let col = range.left; col <= range.right; col += 1) visit(sheet.getCell(row, col));
}
}
// Approximates Maatwebsite ShouldAutoSize; cells merged across columns are skipped like PhpSpreadsheet.
function autoSize(sheet, data, merges, colCount) {
const merged = new Set();
merges.map(parseRange).forEach((range) => {
if (range.right === range.left) return;
for (let row = range.top; row <= range.bottom; row += 1) {
for (let col = range.left; col <= range.right; col += 1) merged.add(`${row}:${col}`);
}
});
for (let col = 1; col <= colCount; col += 1) {
let longest = 0;
data.forEach((line, index) => {
if (merged.has(`${index + 1}:${col}`)) return;
const value = line[col - 1];
if (value === null || value === undefined || value === '') return;
const length = Math.max(...String(value).split('\n').map((part) => part.length));
if (length > longest) longest = length;
});
sheet.getColumn(col).width = Math.max(Math.ceil(longest * 1.1) + 2, 8);
}
}
function freezeView(ref) {
const { col, row } = parseCell(ref);
return [{ state: 'frozen', xSplit: col - 1, ySplit: row - 1 }];
}
export async function exportIvaReport(tab, table, filename) {
const { data, merges, fills, headerRows, freeze } = BUILDERS[tab](table);
const { default: ExcelJS } = await import('exceljs');
const workbook = new ExcelJS.Workbook();
const sheet = workbook.addWorksheet('Worksheet');
const values = data.map((line) => line.map(cellValue));
values.forEach((line) => sheet.addRow(line));
const rowCount = Math.max(1, data.length);
const colCount = Math.max(1, ...data.map((line) => line.length));
autoSize(sheet, values, merges, colCount);
merges.forEach((range) => sheet.mergeCells(range));
const thin = { style: 'thin', color: { argb: 'FFB0B0B0' } };
eachInRange(sheet, { top: 1, left: 1, bottom: rowCount, right: colCount }, (cell) => {
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
cell.border = { top: thin, left: thin, bottom: thin, right: thin };
cell.font = { name: 'Calibri', size: 11 };
});
if (headerRows > 0) {
eachInRange(sheet, { top: 1, left: 1, bottom: headerRows, right: colCount }, (cell) => {
cell.font = { ...cell.font, bold: true };
});
sheet.getRow(1).height = 22;
if (headerRows > 1) sheet.getRow(2).height = 30;
}
fills.forEach(([range, color]) => {
eachInRange(sheet, parseRange(range), (cell) => {
cell.font = { ...cell.font, bold: true, color: { argb: `FF${color.fg ?? '000000'}` } };
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: `FF${color.bg ?? 'FFFFFF'}` } };
cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
});
});
sheet.views = freezeView(freeze);
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(blob);
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(anchor.href);
}