refactor(FE): Refactor PdfTable components to support generic data types

This commit is contained in:
rstubryan
2026-02-11 10:38:51 +07:00
parent 02d13efc25
commit 70b63f7773
6 changed files with 167 additions and 123 deletions
+21 -14
View File
@@ -1,9 +1,10 @@
'use client'; 'use client';
import { View, StyleSheet } from '@react-pdf/renderer'; import { View, StyleSheet } from '@react-pdf/renderer';
import { PdfThead, PdfColumn } from './PdfThead'; import type { PdfColumn } from './types';
import { PdfTbody, PdfTbodyCell } from './PdfTbody'; import { PdfThead } from './PdfThead';
import { PdfTfoot, PdfTfootCell } from './PdfTfoot'; import { PdfTbody } from './PdfTbody';
import { PdfTfoot } from './PdfTfoot';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
table: { table: {
@@ -13,10 +14,10 @@ const styles = StyleSheet.create({
}, },
}); });
interface PdfTableProps { interface PdfTableProps<TData = Record<string, unknown>> {
columns: PdfColumn[]; columns: PdfColumn<TData>[];
data: PdfTbodyCell[][]; data: TData[];
footer?: PdfTfootCell[]; showFooter?: boolean;
footerLabel?: string; footerLabel?: string;
firstRow?: { firstRow?: {
valueKey: string; valueKey: string;
@@ -26,20 +27,26 @@ interface PdfTableProps {
}; };
} }
export const PdfTable = ({ export const PdfTable = <TData = Record<string, unknown>,>({
columns, columns,
data, data,
footer, showFooter = false,
footerLabel = 'Total', footerLabel = 'Total',
firstRow, firstRow,
}: PdfTableProps) => { }: PdfTableProps<TData>) => {
// Check if any column has footer defined
const hasFooter =
showFooter || columns.some((col) => col.footer !== undefined);
return ( return (
<View style={styles.table}> <View style={styles.table}>
<PdfThead columns={columns} /> <PdfThead columns={columns} data={data} />
<PdfTbody columns={columns} rows={data} firstRow={firstRow} /> <PdfTbody columns={columns} data={data} firstRow={firstRow} />
{footer && footer.length > 0 && ( {hasFooter && data.length > 0 && (
<PdfTfoot columns={columns} cells={footer} label={footerLabel} /> <PdfTfoot columns={columns} data={data} label={footerLabel} />
)} )}
</View> </View>
); );
}; };
export type { PdfColumn };
+44 -54
View File
@@ -1,22 +1,8 @@
'use client'; 'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer'; import { Text, View, StyleSheet } from '@react-pdf/renderer';
import { ReactNode } from 'react';
export interface PdfColumn { import type { PdfColumn } from './types';
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({ const styles = StyleSheet.create({
tableRow: { tableRow: {
@@ -71,21 +57,22 @@ const styles = StyleSheet.create({
}, },
}); });
interface PdfTbodyProps { interface PdfTbodyProps<TData = Record<string, unknown>> {
columns: PdfColumn[]; columns: PdfColumn<TData>[];
rows: PdfTbodyCell[][]; data: TData[];
firstRow?: { firstRow?: {
valueKey: string; valueKey: string;
value: number; value: number;
align?: 'right'; align?: 'right';
color?: string; 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 = <TData = Record<string, unknown>,>({
columns,
data,
firstRow,
}: PdfTbodyProps<TData>) => {
return ( return (
<> <>
{/* First Row */} {/* First Row */}
@@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
<View style={[styles.tableRow, styles.tableBorderBottom]}> <View style={[styles.tableRow, styles.tableBorderBottom]}>
{columns.map((column, index) => { {columns.map((column, index) => {
const isLastColumn = index === columns.length - 1; const isLastColumn = index === columns.length - 1;
const isfirstRowColumn = column.key === firstRow.valueKey; const isFirstRowColumn = column.key === firstRow.valueKey;
const align = column.align || 'center'; const align = column.align || 'left';
const cellStyle = const cellStyle =
column.key === 'no' column.key === 'no'
? [styles.tableCellNo, { flex: column.flex }] ? [styles.tableCellNo, { flex: column.flex || 1 }]
: isfirstRowColumn : isFirstRowColumn
? [ ? [
styles.tableCellRight, styles.tableCellRight,
{ {
flex: column.flex, flex: column.flex || 1,
color: firstRow.color || 'black', color: firstRow.color || 'black',
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
@@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [ ? [
styles.tableCellRight, styles.tableCellRight,
{ {
flex: column.flex, flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
@@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [ ? [
styles.tableCellCenter, styles.tableCellCenter,
{ {
flex: column.flex, flex: column.flex || 1,
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
@@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [ ? [
styles.tableCellLast, styles.tableCellLast,
{ {
flex: column.flex, flex: column.flex || 1,
borderRightWidth: 0, borderRightWidth: 0,
}, },
] ]
: [styles.tableCell, { flex: column.flex }]; : [styles.tableCell, { flex: column.flex || 1 }];
return ( return (
<View key={column.key} style={cellStyle}> <View key={column.key} style={cellStyle}>
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text> <Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
</View> </View>
); );
})} })}
@@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
)} )}
{/* Data Rows */} {/* Data Rows */}
{rows.map((row, rowIndex) => { {data.map((row, rowIndex) => {
const isLastRow = rowIndex === rows.length - 1; const isLastRow = rowIndex === data.length - 1;
return ( return (
<View <View
@@ -156,19 +143,27 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
]} ]}
> >
{columns.map((column, colIndex) => { {columns.map((column, colIndex) => {
const cell = row.find((c) => c.key === column.key);
const isLastColumn = colIndex === columns.length - 1; 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<string, unknown>)[column.key] as ReactNode) ??
'-';
}
const cellStyle = const cellStyle =
column.key === 'no' column.key === 'no'
? [styles.tableCellNo, { flex: column.flex }] ? [styles.tableCellNo, { flex: column.flex || 1 }]
: align === 'right' : align === 'right'
? [ ? [
styles.tableCellRight, styles.tableCellRight,
{ {
flex: column.flex, flex: column.flex || 1,
color: cell?.color || 'black',
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
@@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
? [ ? [
styles.tableCellCenter, styles.tableCellCenter,
{ {
flex: column.flex, flex: column.flex || 1,
color: cell?.color || 'black',
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
: isLastColumn : isLastColumn
? [ ? [
styles.tableCellLast, styles.tableCellLast,
{ flex: column.flex, borderRightWidth: 0 }, { flex: column.flex || 1, borderRightWidth: 0 },
] ]
: [ : [
styles.tableCell, styles.tableCell,
{ {
flex: column.flex, flex: column.flex || 1,
color: cell?.color || 'black',
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
]; ];
return ( return (
<View key={column.key} style={cellStyle}> <View key={column.key} style={cellStyle}>
{cell?.value !== undefined && {typeof cellContent === 'string' ||
cell?.value !== null && typeof cellContent === 'number' ? (
cell?.value !== '' ? ( <Text>{String(cellContent)}</Text>
typeof cell.value === 'object' ? (
cell.value
) : ( ) : (
<Text>{String(cell.value)}</Text> cellContent
)
) : (
<Text>-</Text>
)} )}
</View> </View>
); );
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
</> </>
); );
}; };
export type { PdfColumn };
+48 -38
View File
@@ -1,21 +1,8 @@
'use client'; 'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer'; import { Text, View, StyleSheet } from '@react-pdf/renderer';
import { ReactNode } from 'react';
export interface PdfColumn { import type { PdfColumn } from './types';
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({ const styles = StyleSheet.create({
tableRow: { tableRow: {
@@ -69,63 +56,86 @@ const styles = StyleSheet.create({
}, },
}); });
interface PdfTfootProps { interface PdfTfootProps<TData = Record<string, unknown>> {
columns: PdfColumn[]; columns: PdfColumn<TData>[];
cells: PdfTfootCell[]; data: TData[];
label?: string; label?: string;
} }
export const PdfTfoot = ({ export const PdfTfoot = <TData = Record<string, unknown>,>({
columns, columns,
cells, data,
label = 'Total', label = 'Total',
}: PdfTfootProps) => { }: PdfTfootProps<TData>) => {
return ( return (
<View style={[styles.tableRow, styles.summaryRow]}> <View style={[styles.tableRow, styles.summaryRow]}>
{columns.map((column, index) => { {columns.map((column, index) => {
const isLastColumn = index === columns.length - 1; 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 = const cellStyle =
column.key === 'no' column.key === 'no'
? [ ? [
styles.tableCellNo, 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, styles.tableCellRight,
{ {
flex: column.flex, flex: column.flex || 1,
color: cellData?.color || 'black', color,
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
: cellData?.align === 'center' : align === 'center'
? [ ? [
styles.tableCellCenter, styles.tableCellCenter,
{ {
flex: column.flex, flex: column.flex || 1,
color: cellData?.color || 'black', color,
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
] ]
: isLastColumn : isLastColumn
? [styles.tableCellLast, { flex: column.flex }] ? [styles.tableCellLast, { flex: column.flex || 1, color }]
: [ : [styles.tableCell, { flex: column.flex || 1, color }];
styles.tableCell,
{
flex: column.flex,
color: cellData?.color || 'black',
},
];
return ( return (
<View key={column.key} style={cellStyle}> <View key={column.key} style={cellStyle}>
<Text>{column.key === 'no' ? label : cellData?.value || ''}</Text> {displayContent !== undefined && displayContent !== null ? (
typeof displayContent === 'string' ||
typeof displayContent === 'number' ? (
<Text>{String(displayContent)}</Text>
) : (
displayContent
)
) : (
<Text>-</Text>
)}
</View> </View>
); );
})} })}
</View> </View>
); );
}; };
export type { PdfColumn };
+29 -14
View File
@@ -1,13 +1,8 @@
'use client'; 'use client';
import { Text, View, StyleSheet } from '@react-pdf/renderer'; import { Text, View, StyleSheet } from '@react-pdf/renderer';
import { ReactNode } from 'react';
export interface PdfColumn { import type { PdfColumn } from './types';
key: string;
header: string;
flex: number;
align?: 'left' | 'center' | 'right';
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tableRow: { tableRow: {
@@ -48,23 +43,37 @@ const styles = StyleSheet.create({
}, },
}); });
interface PdfTheadProps { interface PdfTheadProps<TData = Record<string, unknown>> {
columns: PdfColumn[]; columns: PdfColumn<TData>[];
data?: TData[];
} }
export const PdfThead = ({ columns }: PdfTheadProps) => { export const PdfThead = <TData = Record<string, unknown>,>({
columns,
data,
}: PdfTheadProps<TData>) => {
return ( return (
<View style={[styles.tableRow, styles.tableHeader]}> <View style={[styles.tableRow, styles.tableHeader]}>
{columns.map((column, index) => { {columns.map((column, index) => {
const align = column.align || 'center';
const isLastColumn = index === columns.length - 1; 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 = const cellStyle =
align === 'right' align === 'right'
? [ ? [
styles.tableCellHeaderRight, styles.tableCellHeaderRight,
{ {
flex: column.flex, flex: column.flex || 1,
textAlign: 'right' as const, textAlign: 'right' as const,
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
@@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
: [ : [
styles.tableCellHeader, styles.tableCellHeader,
{ {
flex: column.flex, flex: column.flex || 1,
textAlign: align as 'left' | 'center' | 'right', textAlign: align as 'left' | 'center' | 'right',
borderRightWidth: isLastColumn ? 0 : 1, borderRightWidth: isLastColumn ? 0 : 1,
}, },
@@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
return ( return (
<View key={column.key} style={cellStyle}> <View key={column.key} style={cellStyle}>
<Text>{column.header}</Text> {typeof headerContent === 'string' ? (
<Text>{headerContent}</Text>
) : (
headerContent
)}
</View> </View>
); );
})} })}
</View> </View>
); );
}; };
export type { PdfColumn };
+1 -3
View File
@@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable';
export { PdfThead } from './PdfThead'; export { PdfThead } from './PdfThead';
export { PdfTbody } from './PdfTbody'; export { PdfTbody } from './PdfTbody';
export { PdfTfoot } from './PdfTfoot'; export { PdfTfoot } from './PdfTfoot';
export type { PdfColumn } from './PdfThead'; export type { PdfColumn } from './types';
export type { PdfTbodyCell } from './PdfTbody';
export type { PdfTfootCell } from './PdfTfoot';
+24
View File
@@ -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<TData = Record<string, unknown>> {
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 };