diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx new file mode 100644 index 00000000..27369db5 --- /dev/null +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { View, StyleSheet } from '@react-pdf/renderer'; +import { PdfThead, PdfColumn } from './PdfThead'; +import { PdfTbody, PdfTbodyCell } from './PdfTbody'; +import { PdfTfoot, PdfTfootCell } from './PdfTfoot'; + +const styles = StyleSheet.create({ + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, +}); + +interface PdfTableProps { + columns: PdfColumn[]; + data: PdfTbodyCell[][]; + footer?: PdfTfootCell[]; + footerLabel?: string; + firstRow?: { + valueKey: string; + value: number; + align?: 'right'; + color?: string; + }; +} + +export const PdfTable = ({ + columns, + data, + footer, + footerLabel = 'Total', + firstRow, +}: PdfTableProps) => { + return ( + + + + {footer && footer.length > 0 && ( + + )} + + ); +}; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx new file mode 100644 index 00000000..fee79726 --- /dev/null +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -0,0 +1,219 @@ +'use client'; + +import { Text, View, StyleSheet } from '@react-pdf/renderer'; + +export interface PdfColumn { + key: string; + header: string; + flex: number; + align?: 'left' | 'center' | 'right'; +} + +export interface PdfTbodyCell { + key: string; + value: string | number | React.ReactNode; + align?: 'left' | 'center' | 'right'; + color?: string; + formatAs?: 'text' | 'date' | 'currency' | 'number'; + formatDate?: string; +} + +const styles = StyleSheet.create({ + tableRow: { + flexDirection: 'row', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'left', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 7, + borderRightWidth: 0, + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableCellNo: { + flex: 0.5, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, +}); + +interface PdfTbodyProps { + columns: PdfColumn[]; + rows: PdfTbodyCell[][]; + firstRow?: { + valueKey: string; + value: number; + align?: 'right'; + color?: string; + }; + formatDate?: (date: string, format: string) => string; + formatNumber?: (num: number) => string; + formatCurrency?: (num: number) => string; +} + +export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { + return ( + <> + {/* First Row */} + {firstRow && ( + + {columns.map((column, index) => { + const isLastColumn = index === columns.length - 1; + const isfirstRowColumn = column.key === firstRow.valueKey; + const align = column.align || 'center'; + + const cellStyle = + column.key === 'no' + ? [styles.tableCellNo, { flex: column.flex }] + : isfirstRowColumn + ? [ + styles.tableCellRight, + { + flex: column.flex, + color: firstRow.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : align === 'right' + ? [ + styles.tableCellRight, + { + flex: column.flex, + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : align === 'center' + ? [ + styles.tableCellCenter, + { + flex: column.flex, + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : isLastColumn + ? [ + styles.tableCellLast, + { + flex: column.flex, + borderRightWidth: 0, + }, + ] + : [styles.tableCell, { flex: column.flex }]; + + return ( + + {isfirstRowColumn ? firstRow.value : ''} + + ); + })} + + )} + + {/* Data Rows */} + {rows.map((row, rowIndex) => { + const isLastRow = rowIndex === rows.length - 1; + + return ( + + {columns.map((column, colIndex) => { + const cell = row.find((c) => c.key === column.key); + const isLastColumn = colIndex === columns.length - 1; + const align = cell?.align || column.align || 'center'; + + const cellStyle = + column.key === 'no' + ? [styles.tableCellNo, { flex: column.flex }] + : align === 'right' + ? [ + styles.tableCellRight, + { + flex: column.flex, + color: cell?.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : align === 'center' + ? [ + styles.tableCellCenter, + { + flex: column.flex, + color: cell?.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : isLastColumn + ? [ + styles.tableCellLast, + { flex: column.flex, borderRightWidth: 0 }, + ] + : [ + styles.tableCell, + { + flex: column.flex, + color: cell?.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ]; + + return ( + + {cell?.value !== undefined && + cell?.value !== null && + cell?.value !== '' ? ( + typeof cell.value === 'object' ? ( + cell.value + ) : ( + {String(cell.value)} + ) + ) : ( + - + )} + + ); + })} + + ); + })} + + ); +}; diff --git a/src/components/helper/pdf/table/PdfTfoot.tsx b/src/components/helper/pdf/table/PdfTfoot.tsx new file mode 100644 index 00000000..a9f209b1 --- /dev/null +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { Text, View, StyleSheet } from '@react-pdf/renderer'; + +export interface PdfColumn { + key: string; + header: string; + flex: number; + align?: 'left' | 'center' | 'right'; +} + +export interface PdfTfootCell { + key: string; + value: string | number; + align?: 'left' | 'center' | 'right'; + flex?: number; + color?: string; +} + +const styles = StyleSheet.create({ + tableRow: { + flexDirection: 'row', + }, + summaryRow: { + backgroundColor: '#F0F0F0', + fontWeight: 'bold', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'left', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 7, + borderRightWidth: 0, + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, + tableCellNo: { + flex: 0.5, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + textAlign: 'center', + }, +}); + +interface PdfTfootProps { + columns: PdfColumn[]; + cells: PdfTfootCell[]; + label?: string; +} + +export const PdfTfoot = ({ + columns, + cells, + label = 'Total', +}: PdfTfootProps) => { + return ( + + {columns.map((column, index) => { + const isLastColumn = index === columns.length - 1; + const cellData = cells.find((c) => c.key === column.key); + + const cellStyle = + column.key === 'no' + ? [ + styles.tableCellNo, + { flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 }, + ] + : cellData?.align === 'right' + ? [ + styles.tableCellRight, + { + flex: column.flex, + color: cellData?.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : cellData?.align === 'center' + ? [ + styles.tableCellCenter, + { + flex: column.flex, + color: cellData?.color || 'black', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : isLastColumn + ? [styles.tableCellLast, { flex: column.flex }] + : [ + styles.tableCell, + { + flex: column.flex, + color: cellData?.color || 'black', + }, + ]; + + return ( + + {column.key === 'no' ? label : cellData?.value || ''} + + ); + })} + + ); +}; diff --git a/src/components/helper/pdf/table/PdfThead.tsx b/src/components/helper/pdf/table/PdfThead.tsx new file mode 100644 index 00000000..89037216 --- /dev/null +++ b/src/components/helper/pdf/table/PdfThead.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { Text, View, StyleSheet } from '@react-pdf/renderer'; + +export interface PdfColumn { + key: string; + header: string; + flex: number; + align?: 'left' | 'center' | 'right'; +} + +const styles = StyleSheet.create({ + tableRow: { + flexDirection: 'row', + }, + tableHeader: { + backgroundColor: '#F5F5F5', + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 7, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + }, +}); + +interface PdfTheadProps { + columns: PdfColumn[]; +} + +export const PdfThead = ({ columns }: PdfTheadProps) => { + return ( + + {columns.map((column, index) => { + const align = column.align || 'center'; + const isLastColumn = index === columns.length - 1; + + const cellStyle = + align === 'right' + ? [ + styles.tableCellHeaderRight, + { + flex: column.flex, + textAlign: 'right' as const, + borderRightWidth: isLastColumn ? 0 : 1, + }, + ] + : [ + styles.tableCellHeader, + { + flex: column.flex, + textAlign: align as 'left' | 'center' | 'right', + borderRightWidth: isLastColumn ? 0 : 1, + }, + ]; + + return ( + + {column.header} + + ); + })} + + ); +}; diff --git a/src/components/helper/pdf/table/index.ts b/src/components/helper/pdf/table/index.ts new file mode 100644 index 00000000..35839f17 --- /dev/null +++ b/src/components/helper/pdf/table/index.ts @@ -0,0 +1,7 @@ +export { PdfTable } from './PdfTable'; +export { PdfThead } from './PdfThead'; +export { PdfTbody } from './PdfTbody'; +export { PdfTfoot } from './PdfTfoot'; +export type { PdfColumn } from './PdfThead'; +export type { PdfTbodyCell } from './PdfTbody'; +export type { PdfTfootCell } from './PdfTfoot'; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index d4f6587a..e6c5d66e 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -12,6 +12,12 @@ import { import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, + PdfTfootCell, +} from '@/components/helper/pdf/table'; Font.register({ family: 'Helvetica', @@ -45,97 +51,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 5, color: '#333333', }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'left', - }, - tableCellNo: { - flex: 0.5, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'center', - }, - tableCellLast: { - flex: 1, - padding: 4, - fontSize: 7, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - summaryRow: { - backgroundColor: '#F0F0F0', - fontWeight: 'bold', - }, badge: { backgroundColor: '#1f74bf', color: '#FFFFFF', @@ -217,6 +132,165 @@ const getParameterText = ( return paramsText; }; +// Helper functions for PdfTable +const getTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' }, + { + key: 'delivery_date', + header: 'Tanggal Realisasi', + flex: 1.2, + align: 'center', + }, + { key: 'aging', header: 'Aging', flex: 0.8, align: 'center' }, + { key: 'reference', header: 'Referensi', flex: 1.5, align: 'left' }, + { key: 'vehicle_numbers', header: 'No Polisi', flex: 1.2, align: 'left' }, + { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, + { key: 'weight', header: 'Berat', flex: 1, align: 'right' }, + { key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' }, + { key: 'unit_price', header: 'Harga/Unit', flex: 1.2, align: 'right' }, + { key: 'final_price', header: 'Harga Akhir', flex: 1.2, align: 'right' }, + { key: 'total_price', header: 'Total', flex: 1.2, align: 'right' }, + { key: 'payment_amount', header: 'Pembayaran', flex: 1.2, align: 'right' }, + { key: 'accounts_receivable', header: 'Saldo', flex: 1.2, align: 'right' }, + { key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' }, + { key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' }, + { key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' }, +]; + +const getTableData = ( + rows: CustomerPaymentReport['rows'] +): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1 }, + { + key: 'trans_date', + value: item.trans_date ? formatDate(item.trans_date, 'DD MMM YY') : '-', + }, + { + key: 'delivery_date', + value: item.delivery_date + ? formatDate(item.delivery_date, 'DD MMM YY') + : '-', + }, + { + key: 'aging', + value: + item.aging_day != null ? `${formatNumber(item.aging_day)} hari` : '-', + }, + { key: 'reference', value: item.reference || '-' }, + { + key: 'vehicle_numbers', + value: + Array.isArray(item.vehicle_numbers) && item.vehicle_numbers.length > 0 + ? item.vehicle_numbers.join(', ') + : '-', + }, + { key: 'qty', value: formatNumber(item.qty), align: 'right' }, + { key: 'weight', value: formatNumber(item.weight), align: 'right' }, + { + key: 'average_weight', + value: formatNumber(item.average_weight), + align: 'right', + }, + { + key: 'unit_price', + value: formatCurrency(item.unit_price), + align: 'right', + }, + { + key: 'final_price', + value: formatCurrency(item.final_price), + align: 'right', + }, + { + key: 'total_price', + value: formatCurrency(item.total_price), + align: 'right', + }, + { + key: 'payment_amount', + value: formatCurrency(item.payment_amount), + align: 'right', + }, + { + key: 'accounts_receivable', + value: formatCurrency(item.accounts_receivable), + align: 'right', + color: item.accounts_receivable < 0 ? '#DC2626' : undefined, + }, + { + key: 'status', + value: item.status ? ( + + {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + + ) : ( + '-' + ), + }, + { + key: 'pickup_info', + value: + Array.isArray(item.pickup_info) && item.pickup_info.length > 0 + ? item.pickup_info.join(', ') + : '-', + }, + { key: 'sales_person', value: item.sales_person || '-' }, + ]); +}; + +const getTableFooter = ( + summary: CustomerPaymentReport['summary'] +): PdfTfootCell[] => [ + { key: 'no', value: 'Total' }, + { key: 'trans_date', value: '' }, + { key: 'delivery_date', value: '' }, + { key: 'aging', value: '' }, + { key: 'reference', value: '' }, + { key: 'vehicle_numbers', value: '' }, + { key: 'qty', value: formatNumber(summary?.total_qty || 0), align: 'right' }, + { + key: 'weight', + value: formatNumber(summary?.total_weight || 0), + align: 'right', + }, + { key: 'average_weight', value: '' }, + { key: 'unit_price', value: '' }, + { + key: 'final_price', + value: formatCurrency(summary?.total_final_amount || 0), + align: 'right', + }, + { + key: 'total_price', + value: formatCurrency(summary?.total_grand_amount || 0), + align: 'right', + }, + { + key: 'payment_amount', + value: formatCurrency(summary?.total_payment || 0), + align: 'right', + }, + { + key: 'accounts_receivable', + value: formatCurrency(summary?.total_accounts_receivable || 0), + align: 'right', + color: + (summary?.total_accounts_receivable || 0) < 0 ? '#DC2626' : undefined, + }, + { key: 'status', value: '' }, + { key: 'pickup_info', value: '' }, + { key: 'sales_person', value: '' }, +]; + const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { return ( @@ -269,329 +343,27 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {/* Table */} - - {/* Table Header */} - - - No - - - Tanggal DO - - - Tanggal Realisasi - - - Aging - - - Referensi - - - No Polisi - - - Qty - - - Berat - - - Rata-Rata - - - Harga/Unit - - - Harga Akhir - - - Total - - - Pembayaran - - - Saldo - - - Keterangan - - - Pengambilan - - - Sales - - - - {/* Table Body */} - <> - {/* Initial Balance Row */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {formatCurrency(customerReport.initial_balance || 0)} - - - - - - - - - - - - - - {/* Data Rows */} - {customerReport.rows.map((item, index) => ( - - - {index + 1} - - - - {item.trans_date - ? formatDate(item.trans_date, 'DD MMM YY') - : '-'} - - - - - {item.delivery_date - ? formatDate(item.delivery_date, 'DD MMM YY') - : '-'} - - - - - {item.aging_day != null - ? `${formatNumber(item.aging_day)} hari` - : '-'} - - - - {item.reference || '-'} - - - - {Array.isArray(item.vehicle_numbers) - ? item.vehicle_numbers.length > 0 - ? item.vehicle_numbers.join(', ') - : '-' - : '-'} - - - - {formatNumber(item.qty)} - - - {formatNumber(item.weight)} - - - {formatNumber(item.average_weight)} - - - {formatCurrency(item.unit_price)} - - - {formatCurrency(item.final_price)} - - - {formatCurrency(item.total_price)} - - - {formatCurrency(item.payment_amount)} - - - - {formatCurrency(item.accounts_receivable)} - - - - {item.status ? ( - - - {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} - - - ) : ( - - - )} - - - - {Array.isArray(item.pickup_info) - ? item.pickup_info.length > 0 - ? item.pickup_info.join(', ') - : '-' - : '-'} - - - - {item.sales_person || '-'} - - - ))} - - - {/* Summary Row */} - {customerReport.summary && ( - - - Total - - - - - - - - - - - - - - - - - - {formatNumber(customerReport.summary.total_qty)} - - - - {formatNumber(customerReport.summary.total_weight)} - - - - - - - - - - - {formatCurrency(customerReport.summary.total_final_amount)} - - - - - {formatCurrency(customerReport.summary.total_grand_amount)} - - - - - {formatCurrency(customerReport.summary.total_payment)} - - - - - {formatCurrency( - customerReport.summary.total_accounts_receivable - )} - - - - - - - - - - - - - )} - + ))} diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx index a7967159..bd6f301a 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -11,6 +11,11 @@ import { } from '@react-pdf/renderer'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, +} from '@/components/helper/pdf/table'; Font.register({ family: 'Helvetica', @@ -39,117 +44,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 8, color: '#1f74bf', }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'left', - }, - tableCellNo: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableCellLast: { - flex: 1, - padding: 4, - fontSize: 8, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellHeaderLast: { - flex: 1, - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableCellCenterLast: { - flex: 1, - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, badge: { backgroundColor: '#1f74bf', color: '#FFFFFF', @@ -174,6 +68,12 @@ const pdfStyles = StyleSheet.create({ flexWrap: 'wrap', marginBottom: 8, }, + supplierSection: { + marginBottom: 10, + }, + supplierSectionBreak: { + marginBottom: 15, + }, }); interface PurchasesPerSupplierExportParams { @@ -218,6 +118,85 @@ const getParameterText = ( return paramsText; }; +// Helper functions for PdfTable +const getTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'receive_date', header: 'Tanggal Terima', flex: 1, align: 'center' }, + { key: 'po_date', header: 'Tanggal PO', flex: 1, align: 'center' }, + { key: 'po_number', header: 'Referensi', flex: 1, align: 'left' }, + { key: 'product', header: 'Produk', flex: 1, align: 'left' }, + { key: 'warehouse', header: 'Tujuan', flex: 1, align: 'left' }, + { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, + { key: 'unit_price', header: 'Harga Beli', flex: 1.2, align: 'right' }, + { + key: 'purchase_value', + header: 'Nilai Pembelian', + flex: 1.5, + align: 'right', + }, + { + key: 'transport_price', + header: 'Biaya Transport', + flex: 1.2, + align: 'right', + }, + { key: 'total_amount', header: 'Total', flex: 1.5, align: 'right' }, + { key: 'expedition', header: 'Armada', flex: 1.2, align: 'center' }, + { key: 'delivery_number', header: 'Surat Jalan', flex: 1, align: 'left' }, +]; + +const getTableData = ( + rows: LogisticPurchasePerSupplierReport['rows'] +): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1, align: 'center' }, + { + key: 'receive_date', + value: formatDate(item.receive_date, 'DD-MMM-YYYY'), + align: 'center', + }, + { + key: 'po_date', + value: formatDate(item.po_date, 'DD-MMM-YYYY'), + align: 'center', + }, + { key: 'po_number', value: item.po_number || '-' }, + { key: 'product', value: item.product?.name || '-' }, + { key: 'warehouse', value: item.warehouse?.name || '-' }, + { key: 'qty', value: formatNumber(item.qty || 0), align: 'right' }, + { + key: 'unit_price', + value: formatCurrency(item.unit_price || 0), + align: 'right', + }, + { + key: 'purchase_value', + value: formatCurrency(item.purchase_value || 0), + align: 'right', + }, + { + key: 'transport_price', + value: formatCurrency(item.transport_unit_price || 0), + align: 'right', + }, + { + key: 'total_amount', + value: formatCurrency(item.total_amount || 0), + align: 'right', + }, + { + key: 'expedition', + value: ( + + {item.expedition || '-'} + + ), + align: 'center', + }, + { key: 'delivery_number', value: item.delivery_number || '-' }, + ]); +}; + const createPDFDocument = ( supplierReports: LogisticPurchasePerSupplierReport[], params: PurchasesPerSupplierExportParams['params'] @@ -266,114 +245,10 @@ const createPDFDocument = ( {supplierReport.supplier.name} - - {/* Table Header */} - - - No - - - Tanggal Terima - - - Tanggal PO - - - Referensi - - - Produk - - - Tujuan - - - Qty - - - Harga Beli - - - Nilai Pembelian - - - Biaya Transport - - - Total - - - Armada - - - Surat Jalan - - - - {/* Table Body */} - {supplierReport.rows.map( - ( - item: LogisticPurchasePerSupplierReport['rows'][number], - index: number - ) => ( - - - {index + 1} - - - - {formatDate(item.receive_date, 'DD-MMM-YYYY')} - - - - {formatDate(item.po_date, 'DD-MMM-YYYY')} - - - {item.po_number || '-'} - - - {item.product?.name || '-'} - - - {item.warehouse?.name || '-'} - - - {formatNumber(item.qty || 0)} - - - {formatCurrency(item.unit_price || 0)} - - - {formatCurrency(item.purchase_value || 0)} - - - - {formatCurrency(item.transport_unit_price || 0)} - - - - {formatCurrency(item.total_amount || 0)} - - - - {item.expedition || '-'} - - - - {item.delivery_number || '-'} - - - ) - )} - + ); } diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index c01eeb61..5366f3cd 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -29,6 +29,7 @@ import Menu from '@/components/menu/Menu'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; +import { Icon } from '@iconify/react'; const PurchasesPerSupplierTab = () => { // ===== STATE MANAGEMENT ===== @@ -723,27 +724,6 @@ const PurchasesPerSupplierTab = () => { subtitle='Laporan > Rekapitulasi Pembelian Per Supplier' className={{ wrapper: 'w-full', body: 'p-1!' }} > -
- - - - Export - - } - align='end' - > - - - - - -
{ />
+
+ + + + Export + + + } + align='end' + > + + + + + +
{!isSubmitted ? (
@@ -880,18 +888,25 @@ const PurchasesPerSupplierTab = () => { key={supplierReport.supplier.id} title={supplierReport.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} - className={{ wrapper: 'w-full' }} + className={{ + wrapper: 'w-full rounded-2xl', + body: 'p-0', + title: + 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + subtitle: + 'px-3 pb-1 bg-primary text-white text-sm font-normal', + }} variant='bordered' collapsible={true} > 0} className={{ - containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto mt-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 9b05a88d..3a76d8f4 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -15,6 +15,12 @@ import { HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { formatDate, formatNumber, formatCurrency } from '@/lib/helper'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, + PdfTfootCell, +} from '@/components/helper/pdf/table'; Font.register({ family: 'Helvetica', @@ -43,85 +49,6 @@ const pdfStyles = StyleSheet.create({ marginBottom: 8, color: '#1f74bf', }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'left', - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 8, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, parameterBadge: { backgroundColor: '#F5F5F5', color: '#333333', @@ -136,6 +63,9 @@ const pdfStyles = StyleSheet.create({ flexWrap: 'wrap', marginBottom: 8, }, + section: { + marginBottom: 15, + }, }); interface HppPerKandangExportParams { @@ -192,6 +122,215 @@ const getParameterText = (params: HppPerKandangExportParams['params']) => { return paramsText; }; +// Helper functions for PdfTable - Rekapitulasi +const getRekapitulasiColumns = (): PdfColumn[] => [ + { key: 'rentang_bw', header: 'Rentang BW', flex: 1.2, align: 'center' }, + { key: 'sisa_butir', header: 'Sisa Butir', flex: 1, align: 'right' }, + { key: 'sisa_kg', header: 'Sisa Kg', flex: 1, align: 'right' }, + { + key: 'rata_rata_bobot', + header: 'Rata-Rata Bobot (Kg)', + flex: 1.2, + align: 'right', + }, + { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left' }, + { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left' }, + { + key: 'rata_harga_doc', + header: 'Rata-Rata Harga DOC', + flex: 1.2, + align: 'right', + }, + { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1.2, align: 'right' }, + { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, +]; + +const getRekapitulasiData = ( + perWeightRange: HppPerKandangPerWeightRange[] +): PdfTbodyCell[][] => { + return perWeightRange.map((group) => [ + { key: 'rentang_bw', value: group.label, align: 'center' }, + { + key: 'sisa_butir', + value: formatNumber(group.egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(group.egg_production_kg), + align: 'right', + }, + { + key: 'rata_rata_bobot', + value: formatNumber(group.avg_weight_kg), + align: 'right', + }, + { + key: 'feed_supplier', + value: + group.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'doc_supplier', + value: + group.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'rata_harga_doc', + value: formatCurrency(group.average_doc_price_rp), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(group.egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(group.egg_value_rp), + align: 'right', + }, + ]); +}; + +// Helper functions for PdfTable - Detail Per Kandang +const getDetailColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'kandang', header: 'Kandang', flex: 1.5, align: 'left' }, + { key: 'rentang_bw', header: 'Rentang BW', flex: 1, align: 'left' }, + { + key: 'rata_rata_bobot', + header: 'Rata-Rata Bobot (Kg)', + flex: 1, + align: 'right', + }, + { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, + { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, + { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left' }, + { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left' }, + { + key: 'rata_harga_doc', + header: 'Rata-Rata Harga DOC', + flex: 1.2, + align: 'right', + }, + { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1, align: 'right' }, + { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, +]; + +const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1, align: 'center' }, + { key: 'kandang', value: item.kandang?.name || '-' }, + { + key: 'rentang_bw', + value: `${item.weight_range.weight_min.toFixed(2)} - ${item.weight_range.weight_max.toFixed(2)}`, + }, + { + key: 'rata_rata_bobot', + value: formatNumber(item.avg_weight_kg), + align: 'right', + }, + { + key: 'sisa_butir', + value: formatNumber(item.egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(item.egg_production_kg), + align: 'right', + }, + { + key: 'feed_supplier', + value: + item.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'doc_supplier', + value: + item.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-', + }, + { + key: 'rata_harga_doc', + value: formatCurrency(item.average_doc_price_rp), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(item.egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(item.egg_value_rp), + align: 'right', + }, + ]); +}; + +const getDetailFooter = ( + summary: HppPerKandangReport['summary'] +): PdfTfootCell[] => { + if (!summary?.total) return []; + + const allFeedSuppliers = + summary.total.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-'; + + const allDocSuppliers = + summary.total.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-'; + + return [ + { key: 'no', value: 'TOTAL' }, + { key: 'kandang', value: 'ALL' }, + { key: 'rentang_bw', value: '-' }, + { + key: 'rata_rata_bobot', + value: formatNumber(summary.total.average_weight_kg), + align: 'right', + }, + { + key: 'sisa_butir', + value: formatNumber(summary.total.total_egg_production_pieces), + align: 'right', + }, + { + key: 'sisa_kg', + value: formatNumber(summary.total.total_egg_production_kg), + align: 'right', + }, + { key: 'feed_supplier', value: allFeedSuppliers }, + { key: 'doc_supplier', value: allDocSuppliers }, + { + key: 'rata_harga_doc', + value: formatCurrency(summary.total.total_average_doc_price_rp), + align: 'right', + }, + { + key: 'hpp_telur', + value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg), + align: 'right', + }, + { + key: 'nominal_sisa', + value: formatCurrency(summary.total.total_egg_value_rp), + align: 'right', + }, + ]; +}; + const createPDFDocument = ( data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams['params'] @@ -216,404 +355,23 @@ const createPDFDocument = ( {/* Rekapitulasi Section */} - + Rekapitulasi - - - {/* Table Header */} - - - Rentang BW - - - Sisa Butir - - - Sisa Kg - - - Rata-Rata Bobot (Kg) - - - Feed (Supplier) - - - DOC (Supplier) - - - Rata-Rata Harga DOC - - - HPP Telur (RP/KG) - - - Nominal Sisa - - - - {/* Table Body - Rekapitulasi */} - {rekapitulasiByWeightRange.map( - (group: HppPerKandangPerWeightRange, index: number) => ( - - - {group.label} - - - {formatNumber(group.egg_production_pieces)} - - - {formatNumber(group.egg_production_kg)} - - - {formatNumber(group.avg_weight_kg)} - - - - {group.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ') || '-'} - - - - - {group.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ') || '-'} - - - - {formatCurrency(group.average_doc_price_rp)} - - - {formatCurrency(group.egg_hpp_rp_per_kg)} - - - {formatCurrency(group.egg_value_rp)} - - - ) - )} - + {/* Detail Per Kandang Section */} - + Detail Per Kandang - - - {/* Table Header */} - - - No - - - Kandang - - - Rentang BW - - - Rata-Rata Bobot (Kg) - - - Sisa Butir - - - Sisa Kg (Telur) - - - Feed (Supplier) - - - DOC (Supplier) - - - Rata-Rata Harga DOC - - - HPP Telur (RP/KG) - - - Nominal Sisa - - - - {/* Table Body - Detail Per Kandang */} - {data.rows.map((item: HppPerKandangRow, index: number) => ( - - - {index + 1} - - - {item.kandang?.name || '-'} - - - - {item.weight_range.weight_min.toFixed(2)} -{' '} - {item.weight_range.weight_max.toFixed(2)} - - - - {formatNumber(item.avg_weight_kg)} - - - {formatNumber(item.egg_production_pieces)} - - - {formatNumber(item.egg_production_kg)} - - - - {item.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ')} - - - - - {item.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - .join(' | ')} - - - - {formatCurrency(item.average_doc_price_rp)} - - - {formatCurrency(item.egg_hpp_rp_per_kg)} - - - {formatCurrency(item.egg_value_rp)} - - - ))} - - {/* TOTAL Row */} - {data.summary?.total && ( - - - TOTAL - - - ALL - - - - - - - - {formatNumber(data.summary.total.average_weight_kg)} - - - - - {formatNumber( - data.summary.total.total_egg_production_pieces - )} - - - - - {formatNumber(data.summary.total.total_egg_production_kg)} - - - - - {data.rows - .flatMap((row: HppPerKandangRow) => - row.feed_suppliers?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - ) - .filter( - (v: string, i: number, a: string[]) => - a.indexOf(v) === i - ) - .join(' | ') || '-'} - - - - - {data.rows - .flatMap((row: HppPerKandangRow) => - row.doc_suppliers?.map( - (s: { alias?: string; name: string }) => - s.alias || s.name - ) - ) - .filter( - (v: string, i: number, a: string[]) => - a.indexOf(v) === i - ) - .join(' | ') || '-'} - - - - - {formatCurrency( - data.summary.total.total_average_doc_price_rp - )} - - - - - {formatCurrency( - data.summary.total.average_egg_hpp_rp_per_kg - )} - - - - - {formatCurrency(data.summary.total.total_egg_value_rp)} - - - - )} - +