fix(FE): fix export report expense & fix report closing table view & fix project flock form set disable inofrmasi umum on edit

This commit is contained in:
randy-ar
2025-12-19 17:20:02 +07:00
parent 0b75d68494
commit a7267370a0
10 changed files with 448 additions and 69985 deletions
@@ -74,17 +74,11 @@ const ClosingFinanceTable = ({
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
? [
// Penjualan group
{
label: 'Penjualan',
group_name: 'Penjualan',
group_index: 0,
isGroupHeader: true as const,
},
...finance.data.profit_loss.data.penjualan.map((item) => ({
label: 'Penjualan',
group_name: 'Penjualan',
group_index: 0,
// Pembelian group
...finance.data.profit_loss.data.pembelian.map((item) => ({
label: 'Pembelian',
group_name: 'Pembelian',
group_index: 1,
type: item.type,
rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg,
@@ -103,17 +97,11 @@ const ClosingFinanceTable = ({
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
},
// Pembelian group
{
label: 'Pembelian',
group_name: 'Pembelian',
group_index: 1,
isGroupHeader: true as const,
},
...finance.data.profit_loss.data.pembelian.map((item) => ({
label: 'Pembelian',
group_name: 'Pembelian',
group_index: 1,
// Penjualan group
...finance.data.profit_loss.data.penjualan.map((item) => ({
label: 'Penjualan',
group_name: 'Penjualan',
group_index: 0,
type: item.type,
rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg,
@@ -369,16 +357,16 @@ const ClosingFinanceTable = ({
data={profitLossTableData}
columns={[
{
header: 'Type',
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.type,
cell: (item) => (
<div className='ps-6'>
<div className=''>
{formatTitleCase(item.row.original.type || '-')}
</div>
),
footer: (item) => (
<div className='font-bold'>
<div className='font-bold uppercase'>
{isResponseSuccess(finance)
? formatTitleCase(
finance.data.profit_loss.data.summary.net_profit
@@ -446,7 +434,7 @@ const ClosingFinanceTable = ({
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
@@ -68,7 +68,7 @@ const ProjectFlockDetail = ({
latestApproval: projectFlock?.approval,
approvalLines: PROJECT_FLOCK_APPROVAL_LINE,
moduleName: 'PROJECT_FLOCKS',
moduleId: projectFlock?.id.toString() ?? '',
moduleId: projectFlock?.id?.toString() ?? '',
});
const { approvals: kandangApprovals, isLoading: kandangApprovalsLoading } =
@@ -47,9 +47,7 @@ import Card from '@/components/Card';
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { useUiStore } from '@/stores/ui/ui.store';
import Link from 'next/link';
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { formatDate } from '@/lib/helper';
interface ProjectFlockFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -260,7 +258,9 @@ const ProjectFlockForm = ({
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('category', (val as OptionType)?.value);
formik.setFieldValue('category_option', val);
formik.setFieldTouched('category', true);
if (val == null) {
formik.setFieldTouched('category', true);
}
};
// Submit Handler
@@ -788,7 +788,7 @@ const ProjectFlockForm = ({
}
errorMessage={formik.errors.area_id as string}
isClearable
isDisabled={formType === 'detail'}
isDisabled={formType != 'add'}
/>
<SelectInput
required
@@ -807,7 +807,7 @@ const ProjectFlockForm = ({
}
errorMessage={formik.errors.location_id as string}
isClearable
isDisabled={formType === 'detail' || disabledLocation}
isDisabled={formType != 'add' || disabledLocation}
/>
<SelectInput
required
@@ -837,7 +837,7 @@ const ProjectFlockForm = ({
}
errorMessage={formik.errors.flock_name as string}
isClearable
isDisabled={formType === 'detail'}
isDisabled={formType != 'add'}
/>
<SelectInput
required
@@ -851,7 +851,7 @@ const ProjectFlockForm = ({
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
errorMessage={formik.errors.fcr_id as string}
isClearable
isDisabled={formType === 'detail'}
isDisabled={formType != 'add'}
/>
<SelectInput
required
@@ -864,7 +864,7 @@ const ProjectFlockForm = ({
}
errorMessage={formik.errors.category as string}
isClearable
isDisabled={formType === 'detail'}
isDisabled={formType != 'add'}
/>
<NumberInput
name='period'
@@ -1,602 +1,218 @@
import { ReportExpense } from '@/types/api/report/report-expense';
import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { formatCurrency, formatDate } from '@/lib/helper';
import pdfStyles from '@/components/pages/report/expense/pdf/styles/ReportExpenseStyles';
import toast from 'react-hot-toast';
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;
kandang_name?: string;
nonstock_name?: string;
category?: string;
realization_date?: string;
search?: string;
}
const getStatusStyle = (action?: string) => {
const getStatusColor = (action?: string): [number, number, number] => {
switch (action) {
case 'APPROVED':
return { backgroundColor: '#dcfce7' };
case 'Selesai': // Berdasarkan data sumber
return [220, 252, 231]; // Hijau muda (#dcfce7)
case 'REJECTED':
return { backgroundColor: '#fee2e2' };
return [254, 226, 226]; // Merah muda (#fee2e2)
case 'Realisasi': // Berdasarkan data sumber
return [254, 243, 199]; // Kuning/Amber muda (#fef3c7)
default:
return { backgroundColor: '#fef3c7' };
return [255, 255, 255]; // Putih
}
};
const PDFDocument = ({
data,
params,
}: {
data: ReportExpense[];
params: PDFParams;
}) => {
// Group data by supplier
const groupedBySupplier = (() => {
const groups: Record<string, ReportExpense[]> = {};
data.forEach((item) => {
const supplierName = item.supplier?.name || 'Unknown Supplier';
if (!groups[supplierName]) {
groups[supplierName] = [];
}
groups[supplierName].push(item);
});
return groups;
})();
// Calculate grand totals
const grandTotals = data.reduce(
(acc, item) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pengajuanTotal,
realisasi: acc.realisasi + realisasiTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
return (
<Document>
<Page size='A4' orientation='landscape' style={pdfStyles.page}>
{/* Header Section */}
<View style={pdfStyles.header}>
<Image
src={'https://placehold.co/120x30/png'}
style={pdfStyles.logo}
id={'mbu-logo'}
/>
<Text style={pdfStyles.companyInfo}>PT LUMBUNG TELUR INDONESIA</Text>
<Text style={pdfStyles.address}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={pdfStyles.divider} />
</View>
{/* Report Title */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.title}>LAPORAN BIAYA OPERASIONAL</Text>
<View style={pdfStyles.poInfo}>
<Text>Tanggal Cetak: {formatDate(new Date(), 'DD MMM YYYY')}</Text>
<Text>Total Data: {data.length} transaksi</Text>
</View>
</View>
{/* Filters Info if any */}
{(params.location_name ||
params.supplier_name ||
params.realization_date) && (
<View style={{ marginBottom: 10, fontSize: 8 }}>
{params.location_name && (
<Text>Lokasi: {params.location_name}</Text>
)}
{params.supplier_name && (
<Text>Supplier: {params.supplier_name}</Text>
)}
{params.realization_date && (
<Text>
Tanggal Realisasi:{' '}
{formatDate(params.realization_date, 'DD MMM YYYY')}
</Text>
)}
</View>
)}
{/* Grouped Tables by Supplier */}
{Object.entries(groupedBySupplier).map(
([supplierName, items], groupIndex) => {
const supplierTotals = items.reduce(
(acc, item) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return {
pengajuan: acc.pengajuan + pengajuanTotal,
realisasi: acc.realisasi + realisasiTotal,
};
},
{ pengajuan: 0, realisasi: 0 }
);
return (
<View key={groupIndex} style={pdfStyles.allocationSection}>
{/* Supplier Header */}
<Text style={pdfStyles.sectionTitle}>{supplierName}</Text>
{/* Table */}
<View style={pdfStyles.table}>
{/* Header Row 1: Group Headers */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View
style={[
pdfStyles.tableCellNarrowHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No</Text>
</View>
<View
style={[
pdfStyles.tableCellWrapHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No. PO</Text>
</View>
<View
style={[
pdfStyles.tableCellWrapHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>No. Referensi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Tgl Realisasi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Tgl Transaksi</Text>
</View>
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Kategori</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Produk</Text>
</View>
<View
style={[
pdfStyles.tableCellSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Lokasi</Text>
</View>
<View
style={[
pdfStyles.tableCellSmallHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Kandang</Text>
</View>
{/* Pengajuan Group */}
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{
borderBottomWidth: 0,
borderRightWidth: 0,
textAlign: 'center',
},
]}
>
<Text>Pengajuan</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text></Text>
</View>
{/* Realisasi Group */}
<View
style={[
pdfStyles.tableCellXSmallHeader,
{ borderBottomWidth: 0, borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{
borderBottomWidth: 0,
borderRightWidth: 0,
textAlign: 'center',
},
]}
>
<Text>Realisasi</Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMediumHeader,
{ borderBottomWidth: 0 },
]}
>
<Text>Status BOP</Text>
</View>
</View>
{/* Header Row 2: Sub Headers */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={pdfStyles.tableCellNarrowHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellWrapHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellWrapHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellXSmallHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellSmallHeader}>
<Text></Text>
</View>
<View style={pdfStyles.tableCellSmallHeader}>
<Text></Text>
</View>
{/* Pengajuan sub-headers */}
<View style={pdfStyles.tableCellXSmallHeader}>
<Text>Qty</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Harga</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Total</Text>
</View>
{/* Realisasi sub-headers */}
<View style={pdfStyles.tableCellXSmallHeader}>
<Text>Qty</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Harga</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text>Total</Text>
</View>
<View style={pdfStyles.tableCellMediumHeader}>
<Text></Text>
</View>
</View>
{/* Table Body */}
{items.map((item, index) => {
const pengajuanTotal =
(item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0);
const realisasiTotal =
(item.realisasi?.qty || 0) * (item.realisasi?.price || 0);
return (
<View key={index} style={pdfStyles.tableRow}>
<View style={pdfStyles.tableCellNarrow}>
<Text>{index + 1}</Text>
</View>
<View style={pdfStyles.tableCellWrap}>
<Text>{item.po_number || '-'}</Text>
</View>
<View style={pdfStyles.tableCellWrap}>
<Text>{item.reference_number || '-'}</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text>
{formatDate(item.realization_date, 'DD MMM YY')}
</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text>
{formatDate(item.transaction_date, 'DD MMM YY')}
</Text>
</View>
<View style={pdfStyles.tableCellXSmall}>
<Text>
{item.category?.split('-').join(' ') || '-'}
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text>{item.pengajuan?.nonstock?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellSmall}>
<Text>{item.kandang?.location?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellSmall}>
<Text>{item.kandang?.name || '-'}</Text>
</View>
<View style={pdfStyles.tableCellRightXSmall}>
<Text>
{(item.pengajuan?.qty || 0).toLocaleString('id-ID')}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>
{formatCurrency(item.pengajuan?.price || 0)}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>{formatCurrency(pengajuanTotal)}</Text>
</View>
<View style={pdfStyles.tableCellRightXSmall}>
<Text>
{(item.realisasi?.qty || 0).toLocaleString('id-ID')}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>
{formatCurrency(item.realisasi?.price || 0)}
</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text>{formatCurrency(realisasiTotal)}</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text
style={[
{ fontSize: 7, padding: 2, borderRadius: 2 },
getStatusStyle(item.latest_approval?.action),
]}
>
{item.latest_approval?.step_name || '-'}
</Text>
</View>
</View>
);
})}
{/* Supplier Subtotal Row */}
<View style={pdfStyles.grandTotalRow}>
<View
style={[
pdfStyles.tableCellNarrow,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[pdfStyles.tableCellWrap, { borderRightWidth: 0 }]}
>
<Text></Text>
</View>
<View
style={[pdfStyles.tableCellWrap, { borderRightWidth: 0 }]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMedium,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellMedium,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
{/* Pengajuan Subtotal */}
<View
style={[
pdfStyles.tableCellRightXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(supplierTotals.pengajuan)}
</Text>
</View>
{/* Realisasi Subtotal */}
<View
style={[
pdfStyles.tableCellRightXSmall,
{ borderRightWidth: 0 },
]}
>
<Text></Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>Subtotal</Text>
</View>
<View style={pdfStyles.tableCellRightMedium}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(supplierTotals.realisasi)}
</Text>
</View>
<View style={pdfStyles.tableCellMedium}>
<Text></Text>
</View>
</View>
</View>
</View>
);
}
)}
{/* Grand Total Section */}
<View style={pdfStyles.allocationSection}>
<View
style={{
width: '30%',
borderTopWidth: 1,
borderTopColor: '#000000',
borderTopStyle: 'solid',
borderLeftWidth: 1,
borderLeftColor: '#000000',
borderLeftStyle: 'solid',
}}
>
<View style={pdfStyles.innerRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
GRAND TOTAL PENGAJUAN
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(grandTotals.pengajuan)}
</Text>
</View>
</View>
<View style={pdfStyles.innerRow}>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
GRAND TOTAL REALISASI
</Text>
</View>
<View style={pdfStyles.tableCell}>
<Text style={{ fontWeight: 'bold' }}>
{formatCurrency(grandTotals.realisasi)}
</Text>
</View>
</View>
</View>
</View>
{/* Footer */}
<View style={pdfStyles.footer}>
<View style={pdfStyles.footerCompany}>
<Text>PT LUMBUNG TELUR INDONESIA</Text>
</View>
</View>
</Page>
</Document>
);
};
export const generateReportExpensePDF = async (
data: ReportExpense[],
params: PDFParams
): Promise<void> => {
try {
const doc = <PDFDocument data={data} params={params} />;
const blob = await pdf(doc).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
throw error;
// 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);
};
File diff suppressed because it is too large Load Diff
-29
View File
@@ -1,29 +0,0 @@
/**
* Dummy data for ReportExpense[]
* Generated from: report-expense.json
*
* This file is auto-generated. Do not edit manually.
*/
import { BaseApiResponse } from '@/types/api/api-general';
import dummyData from './reports-expense.dummy.json';
import { ReportExpense } from '@/types/api/report/report-expense';
/**
* Get dummy ReportExpense[] data
* @returns Promise with BaseApiResponse containing ReportExpense[]
*/
export async function getDummyExpense(): Promise<
BaseApiResponse<ReportExpense[]>
> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 200,
status: 'success',
message: 'Data retrieved successfully',
data: dummyData as unknown as ReportExpense[],
});
}, 500);
});
}
-5
View File
@@ -16,11 +16,6 @@ export class ReportExpenseApiService extends BaseApiService<
async getAllFetcher(
endpoint: string
): Promise<BaseApiResponse<ReportExpense[]>> {
// TODO: Remove this block when backend is ready
// const { getDummyExpense } = await import('@/dummy/reports-expense.dummy');
// return await getDummyExpense();
// Uncomment this when backend is ready
return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint);
}
}