mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 23:05:46 +00:00
refactor(FE): Refactor expense report page to use tab-based layout
This commit is contained in:
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user