From 142ce7fe3afaf5a92a6c57d6ad26bcbd4608b4e7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 27 Jan 2026 16:23:29 +0700 Subject: [PATCH] feat(FE): Add PDF table components for report export --- .../helper/pdf/layout/PdfContainer.tsx | 0 src/components/helper/pdf/table/PdfTable.tsx | 21 ++ src/components/helper/pdf/table/PdfTbody.tsx | 236 ++++++++++++++++++ src/components/helper/pdf/table/PdfTfoot.tsx | 124 +++++++++ src/components/helper/pdf/table/PdfThead.tsx | 89 +++++++ 5 files changed, 470 insertions(+) create mode 100644 src/components/helper/pdf/layout/PdfContainer.tsx create mode 100644 src/components/helper/pdf/table/PdfTable.tsx create mode 100644 src/components/helper/pdf/table/PdfTbody.tsx create mode 100644 src/components/helper/pdf/table/PdfTfoot.tsx create mode 100644 src/components/helper/pdf/table/PdfThead.tsx diff --git a/src/components/helper/pdf/layout/PdfContainer.tsx b/src/components/helper/pdf/layout/PdfContainer.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx new file mode 100644 index 00000000..46724ef6 --- /dev/null +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -0,0 +1,21 @@ +'use client'; + +import { View, StyleSheet } from '@react-pdf/renderer'; +import { PdfColumn } from './PdfThead'; + +const styles = StyleSheet.create({ + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, +}); + +interface PdfTableProps { + columns: PdfColumn[]; + children: React.ReactNode; +} + +export const PdfTable = ({ columns, children }: PdfTableProps) => { + return {children}; +}; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx new file mode 100644 index 00000000..9a23ab4b --- /dev/null +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -0,0 +1,236 @@ +'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', + }, + badge: { + paddingVertical: 2, + paddingHorizontal: 4, + borderRadius: 12, + fontSize: 5, + fontWeight: 'bold', + borderWidth: 1, + textAlign: 'center', + whiteSpace: 'nowrap', + }, +}); + +interface PdfTbodyProps { + columns: PdfColumn[]; + rows: PdfTbodyCell[][]; + initialBalanceRow?: { + 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, + initialBalanceRow, +}: PdfTbodyProps) => { + return ( + <> + {/* Initial Balance Row */} + {initialBalanceRow && ( + + {columns.map((column, index) => { + const isLastColumn = index === columns.length - 1; + const isInitialBalanceColumn = + column.key === initialBalanceRow.valueKey; + const align = column.align || 'center'; + + const cellStyle = + column.key === 'no' + ? [styles.tableCellNo, { flex: column.flex }] + : isInitialBalanceColumn + ? [ + styles.tableCellRight, + { + flex: column.flex, + color: initialBalanceRow.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 ( + + + {isInitialBalanceColumn ? initialBalanceRow.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 // For custom React element (badge, etc) + ) : ( + {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..900673e0 --- /dev/null +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -0,0 +1,124 @@ +'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 }] + : cellData?.align === 'right' + ? [ + styles.tableCellRight, + { + flex: column.flex, + color: cellData?.color || 'black', + }, + ] + : cellData?.align === 'center' + ? [ + styles.tableCellCenter, + { + flex: column.flex, + color: cellData?.color || 'black', + }, + ] + : 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} + + ); + })} + + ); +};