Merge branch 'feat/reusable-pdf-component' into 'development'

[FEAT/FE] Reusable PDF Component and Hotfix Penjualan

See merge request mbugroup/lti-web-client!320
This commit is contained in:
Rivaldi A N S
2026-02-10 02:09:59 +00:00
7 changed files with 408 additions and 646 deletions
@@ -0,0 +1,25 @@
import { Text, View, StyleSheet } from '@react-pdf/renderer';
type PdfParamBadgeProps = {
children: React.ReactNode;
};
const styles = StyleSheet.create({
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
});
export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => {
return (
<View style={styles.parameterBadge}>
<Text>{children}</Text>
</View>
);
};
@@ -0,0 +1,47 @@
import { Text, View, StyleSheet } from '@react-pdf/renderer';
type PdfStatusBadgeProps = {
children: React.ReactNode;
backgroundColor?: string;
textColor?: string;
borderColor?: string;
};
const styles = StyleSheet.create({
statusBadge: {
paddingVertical: 2,
paddingHorizontal: 4,
borderRadius: 12,
fontSize: 7,
fontWeight: 'bold',
borderWidth: 1,
borderStyle: 'solid',
},
statusBadgeText: {
fontSize: 7,
fontWeight: 'bold',
},
});
export const PdfStatusBadge = ({
children,
backgroundColor = '#F5F5F5',
textColor = '#333333',
borderColor = '#E5E7EB',
}: PdfStatusBadgeProps) => {
return (
<View
style={[
styles.statusBadge,
{
backgroundColor,
borderColor,
},
]}
>
<Text style={[styles.statusBadgeText, { color: textColor }]}>
{children}
</Text>
</View>
);
};
@@ -0,0 +1,83 @@
import { Color } from '@/types/theme';
import { Text, StyleSheet } from '@react-pdf/renderer';
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
type TypographyVariant = Color | 'default';
type PdfTypographyProps = {
children: React.ReactNode;
size?: TypographySize;
variant?: TypographyVariant;
color?: string;
marginBottom?: number;
};
const styles = StyleSheet.create({
h1: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
},
h2: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
},
h3: {
fontSize: 10,
fontWeight: 'bold',
marginBottom: 4,
},
h4: {
fontSize: 9,
fontWeight: 'bold',
marginBottom: 3,
},
p: {
fontSize: 10,
marginBottom: 4,
},
small: {
fontSize: 8,
marginBottom: 2,
},
label: {
fontSize: 9,
marginBottom: 5,
},
});
const variantColors: Record<TypographyVariant, string> = {
default: '#333333',
primary: '#1f74bf',
secondary: '#6B7280',
accent: '#8B5CF6',
neutral: '#6B7280',
info: '#3B82F6',
success: '#065F46',
warning: '#92400E',
error: '#DC2626',
none: '#333333',
};
export const PdfTypography = ({
children,
size = 'p',
variant = 'default',
color,
marginBottom,
}: PdfTypographyProps) => {
const sizeStyle = styles[size];
const textColor = color || variantColors[variant];
const customStyle = {
...(marginBottom !== undefined && { marginBottom }),
};
return (
<Text style={[sizeStyle, { color: textColor }, customStyle]}>
{children}
</Text>
);
};
@@ -198,6 +198,13 @@ const SalesOrderFormModal = ({
: 'KG' // termasuk "QTY" dan "KG"
: undefined;
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
let marketingTypeValue =
product.marketing_type?.value?.toUpperCase() || '';
if (marketingTypeValue === 'AYAM,AYAM_PULLET') {
marketingTypeValue = product.week ? 'AYAM_PULLET' : 'AYAM';
}
return {
vehicle_number: product.vehicle_number as string,
kandang_id: product.kandang_id as number,
@@ -207,8 +214,7 @@ const SalesOrderFormModal = ({
qty: parseFloat(String(product.qty || 0)),
avg_weight: parseFloat(String(product.avg_weight || 0)),
total_price: parseFloat(String(product.total_price || 0)),
marketing_type:
product.marketing_type?.value?.toUpperCase() || '',
marketing_type: marketingTypeValue,
convertion_unit: normalizedConvertionUnit,
weight_per_convertion:
product.weight_per_convertion ?? undefined,
@@ -2,7 +2,6 @@
import {
Page,
Text,
View,
Document,
StyleSheet,
@@ -18,6 +17,9 @@ import {
PdfTbodyCell,
PdfTfootCell,
} from '@/components/helper/pdf/table';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
Font.register({
family: 'Helvetica',
@@ -34,53 +36,6 @@ const pdfStyles = StyleSheet.create({
titleSection: {
marginBottom: 10,
},
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
color: '#1f74bf',
},
supplierTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
supplierInfo: {
fontSize: 9,
marginBottom: 5,
color: '#333333',
},
badge: {
backgroundColor: '#1f74bf',
color: '#FFFFFF',
padding: 2,
borderRadius: 2,
fontSize: 7,
fontWeight: 'bold',
alignSelf: 'center',
marginRight: 4,
},
badgeLunas: {
backgroundColor: '#1f74bf',
color: '#FFFFFF',
},
badgeBelumLunas: {
backgroundColor: '#F97316',
color: '#FFFFFF',
},
textError: {
color: '#DC2626',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -100,38 +55,6 @@ interface CustomerPaymentExportPDFParams {
};
}
const getParameterText = (
params?: CustomerPaymentExportPDFParams['params']
) => {
const paramsText = [];
if (params?.customer_name) {
paramsText.push(`Customer: ${params.customer_name}`);
} else {
paramsText.push('Semua Customer');
}
// TODO: Uncomment when BE is ready
// if (params?.sales) {
// paramsText.push(`Sales: ${params.sales}`);
// }
if (params?.start_date && params?.end_date) {
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
const endDate = formatDate(params.end_date, 'DD MMM YYYY');
paramsText.push(`Periode: ${startDate} - ${endDate}`);
} else if (params?.start_date) {
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
paramsText.push(`Tanggal: ${startDate}`);
}
const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm');
paramsText.push(`Dicetak: ${currentDate}`);
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' },
@@ -221,15 +144,14 @@ const getTableData = (
{
key: 'status',
value: item.status ? (
<View
style={[
pdfStyles.badge,
item.status === 'LUNAS'
? pdfStyles.badgeLunas
: pdfStyles.badgeBelumLunas,
]}
>
<Text>{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}</Text>
<View style={{ alignItems: 'center' }}>
<PdfStatusBadge
backgroundColor={item.status === 'LUNAS' ? '#DBEAFE' : '#FEE2E2'}
textColor={item.status === 'LUNAS' ? '#1E40AF' : '#991B1B'}
borderColor={item.status === 'LUNAS' ? '#60A5FA' : '#F87171'}
>
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
</PdfStatusBadge>
</View>
) : (
'-'
@@ -302,43 +224,37 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
>
{/* Title and Parameters */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}>
<PdfTypography size='h1' variant='primary'>
Laporan &gt; Kontrol Pembayaran Customer
</Text>
</PdfTypography>
<View style={pdfStyles.parameterContainer}>
<View style={pdfStyles.parameterBadge}>
<Text>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</Text>
</View>
<PdfParamBadge>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</PdfParamBadge>
{/* TODO: Uncomment when BE is ready */}
{/* <View style={pdfStyles.parameterBadge}>
<Text>Filter Tanggal: Tanggal DO</Text>
</View> */}
<View style={pdfStyles.parameterBadge}>
<Text>
Customer: {params.params?.customer_name || 'Semua Customer'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text>
</View>
{/* <PdfParamBadge>
Filter Tanggal: Tanggal DO
</PdfParamBadge> */}
<PdfParamBadge>
Customer: {params.params?.customer_name || 'Semua Customer'}
</PdfParamBadge>
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View>
<Text style={pdfStyles.supplierTitle}>
<PdfTypography size='h2' variant='primary'>
{customerReport.customer.name}
</Text>
<Text style={pdfStyles.supplierInfo}>
</PdfTypography>
<PdfTypography size='label'>
Alamat: {customerReport.customer.address || '-'}
</Text>
</PdfTypography>
</View>
{/* Table */}
@@ -2,7 +2,6 @@
import {
Page,
Text,
View,
Document,
StyleSheet,
@@ -12,6 +11,15 @@ import {
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
import {
PdfTable,
PdfColumn,
PdfTbodyCell,
PdfTfootCell,
} from '@/components/helper/pdf/table';
Font.register({
family: 'Helvetica',
@@ -69,133 +77,6 @@ const pdfStyles = StyleSheet.create({
titleSection: {
marginBottom: 10,
},
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
color: '#1f74bf',
},
supplierTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
supplierInfo: {
fontSize: 9,
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: {
paddingVertical: 2,
paddingHorizontal: 4,
borderRadius: 12,
fontSize: 5,
fontWeight: 'bold',
borderWidth: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -203,6 +84,153 @@ const pdfStyles = StyleSheet.create({
},
});
const getTableColumns = (): PdfColumn[] => [
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
{ key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' },
{ key: 'po_number', header: 'No. PO', flex: 1, align: 'left' },
{
key: 'received_date',
header: 'Tgl Terima/Bayar',
flex: 0.7,
align: 'center',
},
{ key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' },
{ key: 'aging', header: 'Aging', flex: 0.6, align: 'center' },
{ key: 'area', header: 'Area', flex: 1, align: 'left' },
{ key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' },
{ key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' },
{ key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' },
{
key: 'total_price',
header: 'Nominal Pembelian (Rp)',
flex: 1.5,
align: 'right',
},
{
key: 'payment_price',
header: 'Pembayaran (Rp)',
flex: 1.5,
align: 'right',
},
{
key: 'balance',
header: 'Sisa Saldo Hutang (Rp)',
flex: 1.5,
align: 'right',
},
{ key: 'status', header: 'Status', flex: 1.2, align: 'center' },
{ key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' },
];
const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => {
return rows.map((item, index) => [
{ key: 'no', value: index + 1 },
{ key: 'pr_number', value: item.pr_number || '-' },
{ key: 'po_number', value: item.po_number || '-' },
{
key: 'received_date',
value: item.received_date
? formatDate(item.received_date, 'DD MMM YY')
: '-',
},
{
key: 'po_date',
value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-',
},
{
key: 'aging',
value: item.aging != null ? `${formatNumber(item.aging)}` : '-',
},
{ key: 'area', value: item.area?.name || '-' },
{ key: 'warehouse', value: item.warehouse?.name || '-' },
{
key: 'due_date',
value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-',
},
{
key: 'due_status',
value:
item.due_status && item.due_status !== '-' ? (
<PdfStatusBadge
backgroundColor={getPDFBadgeStyle(item.due_status, 'due').bg}
textColor={getPDFBadgeStyle(item.due_status, 'due').text}
borderColor={getPDFBadgeStyle(item.due_status, 'due').border}
>
{item.due_status}
</PdfStatusBadge>
) : (
'-'
),
},
{
key: 'total_price',
value: formatCurrency(item.total_price),
align: 'right',
color: item.total_price < 0 ? 'red' : undefined,
},
{
key: 'payment_price',
value: formatCurrency(item.payment_price),
align: 'right',
color: item.payment_price < 0 ? 'red' : undefined,
},
{
key: 'balance',
value: formatCurrency(item.balance),
align: 'right',
color: item.balance < 0 ? 'red' : undefined,
},
{
key: 'status',
value:
item.status && item.status !== '-' ? (
<View style={{ alignItems: 'center' }}>
<PdfStatusBadge
backgroundColor={getPDFBadgeStyle(item.status, 'payment').bg}
textColor={getPDFBadgeStyle(item.status, 'payment').text}
borderColor={getPDFBadgeStyle(item.status, 'payment').border}
>
{item.status}
</PdfStatusBadge>
</View>
) : (
'-'
),
},
{ key: 'travel_number', value: item.travel_number || '-' },
]);
};
const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [
{ key: 'no', value: 'Total' },
{ key: 'pr_number', value: '' },
{ key: 'po_number', value: '' },
{ key: 'received_date', value: '' },
{ key: 'po_date', value: '' },
{ key: 'aging', value: formatNumber(total?.aging || 0) + ' Hari' },
{ key: 'area', value: '' },
{ key: 'warehouse', value: '' },
{ key: 'due_date', value: '' },
{ key: 'due_status', value: '' },
{
key: 'total_price',
value: formatCurrency(total?.total_price || 0),
align: 'right',
},
{
key: 'payment_price',
value: formatCurrency(total?.payment_price || 0),
align: 'right',
},
{
key: 'balance',
value: formatCurrency(total?.debt_price || 0),
align: 'right',
},
{ key: 'status', value: '' },
{ key: 'travel_number', value: '' },
];
interface DebtSupplierExportPDFParams {
data: DebtSupplier[];
params?: {
@@ -219,418 +247,74 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
{params.data.map((supplierReport, supplierIndex) => (
<Page
key={supplierIndex}
size='A4'
size='A3'
orientation='landscape'
style={pdfStyles.page}
>
{/* Title and Supplier Info */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}>
<PdfTypography size='h1' variant='primary'>
Laporan &gt; Rekapitulasi Hutang ke Supplier
</Text>
</PdfTypography>
<View style={pdfStyles.parameterContainer}>
<View style={pdfStyles.parameterBadge}>
<Text>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</Text>
</View>
<PdfParamBadge>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</PdfParamBadge>
{params.params?.filter_by && (
<View style={pdfStyles.parameterBadge}>
<Text>
Filter Tanggal:{' '}
{params.params.filter_by === 'po_date'
? 'Tanggal PO'
: params.params.filter_by === 'received_date'
? 'Tanggal Terima'
: params.params.filter_by === 'due_date'
? 'Tanggal Jatuh Tempo'
: params.params.filter_by}
</Text>
</View>
<PdfParamBadge>
Filter Tanggal:{' '}
{params.params.filter_by === 'po_date'
? 'Tanggal PO'
: params.params.filter_by === 'received_date'
? 'Tanggal Terima'
: params.params.filter_by === 'due_date'
? 'Tanggal Jatuh Tempo'
: params.params.filter_by}
</PdfParamBadge>
)}
<View style={pdfStyles.parameterBadge}>
<Text>
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text>
</View>
<PdfParamBadge>
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
</PdfParamBadge>
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View>
<Text style={pdfStyles.supplierTitle}>
<PdfTypography size='h2' variant='primary'>
{supplierReport.supplier.name}
</Text>
<Text style={pdfStyles.supplierInfo}>
</PdfTypography>
<PdfTypography size='label'>
{supplierReport.supplier.category}
</Text>
</PdfTypography>
</View>
{/* Table */}
<View style={pdfStyles.table}>
{/* Table Header */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
<Text>No</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PR</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
<Text>Tgl Terima/Bayar</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
<Text>Tgl PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.6 }]}>
<Text>Aging</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Area</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Gudang</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Jatuh Tempo</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
<Text>Status Jatuh Tempo</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Nominal Pembelian (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Pembayaran (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Sisa Saldo Hutang (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Status</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{ flex: 1, borderRight: 'none' },
]}
>
<Text>No. Perjalanan</Text>
</View>
</View>
{/* Initial Balance Row */}
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text></Text> {/* NO */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* No. PR */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* No. PO */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text></Text> {/* Tgl Terima/Bayar */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text></Text> {/* Tgl PO */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text></Text> {/* Aging */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* Area */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* Gudang */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text> {/* Jatuh Tempo */}
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text> {/* Status Jatuh Tempo */}
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> {/* Nominal Pembelian (Rp) */}
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> {/* Pembayaran (Rp) */}
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
<PdfTable
columns={getTableColumns()}
data={getTableData(supplierReport.rows)}
footer={
supplierReport.total
? getTableFooter(supplierReport.total)
: undefined
}
firstRow={
typeof supplierReport.initial_balance === 'number' &&
supplierReport.initial_balance !== 0
? {
valueKey: 'balance',
value: supplierReport.initial_balance,
align: 'right',
color: supplierReport.initial_balance < 0 ? 'red' : 'black',
},
]}
>
<Text>
{' '}
{/* Sisa Saldo Hutang (Rp) */}
{formatCurrency(supplierReport.initial_balance || 0)}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text> {/* Status */}
</View>
<View
style={[
pdfStyles.tableCell, // No. Perjalanan
{ flex: 1, borderRight: 'none' },
]}
>
<Text></Text>
</View>
</View>
{/* Table Body */}
{supplierReport.rows.map((item, index) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < supplierReport.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.pr_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.po_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text>
{item.received_date
? item.received_date != '-'
? formatDate(item.received_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text>
{item.po_date
? item.po_date != '-'
? formatDate(item.po_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text>{formatNumber(item.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.area?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.warehouse?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text>
{item.due_date
? item.due_date != '-'
? formatDate(item.due_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
{item.due_status && item.due_status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.due_status,
'due'
).bg,
borderColor: getPDFBadgeStyle(item.due_status, 'due')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.due_status, 'due').text,
}}
>
{item.due_status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color: item.total_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(item.total_price)}</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color: item.payment_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(item.payment_price)}</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{ flex: 1.5, color: item.balance < 0 ? 'red' : 'black' },
]}
>
<Text>{formatCurrency(item.balance)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
{item.status && item.status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.status,
'payment'
).bg,
borderColor: getPDFBadgeStyle(item.status, 'payment')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.status, 'payment').text,
}}
>
{item.status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View
style={[
pdfStyles.tableCell, // No. Perjalanan
{ flex: 1, borderRight: 'none' },
]}
>
<Text>{item.travel_number || '-'}</Text>
</View>
</View>
))}
{/* Summary Row */}
{supplierReport.total && (
<View style={[pdfStyles.tableRow, pdfStyles.summaryRow]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>Total</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text>{formatNumber(supplierReport.total.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.total_price < 0 ? 'red' : 'black',
},
]}
>
<Text>
{formatCurrency(supplierReport.total.total_price)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.payment_price < 0
? 'red'
: 'black',
},
]}
>
<Text>
{formatCurrency(supplierReport.total.payment_price)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.debt_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
<Text></Text>
</View>
</View>
)}
</View>
}
: undefined
}
/>
</Page>
))}
</Document>
+1
View File
@@ -40,6 +40,7 @@ export const safeRound = (num: number, decimals: number) => {
export const formatTitleCase = (value: string) => {
return value
.toLowerCase()
.replace(/_/g, ' ')
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');