Report Profit Loss
This commit is contained in:
parent
79cd4f9dcb
commit
dc32c8553b
106
package-lock.json
generated
106
package-lock.json
generated
@ -74,7 +74,8 @@
|
|||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"recharts": "2.15.0",
|
"recharts": "2.15.0",
|
||||||
"use-debounce": "^10.0.5",
|
"use-debounce": "^10.0.5",
|
||||||
"valibot": "0.42.1"
|
"valibot": "0.42.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "2.2.286",
|
"@iconify/json": "2.2.286",
|
||||||
@ -3936,6 +3937,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@ -4660,6 +4670,19 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@ -4812,6 +4835,15 @@
|
|||||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@ -4953,6 +4985,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crelt": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
@ -6877,6 +6921,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@ -11093,6 +11146,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
||||||
@ -13045,6 +13110,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@ -13177,6 +13260,27 @@
|
|||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
|||||||
@ -80,7 +80,8 @@
|
|||||||
"react-use": "17.6.0",
|
"react-use": "17.6.0",
|
||||||
"recharts": "2.15.0",
|
"recharts": "2.15.0",
|
||||||
"use-debounce": "^10.0.5",
|
"use-debounce": "^10.0.5",
|
||||||
"valibot": "0.42.1"
|
"valibot": "0.42.1",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "2.2.286",
|
"@iconify/json": "2.2.286",
|
||||||
|
|||||||
278
src/services/export/excel/ExcelExportProfitLossService.ts
Normal file
278
src/services/export/excel/ExcelExportProfitLossService.ts
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
// services/excelExportService.ts
|
||||||
|
import type { ProfitLossReport } from '@/types/services/analytic'
|
||||||
|
|
||||||
|
export class ExcelExportProfitLossService {
|
||||||
|
/**
|
||||||
|
* Export Profit Loss Report to Excel
|
||||||
|
*/
|
||||||
|
static async exportProfitLossToExcel(profitData: ProfitLossReport, filename?: string) {
|
||||||
|
try {
|
||||||
|
// Dynamic import untuk xlsx library
|
||||||
|
const XLSX = await import('xlsx')
|
||||||
|
|
||||||
|
// Prepare data untuk Excel
|
||||||
|
const worksheetData: any[][] = []
|
||||||
|
|
||||||
|
// Header dengan company info (baris 1-2) - update data
|
||||||
|
worksheetData.push(['LABA RUGI']) // Row 0 - Main title
|
||||||
|
worksheetData.push([`Periode: ${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`]) // Row 1 - Period
|
||||||
|
worksheetData.push([]) // Empty row
|
||||||
|
|
||||||
|
// Add Summary Section
|
||||||
|
worksheetData.push(['RINGKASAN PERIODE']) // Section header
|
||||||
|
worksheetData.push([]) // Empty row
|
||||||
|
|
||||||
|
const summaryData = [
|
||||||
|
['Total Revenue:', `Rp ${profitData.summary.total_revenue.toLocaleString('id-ID')}`],
|
||||||
|
['Total Cost:', `Rp ${profitData.summary.total_cost.toLocaleString('id-ID')}`],
|
||||||
|
['Gross Profit:', `Rp ${profitData.summary.gross_profit.toLocaleString('id-ID')}`],
|
||||||
|
['Gross Profit Margin:', `${profitData.summary.gross_profit_margin.toFixed(1)}%`],
|
||||||
|
['Total Tax:', `Rp ${profitData.summary.total_tax.toLocaleString('id-ID')}`],
|
||||||
|
['Total Discount:', `Rp ${profitData.summary.total_discount.toLocaleString('id-ID')}`],
|
||||||
|
['Net Profit:', `Rp ${profitData.summary.net_profit.toLocaleString('id-ID')}`],
|
||||||
|
['Net Profit Margin:', `${profitData.summary.net_profit_margin.toFixed(1)}%`],
|
||||||
|
['Total Orders:', profitData.summary.total_orders.toString()],
|
||||||
|
['Average Profit:', `Rp ${profitData.summary.average_profit.toLocaleString('id-ID')}`],
|
||||||
|
['Profitability Ratio:', `${profitData.summary.profitability_ratio.toFixed(1)}%`]
|
||||||
|
]
|
||||||
|
|
||||||
|
summaryData.forEach(row => {
|
||||||
|
worksheetData.push([row[0], row[1]]) // Only 2 columns needed
|
||||||
|
})
|
||||||
|
|
||||||
|
worksheetData.push([]) // Empty row
|
||||||
|
worksheetData.push([]) // Empty row
|
||||||
|
|
||||||
|
// Daily Data Section Header
|
||||||
|
worksheetData.push(['RINCIAN HARIAN']) // Section header
|
||||||
|
worksheetData.push([]) // Empty row
|
||||||
|
|
||||||
|
// Prepare date columns - ambil dari daily data
|
||||||
|
const dateColumns = profitData.data.map(daily => {
|
||||||
|
const date = new Date(daily.date)
|
||||||
|
return date.toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Header row untuk tabel daily data (hanya tanggal)
|
||||||
|
const headerRow = ['NO', 'KET', '', ...dateColumns]
|
||||||
|
worksheetData.push(headerRow)
|
||||||
|
|
||||||
|
// Prepare data rows
|
||||||
|
const rows = this.prepareProfitLossRows(profitData)
|
||||||
|
|
||||||
|
// Add data rows ke worksheet (hanya nilai per tanggal)
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowData = [
|
||||||
|
row.no,
|
||||||
|
row.label,
|
||||||
|
':',
|
||||||
|
...row.values.map(val => val.today) // Store as numbers for better Excel handling
|
||||||
|
]
|
||||||
|
worksheetData.push(rowData)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create workbook dan worksheet
|
||||||
|
const workbook = XLSX.utils.book_new()
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData)
|
||||||
|
|
||||||
|
// Apply basic formatting
|
||||||
|
this.applyBasicFormatting(worksheet, dateColumns.length, worksheetData.length, XLSX)
|
||||||
|
|
||||||
|
// Add worksheet ke workbook
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Laba Rugi')
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
const exportFilename = filename || this.generateFilename('Laba_Rugi')
|
||||||
|
|
||||||
|
// Download file
|
||||||
|
XLSX.writeFile(workbook, exportFilename)
|
||||||
|
|
||||||
|
return { success: true, filename: exportFilename }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting to Excel:', error)
|
||||||
|
return { success: false, error: 'Failed to export Excel file' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare profit loss data rows (tanpa percentage)
|
||||||
|
*/
|
||||||
|
private static prepareProfitLossRows(profitData: ProfitLossReport) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
label: 'TOTAL PENJ',
|
||||||
|
values: profitData.data.map(daily => ({
|
||||||
|
today: daily.revenue,
|
||||||
|
mtd: daily.revenue // TODO: Replace with actual MTD data from API
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
label: 'HPP',
|
||||||
|
values: profitData.data.map(daily => ({
|
||||||
|
today: daily.cost,
|
||||||
|
mtd: daily.cost
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 3,
|
||||||
|
label: 'Laba Kotor (1-2)',
|
||||||
|
values: profitData.data.map(daily => ({
|
||||||
|
today: daily.gross_profit,
|
||||||
|
mtd: daily.gross_profit
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
label: 'Biaya lain',
|
||||||
|
values: profitData.data.map(daily => {
|
||||||
|
const totalCosts = daily.tax + daily.discount
|
||||||
|
return {
|
||||||
|
today: totalCosts,
|
||||||
|
mtd: totalCosts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
label: 'Laba/Rugi (3-4)',
|
||||||
|
values: profitData.data.map(daily => ({
|
||||||
|
today: daily.net_profit,
|
||||||
|
mtd: daily.net_profit
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply basic formatting (SheetJS compatible)
|
||||||
|
*/
|
||||||
|
private static applyBasicFormatting(worksheet: any, dateColumnsCount: number, totalRows: number, XLSX: any) {
|
||||||
|
// Set column widths
|
||||||
|
const colWidths = [
|
||||||
|
{ wch: 5 }, // NO
|
||||||
|
{ wch: 25 }, // KET
|
||||||
|
{ wch: 3 }, // :
|
||||||
|
...Array(dateColumnsCount).fill({ wch: 18 })
|
||||||
|
]
|
||||||
|
worksheet['!cols'] = colWidths
|
||||||
|
|
||||||
|
// Set row heights for better spacing
|
||||||
|
worksheet['!rows'] = [
|
||||||
|
{ hpt: 30 }, // Title row
|
||||||
|
{ hpt: 25 }, // Period row
|
||||||
|
{ hpt: 15 }, // Empty row
|
||||||
|
{ hpt: 25 }, // Summary header
|
||||||
|
{ hpt: 15 } // Empty row
|
||||||
|
]
|
||||||
|
|
||||||
|
// Merge cells untuk headers
|
||||||
|
const merges = [
|
||||||
|
{ s: { r: 0, c: 0 }, e: { r: 0, c: dateColumnsCount + 2 } }, // Title
|
||||||
|
{ s: { r: 1, c: 0 }, e: { r: 1, c: dateColumnsCount + 2 } }, // Period
|
||||||
|
{ s: { r: 3, c: 0 }, e: { r: 3, c: dateColumnsCount + 2 } } // Summary header
|
||||||
|
]
|
||||||
|
|
||||||
|
// Find and add merge for daily data header
|
||||||
|
for (let i = 0; i < totalRows; i++) {
|
||||||
|
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
|
||||||
|
if (cell && cell.v === 'RINCIAN HARIAN') {
|
||||||
|
merges.push({ s: { r: i, c: 0 }, e: { r: i, c: dateColumnsCount + 2 } })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worksheet['!merges'] = merges
|
||||||
|
|
||||||
|
// Apply number formatting untuk currency cells
|
||||||
|
this.applyNumberFormatting(worksheet, totalRows, dateColumnsCount + 3, XLSX)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply number formatting for currency
|
||||||
|
*/
|
||||||
|
private static applyNumberFormatting(worksheet: any, totalRows: number, totalCols: number, XLSX: any) {
|
||||||
|
// Find table data start (after "NO" header)
|
||||||
|
let dataStartRow = -1
|
||||||
|
for (let i = 0; i < totalRows; i++) {
|
||||||
|
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
|
||||||
|
if (cell && cell.v === 'NO') {
|
||||||
|
dataStartRow = i + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataStartRow === -1) return
|
||||||
|
|
||||||
|
// Apply currency formatting to data cells (starting from column 3 - after NO, KET, :)
|
||||||
|
for (let row = dataStartRow; row < dataStartRow + 5; row++) {
|
||||||
|
// 5 data rows
|
||||||
|
for (let col = 3; col < totalCols; col++) {
|
||||||
|
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
||||||
|
const cell = worksheet[cellAddress]
|
||||||
|
|
||||||
|
if (cell && typeof cell.v === 'number') {
|
||||||
|
// Apply Indonesian currency format
|
||||||
|
cell.z = '#,##0'
|
||||||
|
cell.t = 'n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate filename with timestamp
|
||||||
|
*/
|
||||||
|
private static generateFilename(prefix: string): string {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = now.getDate().toString().padStart(2, '0')
|
||||||
|
const hour = now.getHours().toString().padStart(2, '0')
|
||||||
|
const minute = now.getMinutes().toString().padStart(2, '0')
|
||||||
|
|
||||||
|
return `${prefix}_${year}_${month}_${day}_${hour}${minute}.xlsx`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export any data to Excel with custom configuration
|
||||||
|
*/
|
||||||
|
static async exportToExcel(
|
||||||
|
data: any[][],
|
||||||
|
sheetName: string = 'Sheet1',
|
||||||
|
filename?: string,
|
||||||
|
options?: {
|
||||||
|
colWidths?: { wch: number }[]
|
||||||
|
merges?: { s: { r: number; c: number }; e: { r: number; c: number } }[]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const XLSX = await import('xlsx')
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new()
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet(data)
|
||||||
|
|
||||||
|
// Apply options
|
||||||
|
if (options?.colWidths) {
|
||||||
|
worksheet['!cols'] = options.colWidths
|
||||||
|
}
|
||||||
|
if (options?.merges) {
|
||||||
|
worksheet['!merges'] = options.merges
|
||||||
|
}
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
||||||
|
|
||||||
|
const exportFilename = filename || this.generateFilename('Export')
|
||||||
|
XLSX.writeFile(workbook, exportFilename)
|
||||||
|
|
||||||
|
return { success: true, filename: exportFilename }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting to Excel:', error)
|
||||||
|
return { success: false, error: 'Failed to export Excel file' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import DateRangePicker from '@/components/RangeDatePicker'
|
import DateRangePicker from '@/components/RangeDatePicker'
|
||||||
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||||
|
import { ExcelExportProfitLossService } from '@/services/export/excel/ExcelExportProfitLossService'
|
||||||
import { ProfitLossReport } from '@/types/services/analytic'
|
import { ProfitLossReport } from '@/types/services/analytic'
|
||||||
import { Button, Card, CardContent, Box } from '@mui/material'
|
import { Button, Card, CardContent, Box } from '@mui/material'
|
||||||
|
|
||||||
@ -30,9 +31,24 @@ const ReportProfitLossContent = ({
|
|||||||
onStartDateChange,
|
onStartDateChange,
|
||||||
onEndDateChange
|
onEndDateChange
|
||||||
}: ReportProfitLossContentProps) => {
|
}: ReportProfitLossContentProps) => {
|
||||||
const handleExport = () => {
|
const handleExport = async () => {
|
||||||
// TODO: Implement export functionality
|
if (!profitData) return
|
||||||
console.log('Export data:', profitData)
|
|
||||||
|
try {
|
||||||
|
const result = await ExcelExportProfitLossService.exportProfitLossToExcel(profitData)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Optional: Show success notification
|
||||||
|
console.log('Export successful:', result.filename)
|
||||||
|
// You can add toast notification here
|
||||||
|
} else {
|
||||||
|
console.error('Export failed:', result.error)
|
||||||
|
alert('Export gagal. Silakan coba lagi.')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export error:', error)
|
||||||
|
alert('Terjadi kesalahan saat export.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user