phosphat-report-app/app/utils/excelExport.ts
2025-07-24 12:39:15 +03:00

568 lines
22 KiB
TypeScript

import * as ExcelJS from 'exceljs';
import * as FileSaver from 'file-saver';
interface ReportData {
id: number;
createdDate: string;
shift: string;
area: { name: string };
dredgerLocation: { name: string };
dredgerLineLength: number;
reclamationLocation: { name: string };
shoreConnection: number;
reclamationHeight: { base: number; extra: number };
pipelineLength: { main: number; ext1: number; reserve: number; ext2: number };
stats: { Dozers: number; Exc: number; Loaders: number; Foreman: string; Laborer: number };
timeSheet: Array<{
machine: string;
from1: string;
to1: string;
from2: string;
to2: string;
total: string;
reason: string;
}>;
stoppages: Array<{
from: string;
to: string;
total: string;
reason: string;
responsible: string;
note: string;
}>;
notes: string;
}
export async function exportReportToExcel(report: ReportData) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Report');
// Set column widths to match the professional layout from the reference file
worksheet.columns = [
{ width: 30 }, // A - Labels/Machine names
{ width: 20 }, // B - Data/Values
{ width: 20 }, // C - Labels/Data
{ width: 20 }, // D - Data/Values
{ width: 15 }, // E - Pipeline data
{ width: 15 }, // F - Pipeline data
{ width: 25 } // G - Reason/Notes
];
let currentRow = 1;
// 1. HEADER SECTION - Professional layout matching reference file
// Main header with company info
worksheet.mergeCells(`A${currentRow}:E${currentRow + 2}`);
const headerCell = worksheet.getCell(`A${currentRow}`);
headerCell.value = 'Reclamation Work Diary';
headerCell.style = {
font: { name: 'Arial', size: 16, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Logo area
worksheet.mergeCells(`F${currentRow}:G${currentRow + 2}`);
const logoCell = worksheet.getCell(`F${currentRow}`);
logoCell.value = 'Arab Potash\nCompany Logo';
logoCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Sub-header info
const qfCell = worksheet.getCell(`A${currentRow + 1}`);
qfCell.value = 'QF-3.6.1-08';
qfCell.style = {
font: { name: 'Arial', size: 10 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thin', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thin', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const revCell = worksheet.getCell(`A${currentRow + 2}`);
revCell.value = 'Rev. 1.0';
revCell.style = {
font: { name: 'Arial', size: 10 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thin', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow += 4; // Skip to next section
// 2. REPORT INFO SECTION - Professional table layout
const infoRowCells = [
{ col: 'A', label: 'Date:', value: new Date(report.createdDate).toLocaleDateString('en-GB') },
{ col: 'C', label: 'Report No.', value: report.id.toString() }
];
// Create bordered info section
['A', 'B', 'C', 'D', 'E', 'F', 'G'].forEach(col => {
const cell = worksheet.getCell(`${col}${currentRow}`);
cell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
const dateCell = worksheet.getCell(`A${currentRow}`);
dateCell.value = 'Date:';
dateCell.style = {
font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const dateValueCell = worksheet.getCell(`B${currentRow}`);
dateValueCell.value = new Date(report.createdDate).toLocaleDateString('en-GB');
dateValueCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const reportNoCell = worksheet.getCell(`E${currentRow}`);
reportNoCell.value = 'Report No.';
reportNoCell.style = {
font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const reportNoValueCell = worksheet.getCell(`F${currentRow}`);
reportNoValueCell.value = report.id.toString();
reportNoValueCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow += 2; // Skip empty row
// 3. DREDGER SECTION - Professional centered title
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const dredgerCell = worksheet.getCell(`A${currentRow}`);
dredgerCell.value = `${report.area.name} Dredger`;
dredgerCell.style = {
font: { name: 'Arial', size: 18, bold: true, underline: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } }
};
currentRow += 2; // Skip empty row
// 4. LOCATION DATA SECTION - Professional table with green headers
const locationRows = [
['Dredger Location', report.dredgerLocation.name, '', 'Dredger Line Length', report.dredgerLineLength.toString()],
['Reclamation Location', report.reclamationLocation.name, '', 'Shore Connection', report.shoreConnection.toString()],
['Reclamation Height', `${report.reclamationHeight?.base || 0}m - ${(report.reclamationHeight?.base || 0) + (report.reclamationHeight?.extra || 0)}m`, '', '', '']
];
locationRows.forEach((rowData, index) => {
const row = currentRow + index;
// Apply styling to all cells in the row first
for (let col = 1; col <= 7; col++) {
const cell = worksheet.getCell(row, col);
cell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
rowData.forEach((cellValue, colIndex) => {
if (cellValue !== '') {
const cell = worksheet.getCell(row, colIndex + 1);
cell.value = cellValue;
const isGreenHeader = (colIndex === 0 || colIndex === 3);
cell.style = {
font: { name: 'Arial', size: 11, bold: isGreenHeader, color: isGreenHeader ? { argb: 'FFFFFF' } : { argb: '000000' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: isGreenHeader ? { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } } : { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
});
// Merge cells for better layout
if (index === 0) {
worksheet.mergeCells(`B${row}:C${row}`); // Dredger Location value
worksheet.mergeCells(`E${row}:G${row}`); // Dredger Line Length value
} else if (index === 1) {
worksheet.mergeCells(`B${row}:C${row}`); // Reclamation Location value
worksheet.mergeCells(`E${row}:G${row}`); // Shore Connection value
} else if (index === 2) {
worksheet.mergeCells(`B${row}:G${row}`); // Reclamation Height spans all remaining columns
}
});
currentRow += 4; // Skip empty row
// 5. PIPELINE LENGTH SECTION - Professional table with green headers
const pipelineHeaderRow = currentRow;
// First row - main header with rowspan
const mainHeaderCell = worksheet.getCell(pipelineHeaderRow, 1);
mainHeaderCell.value = 'Pipeline Length "from Shore Connection"';
mainHeaderCell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Sub-headers
const pipelineSubHeaders = ['Main', 'extension', 'total', 'Reserve', 'extension', 'total'];
pipelineSubHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(pipelineHeaderRow, colIndex + 2);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 10, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
// Data row
const pipelineDataRow = currentRow + 1;
const pipelineData = ['',
(report.pipelineLength?.main || 0).toString(),
(report.pipelineLength?.ext1 || 0).toString(),
((report.pipelineLength?.main || 0) + (report.pipelineLength?.ext1 || 0)).toString(),
(report.pipelineLength?.reserve || 0).toString(),
(report.pipelineLength?.ext2 || 0).toString(),
((report.pipelineLength?.reserve || 0) + (report.pipelineLength?.ext2 || 0)).toString()
];
pipelineData.forEach((data, colIndex) => {
const cell = worksheet.getCell(pipelineDataRow, colIndex + 1);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
currentRow += 4; // Skip empty row
// 6. SHIFT HEADER SECTION - Professional full-width header
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const shiftCell = worksheet.getCell(`A${currentRow}`);
shiftCell.value = `${report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift`;
shiftCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow += 2; // Skip empty row
// 7. EQUIPMENT STATS SECTION - Professional table with green headers
const equipmentHeaders = ['Dozers', 'Exc.', 'Loader', 'Foreman', 'Laborer'];
// Apply borders to all cells in the equipment section
for (let col = 1; col <= 7; col++) {
for (let row = currentRow; row <= currentRow + 1; row++) {
const cell = worksheet.getCell(row, col);
cell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
}
equipmentHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
const equipmentData = [
(report.stats?.Dozers || 0).toString(),
(report.stats?.Exc || 0).toString(),
(report.stats?.Loaders || 0).toString(),
report.stats?.Foreman || '',
(report.stats?.Laborer || 0).toString()
];
equipmentData.forEach((data, colIndex) => {
const cell = worksheet.getCell(currentRow + 1, colIndex + 1);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
currentRow += 4; // Skip empty row
// 8. TIME SHEET SECTION - Professional table
const createProfessionalTable = (headers: string[], data: any[][], startRow: number) => {
// Headers
headers.forEach((header, colIndex) => {
const cell = worksheet.getCell(startRow, colIndex + 1);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
// Data rows
data.forEach((rowData, rowIndex) => {
const row = startRow + rowIndex + 1;
rowData.forEach((cellData, colIndex) => {
const cell = worksheet.getCell(row, colIndex + 1);
cell.value = cellData;
cell.style = {
font: { name: 'Arial', size: 10, bold: colIndex === 0 },
alignment: { horizontal: colIndex === 0 ? 'left' : 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
});
return startRow + data.length + 1;
};
const timeSheetHeaders = ['Time Sheet', 'From', 'To', 'From', 'To', 'Total', 'Reason'];
const timeSheetData = Array.isArray(report.timeSheet) && report.timeSheet.length > 0
? report.timeSheet.map(entry => [entry.machine, entry.from1, entry.to1, entry.from2, entry.to2, entry.total, entry.reason])
: [['No time sheet entries', '', '', '', '', '', '']];
currentRow = createProfessionalTable(timeSheetHeaders, timeSheetData, currentRow);
currentRow += 2; // Skip empty row
// 9. STOPPAGES SECTION - Professional section with header
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const stoppagesHeaderCell = worksheet.getCell(`A${currentRow}`);
stoppagesHeaderCell.value = 'Dredger Stoppages';
stoppagesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow++;
const stoppagesHeaders = ['From', 'To', 'Total', 'Reason', 'Responsible', 'Notes', ''];
const stoppagesData = Array.isArray(report.stoppages) && report.stoppages.length > 0
? report.stoppages.map(entry => [entry.from, entry.to, entry.total, entry.reason, entry.responsible, entry.note, ''])
: [['No stoppages recorded', '', '', '', '', '', '']];
currentRow = createProfessionalTable(stoppagesHeaders, stoppagesData, currentRow);
currentRow += 2; // Skip empty row
// 10. NOTES SECTION - Professional notes section
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const notesHeaderCell = worksheet.getCell(`A${currentRow}`);
notesHeaderCell.value = 'Notes & Comments';
notesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow++;
worksheet.mergeCells(`A${currentRow}:G${currentRow + 3}`);
const notesContentCell = worksheet.getCell(`A${currentRow}`);
notesContentCell.value = report.notes || 'No additional notes';
notesContentCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'left', vertical: 'top', wrapText: true },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
currentRow += 6; // Skip to footer
// 11. FOOTER SECTION - Professional footer
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const footerCell = worksheet.getCell(`A${currentRow}`);
footerCell.value = 'موقعة لأعمال الصيانة';
footerCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Set row heights for professional appearance
worksheet.eachRow((row, rowNumber) => {
if (rowNumber <= 3) {
row.height = 25; // Header rows
} else if (row.getCell(1).value && typeof row.getCell(1).value === 'string' &&
(row.getCell(1).value.includes('Shift') ||
row.getCell(1).value.includes('Stoppages') ||
row.getCell(1).value.includes('Notes'))) {
row.height = 22; // Section headers
} else {
row.height = 18; // Standard rows
}
});
// Set print settings for professional output
worksheet.pageSetup = {
paperSize: 9, // A4
orientation: 'landscape',
fitToPage: true,
fitToWidth: 1,
fitToHeight: 0,
margins: {
left: 0.7,
right: 0.7,
top: 0.75,
bottom: 0.75,
header: 0.3,
footer: 0.3
}
};
// Generate and save file
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const fileName = `Report_${report.id}_${new Date(report.createdDate).toLocaleDateString('en-GB').replace(/\//g, '-')}.xlsx`;
FileSaver.saveAs(blob, fileName);
}