mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
feat(FE): Add PDF table components for report export
This commit is contained in:
@@ -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 <View style={styles.table}>{children}</View>;
|
||||||
|
};
|
||||||
@@ -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 && (
|
||||||
|
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
||||||
|
{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 (
|
||||||
|
<View key={column.key} style={cellStyle}>
|
||||||
|
<Text>
|
||||||
|
{isInitialBalanceColumn ? initialBalanceRow.value : ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data Rows */}
|
||||||
|
{rows.map((row, rowIndex) => {
|
||||||
|
const isLastRow = rowIndex === rows.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={rowIndex}
|
||||||
|
style={[
|
||||||
|
styles.tableRow,
|
||||||
|
!isLastRow ? styles.tableBorderBottom : {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<View key={column.key} style={cellStyle}>
|
||||||
|
{cell?.value !== undefined &&
|
||||||
|
cell?.value !== null &&
|
||||||
|
cell?.value !== '' ? (
|
||||||
|
typeof cell.value === 'object' ? (
|
||||||
|
cell.value // For custom React element (badge, etc)
|
||||||
|
) : (
|
||||||
|
<Text>{String(cell.value)}</Text>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Text>-</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<View style={[styles.tableRow, styles.summaryRow]}>
|
||||||
|
{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 (
|
||||||
|
<View key={column.key} style={cellStyle}>
|
||||||
|
<Text>
|
||||||
|
{column.key === 'no' ? label : cellData?.value || ''}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||||
|
{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 (
|
||||||
|
<View key={column.key} style={cellStyle}>
|
||||||
|
<Text>{column.header}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user