From 70b63f7773e58141727929103f8bcad87a5b1905 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:38:51 +0700 Subject: [PATCH] refactor(FE): Refactor PdfTable components to support generic data types --- src/components/helper/pdf/table/PdfTable.tsx | 35 ++++--- src/components/helper/pdf/table/PdfTbody.tsx | 98 +++++++++----------- src/components/helper/pdf/table/PdfTfoot.tsx | 86 +++++++++-------- src/components/helper/pdf/table/PdfThead.tsx | 43 ++++++--- src/components/helper/pdf/table/index.ts | 4 +- src/components/helper/pdf/table/types.ts | 24 +++++ 6 files changed, 167 insertions(+), 123 deletions(-) create mode 100644 src/components/helper/pdf/table/types.ts diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx index 27369db5..86f4ed77 100644 --- a/src/components/helper/pdf/table/PdfTable.tsx +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -1,9 +1,10 @@ 'use client'; import { View, StyleSheet } from '@react-pdf/renderer'; -import { PdfThead, PdfColumn } from './PdfThead'; -import { PdfTbody, PdfTbodyCell } from './PdfTbody'; -import { PdfTfoot, PdfTfootCell } from './PdfTfoot'; +import type { PdfColumn } from './types'; +import { PdfThead } from './PdfThead'; +import { PdfTbody } from './PdfTbody'; +import { PdfTfoot } from './PdfTfoot'; const styles = StyleSheet.create({ table: { @@ -13,10 +14,10 @@ const styles = StyleSheet.create({ }, }); -interface PdfTableProps { - columns: PdfColumn[]; - data: PdfTbodyCell[][]; - footer?: PdfTfootCell[]; +interface PdfTableProps> { + columns: PdfColumn[]; + data: TData[]; + showFooter?: boolean; footerLabel?: string; firstRow?: { valueKey: string; @@ -26,20 +27,26 @@ interface PdfTableProps { }; } -export const PdfTable = ({ +export const PdfTable = ,>({ columns, data, - footer, + showFooter = false, footerLabel = 'Total', firstRow, -}: PdfTableProps) => { +}: PdfTableProps) => { + // Check if any column has footer defined + const hasFooter = + showFooter || columns.some((col) => col.footer !== undefined); + return ( - - - {footer && footer.length > 0 && ( - + + + {hasFooter && data.length > 0 && ( + )} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx index fee79726..cc9fe41d 100644 --- a/src/components/helper/pdf/table/PdfTbody.tsx +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -1,22 +1,8 @@ '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; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -71,21 +57,22 @@ const styles = StyleSheet.create({ }, }); -interface PdfTbodyProps { - columns: PdfColumn[]; - rows: PdfTbodyCell[][]; +interface PdfTbodyProps> { + columns: PdfColumn[]; + data: TData[]; 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) => { +export const PdfTbody = ,>({ + columns, + data, + firstRow, +}: PdfTbodyProps) => { return ( <> {/* First Row */} @@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const isfirstRowColumn = column.key === firstRow.valueKey; - const align = column.align || 'center'; + const isFirstRowColumn = column.key === firstRow.valueKey; + const align = column.align || 'left'; const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] - : isfirstRowColumn + ? [styles.tableCellNo, { flex: column.flex || 1 }] + : isFirstRowColumn ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, color: firstRow.color || 'black', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellLast, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: 0, }, ] - : [styles.tableCell, { flex: column.flex }]; + : [styles.tableCell, { flex: column.flex || 1 }]; return ( - {isfirstRowColumn ? firstRow.value : ''} + {isFirstRowColumn ? firstRow.value : ''} ); })} @@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { )} {/* Data Rows */} - {rows.map((row, rowIndex) => { - const isLastRow = rowIndex === rows.length - 1; + {data.map((row, rowIndex) => { + const isLastRow = rowIndex === data.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 align = column.align || 'left'; + + // Get cell content from column.cell function or fallback to row value + let cellContent: ReactNode; + if (column.cell) { + cellContent = column.cell({ row, index: rowIndex }); + } else { + cellContent = + ((row as Record)[column.key] as ReactNode) ?? + '-'; + } const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] + ? [styles.tableCellNo, { flex: column.flex || 1 }] : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn ? [ styles.tableCellLast, - { flex: column.flex, borderRightWidth: 0 }, + { flex: column.flex || 1, borderRightWidth: 0 }, ] : [ styles.tableCell, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ]; return ( - {cell?.value !== undefined && - cell?.value !== null && - cell?.value !== '' ? ( - typeof cell.value === 'object' ? ( - cell.value - ) : ( - {String(cell.value)} - ) + {typeof cellContent === 'string' || + typeof cellContent === 'number' ? ( + {String(cellContent)} ) : ( - - + cellContent )} ); @@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTfoot.tsx b/src/components/helper/pdf/table/PdfTfoot.tsx index a9f209b1..9d974f38 100644 --- a/src/components/helper/pdf/table/PdfTfoot.tsx +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -1,21 +1,8 @@ '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; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -69,63 +56,86 @@ const styles = StyleSheet.create({ }, }); -interface PdfTfootProps { - columns: PdfColumn[]; - cells: PdfTfootCell[]; +interface PdfTfootProps> { + columns: PdfColumn[]; + data: TData[]; label?: string; } -export const PdfTfoot = ({ +export const PdfTfoot = ,>({ columns, - cells, + data, label = 'Total', -}: PdfTfootProps) => { +}: PdfTfootProps) => { return ( {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const cellData = cells.find((c) => c.key === column.key); + + // Get footer content from column definition + let footerContent: ReactNode; + if (typeof column.footer === 'function') { + footerContent = column.footer(data); + } else { + footerContent = column.footer; + } + + // Use label for first column (usually 'no' column) + const displayContent = column.key === 'no' ? label : footerContent; + + // Determine alignment + const align = column.footerAlign || column.align || 'left'; + const color = column.footerColor || 'black'; const cellStyle = column.key === 'no' ? [ styles.tableCellNo, - { flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 }, + { + flex: column.flex || 1, + borderRightWidth: isLastColumn ? 0 : 1, + color, + }, ] - : cellData?.align === 'right' + : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] - : cellData?.align === 'center' + : align === 'center' ? [ styles.tableCellCenter, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn - ? [styles.tableCellLast, { flex: column.flex }] - : [ - styles.tableCell, - { - flex: column.flex, - color: cellData?.color || 'black', - }, - ]; + ? [styles.tableCellLast, { flex: column.flex || 1, color }] + : [styles.tableCell, { flex: column.flex || 1, color }]; return ( - {column.key === 'no' ? label : cellData?.value || ''} + {displayContent !== undefined && displayContent !== null ? ( + typeof displayContent === 'string' || + typeof displayContent === 'number' ? ( + {String(displayContent)} + ) : ( + displayContent + ) + ) : ( + - + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfThead.tsx b/src/components/helper/pdf/table/PdfThead.tsx index 89037216..889f9f34 100644 --- a/src/components/helper/pdf/table/PdfThead.tsx +++ b/src/components/helper/pdf/table/PdfThead.tsx @@ -1,13 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -48,23 +43,37 @@ const styles = StyleSheet.create({ }, }); -interface PdfTheadProps { - columns: PdfColumn[]; +interface PdfTheadProps> { + columns: PdfColumn[]; + data?: TData[]; } -export const PdfThead = ({ columns }: PdfTheadProps) => { +export const PdfThead = ,>({ + columns, + data, +}: PdfTheadProps) => { return ( {columns.map((column, index) => { - const align = column.align || 'center'; const isLastColumn = index === columns.length - 1; + // Get header content from column definition + let headerContent: ReactNode; + if (typeof column.header === 'function') { + headerContent = column.header(data || []); + } else { + headerContent = column.header || column.key; + } + + // Determine alignment - columns align right by default for numeric data + const align = column.align || 'left'; + const cellStyle = align === 'right' ? [ styles.tableCellHeaderRight, { - flex: column.flex, + flex: column.flex || 1, textAlign: 'right' as const, borderRightWidth: isLastColumn ? 0 : 1, }, @@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { : [ styles.tableCellHeader, { - flex: column.flex, + flex: column.flex || 1, textAlign: align as 'left' | 'center' | 'right', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { return ( - {column.header} + {typeof headerContent === 'string' ? ( + {headerContent} + ) : ( + headerContent + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/index.ts b/src/components/helper/pdf/table/index.ts index 35839f17..3c780688 100644 --- a/src/components/helper/pdf/table/index.ts +++ b/src/components/helper/pdf/table/index.ts @@ -2,6 +2,4 @@ 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'; +export type { PdfColumn } from './types'; diff --git a/src/components/helper/pdf/table/types.ts b/src/components/helper/pdf/table/types.ts new file mode 100644 index 00000000..c2437f13 --- /dev/null +++ b/src/components/helper/pdf/table/types.ts @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; + +/** + * PdfColumn - Mirip dengan ColumnDef di TanStack Table + * Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi + */ +export interface PdfColumn> { + key: string; + flex?: number; + + // Header configuration (thead) + header?: string | ((data: TData[]) => ReactNode); + + // Body configuration (tbody) + align?: 'left' | 'center' | 'right'; + cell?: (props: { row: TData; index: number }) => ReactNode | string | number; + + // Footer configuration (tfoot) + footer?: string | number | ((data: TData[]) => ReactNode | string | number); + footerAlign?: 'left' | 'center' | 'right'; + footerColor?: string; +} + +export type { PdfColumn as default };