refactor(FE): Refactor expense report page to use tab-based layout

This commit is contained in:
rstubryan
2026-02-13 10:19:09 +07:00
parent ceb594a4cc
commit 67f2a80f23
9 changed files with 1031 additions and 1272 deletions
@@ -0,0 +1,219 @@
import { ReportExpense } from '@/types/api/report/report-expense';
import { formatCurrency, formatDate } from '@/lib/helper';
import jsPDF from 'jspdf';
import autoTable, { UserOptions } from 'jspdf-autotable';
interface jsPDFWithAutoTable extends jsPDF {
lastAutoTable: {
finalY: number;
};
}
export interface PDFParams {
location_name?: string;
supplier_name?: string;
realization_date?: string;
}
const getStatusColor = (action?: string): [number, number, number] => {
switch (action) {
case 'APPROVED':
case 'Selesai': // Berdasarkan data sumber
return [220, 252, 231]; // Hijau muda (#dcfce7)
case 'REJECTED':
return [254, 226, 226]; // Merah muda (#fee2e2)
case 'Realisasi': // Berdasarkan data sumber
return [254, 243, 199]; // Kuning/Amber muda (#fef3c7)
default:
return [255, 255, 255]; // Putih
}
};
export const generateReportExpensePDF = async (
data: ReportExpense[],
params: PDFParams
): Promise<void> => {
// Inisialisasi dokumen dengan tipe yang sudah diekstensi
const doc = new jsPDF('l', 'mm', 'a4') as jsPDFWithAutoTable;
const pageWidth: number = doc.internal.pageSize.getWidth();
const marginX: number = 14;
// --- HEADER SECTION ---
doc.setFont('helvetica', 'bold');
doc.setFontSize(18);
doc.setTextColor(31, 116, 191); // #1f74bf sesuai style
doc.text('PT LUMBUNG TELUR INDONESIA', marginX, 20);
doc.setFont('helvetica', 'normal');
doc.setFontSize(7);
doc.setTextColor(102, 102, 102);
doc.text(
'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162',
marginX,
25
);
doc.setDrawColor(0);
doc.line(marginX, 28, pageWidth - marginX, 28);
// --- TITLE & INFO SECTION ---
doc.setFontSize(18);
doc.setTextColor(31, 116, 191);
doc.text('LAPORAN BIAYA OPERASIONAL', marginX, 38);
doc.setFontSize(7);
doc.setTextColor(0);
const infoX: number = pageWidth - marginX;
doc.text(
`Tanggal Cetak: ${formatDate(new Date(), 'DD MMM YYYY')}`,
infoX,
35,
{ align: 'right' }
);
doc.text(`Total Data: ${data.length} transaksi`, infoX, 40, {
align: 'right',
});
// --- GROUPING LOGIC ---
const groupedBySupplier = data.reduce(
(acc, item) => {
const supplierName: string = item.supplier?.name || 'Unknown Supplier';
if (!acc[supplierName]) acc[supplierName] = [];
acc[supplierName].push(item);
return acc;
},
{} as Record<string, ReportExpense[]>
);
let currentY: number = 50;
// --- RENDER TABLES PER SUPPLIER ---
Object.entries(groupedBySupplier).forEach(([supplierName, items]) => {
// Cek sisa ruang halaman sebelum cetak judul supplier
if (currentY > 180) {
doc.addPage();
currentY = 20;
}
doc.setFontSize(14);
doc.setTextColor(31, 116, 191);
doc.text(supplierName, marginX, currentY);
currentY += 5;
const tableOptions: UserOptions = {
startY: currentY,
head: [
[
{ content: 'No', rowSpan: 2 },
{ content: 'No. PO', rowSpan: 2 },
{ content: 'No. Referensi', rowSpan: 2 },
{ content: 'Tgl Realisasi', rowSpan: 2 },
{ content: 'Tgl Transaksi', rowSpan: 2 },
{ content: 'Kategori', rowSpan: 2 },
{ content: 'Produk', rowSpan: 2 },
{ content: 'Lokasi', rowSpan: 2 },
{ content: 'Kandang', rowSpan: 2 },
{ content: 'Pengajuan', colSpan: 3, styles: { halign: 'center' } },
{ content: 'Realisasi', colSpan: 3, styles: { halign: 'center' } },
{ content: 'Status BOP', rowSpan: 2 },
],
['Qty', 'Harga', 'Total', 'Qty', 'Harga', 'Total'],
],
body: items.map((item, index) => {
const pQty: number = item.pengajuan?.qty || 0;
const pPrice: number = item.pengajuan?.price || 0;
const rQty: number = item.realisasi?.qty || 0;
const rPrice: number = item.realisasi?.price || 0;
return [
index + 1,
item.po_number || '-',
item.reference_number || '-',
formatDate(item.realization_date, 'DD MMM YY'),
formatDate(item.transaction_date, 'DD MMM YY'),
item.category?.replace('-', ' ') || '-',
item.pengajuan?.nonstock?.name || '-',
item.kandang?.location?.name || '-',
item.kandang?.name || '-',
pQty.toLocaleString('id-ID'),
formatCurrency(pPrice),
formatCurrency(pQty * pPrice),
rQty.toLocaleString('id-ID'),
formatCurrency(rPrice),
formatCurrency(rQty * rPrice),
item.latest_approval?.step_name || '-',
];
}),
theme: 'grid',
styles: { fontSize: 6, cellPadding: 1.5, overflow: 'linebreak' },
headStyles: {
fillColor: [245, 245, 245],
textColor: 0,
fontStyle: 'bold',
lineWidth: 0.1,
},
// HOOK UNTUK BADGE:
didParseCell: (dataCell) => {
// Index kolom 15 adalah Status BOP (berdasarkan array di atas)
if (dataCell.section === 'body' && dataCell.column.index === 15) {
const statusText = dataCell.cell.raw as string;
// Berikan warna latar belakang sel sesuai status
dataCell.cell.styles.fillColor = getStatusColor(statusText);
dataCell.cell.styles.textColor = [0, 0, 0]; // Teks hitam agar terbaca
dataCell.cell.styles.fontStyle = 'bold';
dataCell.cell.styles.halign = 'center';
}
},
margin: { left: marginX, right: marginX },
};
autoTable(doc, tableOptions);
currentY = doc.lastAutoTable.finalY + 10;
});
// --- GRAND TOTAL SECTION ---
const grandTotals = data.reduce(
(acc, item) => {
const pTotal = (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const rTotal = (item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pTotal,
realisasi: acc.realisasi + rTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
if (currentY > 250) {
doc.addPage();
currentY = 20;
}
autoTable(doc, {
startY: currentY,
body: [
['GRAND TOTAL PENGAJUAN', formatCurrency(grandTotals.pengajuan)],
['GRAND TOTAL REALISASI', formatCurrency(grandTotals.realisasi)],
],
styles: { fontSize: 8, fontStyle: 'bold' },
columnStyles: {
0: { cellWidth: 50, fillColor: [245, 245, 245] },
1: { cellWidth: 50 },
},
theme: 'grid',
margin: { left: marginX },
});
// --- FOOTER ---
const finalY: number = doc.lastAutoTable.finalY + 20;
doc.setFontSize(14);
doc.setTextColor(31, 116, 191);
doc.text('PT LUMBUNG TELUR INDONESIA', pageWidth - marginX, finalY, {
align: 'right',
});
// Download File
const fileName: string = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
doc.save(fileName);
};
@@ -0,0 +1,109 @@
import * as XLSX from 'xlsx';
import { ReportExpense } from '@/types/api/report/report-expense';
import { formatCurrency, formatDate } from '@/lib/helper';
export const generateReportExpenseExcel = async (
data: ReportExpense[]
): Promise<void> => {
// Group by supplier
const groupedBySupplier: Record<string, ReportExpense[]> = {};
data.forEach((item) => {
const supplierName = item.supplier?.name || 'Unknown Supplier';
if (!groupedBySupplier[supplierName]) {
groupedBySupplier[supplierName] = [];
}
groupedBySupplier[supplierName].push(item);
});
const workbook = XLSX.utils.book_new();
Object.entries(groupedBySupplier).forEach(([supplierName, supplierData]) => {
const totals = supplierData.reduce(
(acc, item) => ({
qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0),
total_pengajuan:
acc.total_pengajuan +
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0),
qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0),
total_realisasi:
acc.total_realisasi +
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0),
}),
{
qty_pengajuan: 0,
total_pengajuan: 0,
qty_realisasi: 0,
total_realisasi: 0,
}
);
const excelData = supplierData.map((item, index) => ({
No: index + 1,
'No. PO': item.po_number || '',
'No. Referensi': item.reference_number || '',
'Tanggal Realisasi': item.realization_date
? formatDate(item.realization_date, 'DD MMM YYYY')
: '',
'Tanggal Transaksi': item.transaction_date
? formatDate(item.transaction_date, 'DD MMM YYYY')
: '',
Kategori: item.category || '',
Produk: item.pengajuan?.nonstock?.name || '',
Lokasi: item.kandang?.location?.name || '',
Kandang: item.kandang?.name || '',
'Qty Pengajuan': item.pengajuan?.qty || 0,
'Harga Pengajuan': item.pengajuan?.price || 0,
'Total Pengajuan':
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0),
'Qty Realisasi': item.realisasi?.qty || 0,
'Harga Realisasi': item.realisasi?.price || 0,
'Total Realisasi':
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0),
'Status Pencairan': item.latest_approval?.step_name || '',
}));
excelData.push({
No: 'Total' as unknown as number,
'No. PO': '',
'No. Referensi': '',
'Tanggal Realisasi': '',
'Tanggal Transaksi': '',
Kategori: '',
Produk: '',
Lokasi: '',
Kandang: '',
'Qty Pengajuan': totals.qty_pengajuan,
'Harga Pengajuan': 0,
'Total Pengajuan': totals.total_pengajuan,
'Qty Realisasi': totals.qty_realisasi,
'Harga Realisasi': 0,
'Total Realisasi': totals.total_realisasi,
'Status Pencairan': '',
});
const worksheet = XLSX.utils.json_to_sheet(excelData);
const colWidths = [
{ wch: 5 },
{ wch: 20 },
{ wch: 20 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
{ wch: 30 },
{ wch: 20 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
{ wch: 20 },
{ wch: 15 },
{ wch: 20 },
];
worksheet['!cols'] = colWidths;
const sheetName = supplierName.slice(0, 31);
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
XLSX.writeFile(workbook, filename);
};