mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 14:55:44 +00:00
Merge branch 'fix/refactor-native-pdf-renderer' into 'development'
[FIX/FE] Refactor Native PDF Renderer to Component Based See merge request mbugroup/lti-web-client!321
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent';
|
||||||
|
|
||||||
const MarketingReportPage = () => {
|
const MarketingReportPage = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ProductionResultContent from '@/components/pages/report/production-result
|
|||||||
|
|
||||||
const ProductionResultReportPage = () => {
|
const ProductionResultReportPage = () => {
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full'>
|
||||||
<ProductionResultContent />
|
<ProductionResultContent />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
type PdfParamBadgeProps = {
|
type PdfParamBadgeProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
style?: Style;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -16,9 +18,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => {
|
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.parameterBadge}>
|
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
||||||
<Text>{children}</Text>
|
<Text>{children}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
type PdfStatusBadgeProps = {
|
type PdfStatusBadgeProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
backgroundColor?: string;
|
style?: Style;
|
||||||
textColor?: string;
|
|
||||||
borderColor?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -16,30 +15,38 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
},
|
},
|
||||||
statusBadgeText: {
|
statusBadgeText: {
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
color: '#333333',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PdfStatusBadge = ({
|
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
|
||||||
children,
|
const styleRecord = style as Record<string, unknown>;
|
||||||
backgroundColor = '#F5F5F5',
|
const color = styleRecord?.color as string | undefined;
|
||||||
textColor = '#333333',
|
|
||||||
borderColor = '#E5E7EB',
|
const viewStyle = Object.entries(styleRecord || {}).reduce(
|
||||||
}: PdfStatusBadgeProps) => {
|
(acc, [key, value]) => {
|
||||||
|
if (key !== 'color') {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, unknown>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.statusBadge,
|
styles.statusBadge,
|
||||||
{
|
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
||||||
backgroundColor,
|
|
||||||
borderColor,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.statusBadgeText, { color: textColor }]}>
|
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
|
type PdfPageNumberProps = {
|
||||||
|
style?: Style;
|
||||||
|
/**
|
||||||
|
* Format template for page number.
|
||||||
|
* Use {pageNumber} and {totalPages} as placeholders.
|
||||||
|
* Default: "{pageNumber} / {totalPages}"
|
||||||
|
*/
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
position: 'absolute',
|
||||||
|
fontSize: 8,
|
||||||
|
bottom: 30,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PdfPageNumber = ({
|
||||||
|
style,
|
||||||
|
format = '{pageNumber} / {totalPages}',
|
||||||
|
}: PdfPageNumberProps) => {
|
||||||
|
return (
|
||||||
|
<View style={style || styles.footer} fixed>
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
format
|
||||||
|
.replace('{pageNumber}', String(pageNumber))
|
||||||
|
.replace('{totalPages}', String(totalPages))
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Text>-</Text>
|
cellContent
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { PdfColumn };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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';
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
import { Text, StyleSheet } from '@react-pdf/renderer';
|
import { Text, StyleSheet } from '@react-pdf/renderer';
|
||||||
|
import type { Style } from '@react-pdf/types';
|
||||||
|
|
||||||
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ type PdfTypographyProps = {
|
|||||||
size?: TypographySize;
|
size?: TypographySize;
|
||||||
variant?: TypographyVariant;
|
variant?: TypographyVariant;
|
||||||
color?: string;
|
color?: string;
|
||||||
marginBottom?: number;
|
style?: Style;
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
@@ -66,17 +67,13 @@ export const PdfTypography = ({
|
|||||||
size = 'p',
|
size = 'p',
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
color,
|
color,
|
||||||
marginBottom,
|
style,
|
||||||
}: PdfTypographyProps) => {
|
}: PdfTypographyProps) => {
|
||||||
const sizeStyle = styles[size];
|
const sizeStyle = styles[size];
|
||||||
const textColor = color || variantColors[variant];
|
const textColor = color || variantColors[variant];
|
||||||
|
|
||||||
const customStyle = {
|
|
||||||
...(marginBottom !== undefined && { marginBottom }),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text style={[sizeStyle, { color: textColor }, customStyle]}>
|
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
export type StatusColor = {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Due status colors (for debt supplier reports)
|
||||||
|
export const dueStatusColors: Record<string, StatusColor> = {
|
||||||
|
'SUDAH JATUH TEMPO': {
|
||||||
|
bg: '#FEE2E2',
|
||||||
|
text: '#991B1B',
|
||||||
|
border: '#F87171',
|
||||||
|
}, // error/red
|
||||||
|
'BELUM JATUH TEMPO': {
|
||||||
|
bg: '#D1FAE5',
|
||||||
|
text: '#065F46',
|
||||||
|
border: '#34D399',
|
||||||
|
}, // success/green
|
||||||
|
'MENDEKATI JATUH TEMPO': {
|
||||||
|
bg: '#FEF3C7',
|
||||||
|
text: '#92400E',
|
||||||
|
border: '#FBBF24',
|
||||||
|
}, // warning/yellow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Payment status colors (for customer payment & debt supplier reports)
|
||||||
|
export const paymentStatusColors: Record<string, StatusColor> = {
|
||||||
|
'BELUM LUNAS': {
|
||||||
|
bg: '#FEF3C7',
|
||||||
|
text: '#92400E',
|
||||||
|
border: '#FBBF24',
|
||||||
|
}, // warning/yellow
|
||||||
|
LUNAS: {
|
||||||
|
bg: '#DBEAFE',
|
||||||
|
text: '#1E40AF',
|
||||||
|
border: '#60A5FA',
|
||||||
|
}, // primary/blue
|
||||||
|
'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
||||||
|
PEMBAYARAN: {
|
||||||
|
bg: '#D1FAE5',
|
||||||
|
text: '#065F46',
|
||||||
|
border: '#34D399',
|
||||||
|
}, // success/green
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fallback color for unknown statuses
|
||||||
|
export const fallbackStatusColor: StatusColor = {
|
||||||
|
bg: '#F3F4F6',
|
||||||
|
text: '#374151',
|
||||||
|
border: '#D1D5DB',
|
||||||
|
}; // neutral
|
||||||
|
|
||||||
|
export const getPDFBadgeStyle = (
|
||||||
|
statusText: string,
|
||||||
|
type: 'due' | 'payment' = 'payment'
|
||||||
|
): StatusColor => {
|
||||||
|
const normalizedStatus = statusText.toUpperCase().trim();
|
||||||
|
|
||||||
|
const colors =
|
||||||
|
type === 'due'
|
||||||
|
? dueStatusColors[normalizedStatus]
|
||||||
|
: paymentStatusColors[normalizedStatus];
|
||||||
|
|
||||||
|
return colors || fallbackStatusColor;
|
||||||
|
};
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Document,
|
|
||||||
Image,
|
|
||||||
Page,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DailyMarketingReport,
|
|
||||||
SalesSummary,
|
|
||||||
} from '@/types/api/report/marketing';
|
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
|
|
||||||
interface DailyMarketingReportPDFProps {
|
|
||||||
data?: DailyMarketingReport;
|
|
||||||
total?: SalesSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DailyMarketingReportPDFStyle = StyleSheet.create({
|
|
||||||
page: {
|
|
||||||
paddingTop: 24,
|
|
||||||
paddingBottom: 64,
|
|
||||||
paddingHorizontal: 16, // Reduce padding to fit more columns
|
|
||||||
orientation: 'landscape',
|
|
||||||
},
|
|
||||||
|
|
||||||
companyInfoHeader: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
companyLogo: {
|
|
||||||
width: 64,
|
|
||||||
height: 'auto',
|
|
||||||
},
|
|
||||||
companyInfoHeaderDate: {
|
|
||||||
paddingTop: 8,
|
|
||||||
fontSize: 10,
|
|
||||||
},
|
|
||||||
companyName: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
companyAddress: {
|
|
||||||
fontSize: 8,
|
|
||||||
maxWidth: 400,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
marginTop: 16,
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: '150%',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: 'Times-Roman',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
footer: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
|
|
||||||
position: 'absolute',
|
|
||||||
fontSize: 8,
|
|
||||||
bottom: 30,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'grey',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Table Styles
|
|
||||||
table: {
|
|
||||||
width: '100%',
|
|
||||||
marginTop: 16,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000000',
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
fontSize: 7, // Smaller font for report
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#000000',
|
|
||||||
alignItems: 'center',
|
|
||||||
minHeight: 20,
|
|
||||||
},
|
|
||||||
tableHeader: {
|
|
||||||
backgroundColor: '#f0f0f0',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Columns definition (Total 100%)
|
|
||||||
colNo: {
|
|
||||||
width: '3%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'center',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colSoDate: {
|
|
||||||
width: '6%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colDoDate: {
|
|
||||||
width: '6%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colAging: {
|
|
||||||
width: '3%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'center',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colWarehouse: {
|
|
||||||
width: '7%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colCustomer: {
|
|
||||||
width: '9%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
}, // Reduced slightly
|
|
||||||
colSales: {
|
|
||||||
width: '6%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colProduct: {
|
|
||||||
width: '8%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
}, // Reduced slightly
|
|
||||||
colDoNumber: {
|
|
||||||
width: '7%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colVehicle: {
|
|
||||||
width: '5%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colMarketingType: {
|
|
||||||
width: '5%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'left',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colQty: {
|
|
||||||
width: '4%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colAvgWeight: {
|
|
||||||
width: '4%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colTotalWeight: {
|
|
||||||
width: '5%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colSalesPrice: {
|
|
||||||
width: '5%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colHppPrice: {
|
|
||||||
width: '5%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colSalesAmount: {
|
|
||||||
width: '6%',
|
|
||||||
padding: 2,
|
|
||||||
textAlign: 'right',
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000000',
|
|
||||||
},
|
|
||||||
colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column
|
|
||||||
|
|
||||||
// Text inside columns
|
|
||||||
cellText: {
|
|
||||||
fontSize: 6,
|
|
||||||
},
|
|
||||||
headerText: {
|
|
||||||
fontSize: 7,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
doubleDivider: {
|
|
||||||
width: '100%',
|
|
||||||
height: 6,
|
|
||||||
borderTop: '2px solid black',
|
|
||||||
borderBottom: '2px solid black',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
summaryContainer: {
|
|
||||||
marginTop: 12,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
summaryTable: {
|
|
||||||
width: '30%',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000000',
|
|
||||||
fontSize: 8,
|
|
||||||
},
|
|
||||||
summaryRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
padding: 2,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#eee',
|
|
||||||
},
|
|
||||||
summaryLabel: {
|
|
||||||
width: '50%',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
summaryValue: {
|
|
||||||
width: '50%',
|
|
||||||
textAlign: 'right',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const DailyMarketingReportPDF = ({
|
|
||||||
data,
|
|
||||||
total,
|
|
||||||
}: DailyMarketingReportPDFProps) => {
|
|
||||||
const rows = data || [];
|
|
||||||
const summary = total;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Document>
|
|
||||||
<Page
|
|
||||||
style={DailyMarketingReportPDFStyle.page}
|
|
||||||
orientation='landscape'
|
|
||||||
size='A4'
|
|
||||||
>
|
|
||||||
<View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}>
|
|
||||||
<Image
|
|
||||||
style={DailyMarketingReportPDFStyle.companyLogo}
|
|
||||||
src='/assets/img/lti-logo.png'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}>
|
|
||||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.companyName}>
|
|
||||||
PT LUMBUNG TELUR INDONESIA
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.companyAddress}>
|
|
||||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
|
||||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={DailyMarketingReportPDFStyle.doubleDivider} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.title}>
|
|
||||||
Laporan Penjualan Harian
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{/* Data Table */}
|
|
||||||
<View style={DailyMarketingReportPDFStyle.table}>
|
|
||||||
{/* Header */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
DailyMarketingReportPDFStyle.tableRow,
|
|
||||||
DailyMarketingReportPDFStyle.tableHeader,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Tgl SO
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Tgl DO
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Aging</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Gudang
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Pelanggan
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Sales</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Produk
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>No DO</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Plat No
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Tipe</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Qty</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Rerata
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>Berat</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Hrg Jual
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
HPP/kg
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Total Jual
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
|
||||||
Total HPP
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Rows */}
|
|
||||||
{rows.map((row, index) => (
|
|
||||||
<View style={DailyMarketingReportPDFStyle.tableRow} key={index}>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colNo}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{index + 1}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatDate(row.so_date, 'DD/MM/YYYY')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatDate(row.realization_date, 'DD/MM/YYYY')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colAging}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.aging_days}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.warehouse?.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.customer?.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSales}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.sales.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.product?.name}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.do_number}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.vehicle_number}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{row.marketing_type}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colQty}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatNumber(row.qty)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatNumber(row.average_weight_kg)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatNumber(row.total_weight_kg)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatCurrency(row.sales_price_per_kg)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatCurrency(row.hpp_price_per_kg)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatCurrency(row.sales_amount)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
|
||||||
{formatCurrency(row.hpp_amount)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Summary */}
|
|
||||||
<View style={DailyMarketingReportPDFStyle.summaryContainer}>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.summaryTable}>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
|
||||||
Total Qty:
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
|
||||||
{formatNumber(summary?.total_qty ?? 0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
|
||||||
Total Berat (kg):
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
|
||||||
{formatNumber(summary?.total_weight_kg ?? 0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
|
||||||
Total Penjualan:
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
|
||||||
{formatCurrency(summary?.total_sales_amount ?? 0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
DailyMarketingReportPDFStyle.summaryRow,
|
|
||||||
{ borderBottomWidth: 0 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
|
||||||
Total HPP Per KG:
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
|
||||||
{formatCurrency(summary?.total_hpp_price_per_kg ?? 0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
DailyMarketingReportPDFStyle.summaryRow,
|
|
||||||
{ borderBottomWidth: 0 },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
|
||||||
Total HPP:
|
|
||||||
</Text>
|
|
||||||
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
|
||||||
{formatCurrency(summary?.total_hpp_amount ?? 0)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={DailyMarketingReportPDFStyle.footer} fixed>
|
|
||||||
<Text
|
|
||||||
render={({ pageNumber, totalPages }) =>
|
|
||||||
`${pageNumber} / ${totalPages}`
|
|
||||||
}
|
|
||||||
fixed
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
</Document>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DailyMarketingReportPDF;
|
|
||||||
@@ -7,19 +7,21 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Font,
|
Font,
|
||||||
pdf,
|
pdf,
|
||||||
|
Text,
|
||||||
} from '@react-pdf/renderer';
|
} from '@react-pdf/renderer';
|
||||||
|
|
||||||
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
|
||||||
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
|
||||||
import {
|
import {
|
||||||
PdfTable,
|
formatDate,
|
||||||
PdfColumn,
|
formatCurrency,
|
||||||
PdfTbodyCell,
|
formatNumber,
|
||||||
PdfTfootCell,
|
formatTitleCase,
|
||||||
} from '@/components/helper/pdf/table';
|
} from '@/lib/helper';
|
||||||
|
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
||||||
|
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||||
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||||
|
import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge';
|
||||||
|
|
||||||
Font.register({
|
Font.register({
|
||||||
family: 'Helvetica',
|
family: 'Helvetica',
|
||||||
@@ -55,161 +57,183 @@ interface CustomerPaymentExportPDFParams {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTableColumns = (): PdfColumn[] => [
|
const getTableColumns = (
|
||||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
summary?: CustomerPaymentReport['summary']
|
||||||
{ key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' },
|
): PdfColumn<CustomerPaymentReport['rows'][number]>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
footer: 'Total',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'trans_date',
|
||||||
|
header: 'Tanggal DO',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.trans_date ? formatDate(row.trans_date, 'DD MMM YY') : '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'delivery_date',
|
key: 'delivery_date',
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
flex: 1.2,
|
flex: 1.2,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.delivery_date ? formatDate(row.delivery_date, 'DD MMM YY') : '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{ key: 'aging', header: 'Aging', flex: 0.8, align: 'center' },
|
{
|
||||||
{ key: 'reference', header: 'Referensi', flex: 1.5, align: 'left' },
|
key: 'aging',
|
||||||
{ key: 'vehicle_numbers', header: 'No Polisi', flex: 1.2, align: 'left' },
|
header: 'Aging',
|
||||||
{ key: 'qty', header: 'Qty', flex: 0.8, align: 'right' },
|
flex: 0.8,
|
||||||
{ key: 'weight', header: 'Berat', flex: 1, align: 'right' },
|
align: 'center',
|
||||||
{ key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' },
|
cell: ({ row }) =>
|
||||||
{ key: 'unit_price', header: 'Harga/Unit', flex: 1.2, align: 'right' },
|
row.aging_day != null ? `${formatNumber(row.aging_day)} hari` : '-',
|
||||||
{ key: 'final_price', header: 'Harga Akhir', flex: 1.2, align: 'right' },
|
footer: '',
|
||||||
{ key: 'total_price', header: 'Total', flex: 1.2, align: 'right' },
|
},
|
||||||
{ key: 'payment_amount', header: 'Pembayaran', flex: 1.2, align: 'right' },
|
{
|
||||||
{ key: 'accounts_receivable', header: 'Saldo', flex: 1.2, align: 'right' },
|
key: 'reference',
|
||||||
{ key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' },
|
header: 'Referensi',
|
||||||
{ key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' },
|
flex: 1.5,
|
||||||
{ key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' },
|
align: 'left',
|
||||||
];
|
cell: ({ row }) => row.reference || '-',
|
||||||
|
footer: '',
|
||||||
const getTableData = (
|
},
|
||||||
rows: CustomerPaymentReport['rows']
|
{
|
||||||
): PdfTbodyCell[][] => {
|
key: 'vehicle_numbers',
|
||||||
return rows.map((item, index) => [
|
header: 'No Polisi',
|
||||||
{ key: 'no', value: index + 1 },
|
flex: 1.2,
|
||||||
{
|
align: 'left',
|
||||||
key: 'trans_date',
|
cell: ({ row }) =>
|
||||||
value: item.trans_date ? formatDate(item.trans_date, 'DD MMM YY') : '-',
|
Array.isArray(row.vehicle_numbers) && row.vehicle_numbers.length > 0
|
||||||
},
|
? row.vehicle_numbers.join(', ')
|
||||||
{
|
|
||||||
key: 'delivery_date',
|
|
||||||
value: item.delivery_date
|
|
||||||
? formatDate(item.delivery_date, 'DD MMM YY')
|
|
||||||
: '-',
|
: '-',
|
||||||
},
|
footer: '',
|
||||||
{
|
},
|
||||||
key: 'aging',
|
{
|
||||||
value:
|
key: 'qty',
|
||||||
item.aging_day != null ? `${formatNumber(item.aging_day)} hari` : '-',
|
header: 'Qty',
|
||||||
},
|
flex: 0.8,
|
||||||
{ key: 'reference', value: item.reference || '-' },
|
align: 'right',
|
||||||
{
|
cell: ({ row }) => formatNumber(row.qty),
|
||||||
key: 'vehicle_numbers',
|
footer: summary ? formatNumber(summary.total_qty || 0) : '',
|
||||||
value:
|
footerAlign: 'right',
|
||||||
Array.isArray(item.vehicle_numbers) && item.vehicle_numbers.length > 0
|
},
|
||||||
? item.vehicle_numbers.join(', ')
|
{
|
||||||
: '-',
|
key: 'weight',
|
||||||
},
|
header: 'Berat',
|
||||||
{ key: 'qty', value: formatNumber(item.qty), align: 'right' },
|
flex: 1,
|
||||||
{ key: 'weight', value: formatNumber(item.weight), align: 'right' },
|
align: 'right',
|
||||||
{
|
cell: ({ row }) => formatNumber(row.weight),
|
||||||
key: 'average_weight',
|
footer: summary ? formatNumber(summary.total_weight || 0) : '',
|
||||||
value: formatNumber(item.average_weight),
|
footerAlign: 'right',
|
||||||
align: 'right',
|
},
|
||||||
},
|
{
|
||||||
{
|
key: 'average_weight',
|
||||||
key: 'unit_price',
|
header: 'Rata-Rata',
|
||||||
value: formatCurrency(item.unit_price),
|
flex: 0.8,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
},
|
cell: ({ row }) => formatNumber(row.average_weight),
|
||||||
{
|
footer: '',
|
||||||
key: 'final_price',
|
},
|
||||||
value: formatCurrency(item.final_price),
|
{
|
||||||
align: 'right',
|
key: 'unit_price',
|
||||||
},
|
header: 'Harga/Unit (Rp)',
|
||||||
{
|
flex: 1.2,
|
||||||
key: 'total_price',
|
align: 'right',
|
||||||
value: formatCurrency(item.total_price),
|
cell: ({ row }) => formatCurrency(row.unit_price),
|
||||||
align: 'right',
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'payment_amount',
|
key: 'final_price',
|
||||||
value: formatCurrency(item.payment_amount),
|
header: 'Harga Akhir (Rp)',
|
||||||
align: 'right',
|
flex: 1.2,
|
||||||
},
|
align: 'right',
|
||||||
{
|
cell: ({ row }) => formatCurrency(row.final_price),
|
||||||
key: 'accounts_receivable',
|
footer: summary ? formatCurrency(summary.total_final_amount || 0) : '',
|
||||||
value: formatCurrency(item.accounts_receivable),
|
footerAlign: 'right',
|
||||||
align: 'right',
|
},
|
||||||
color: item.accounts_receivable < 0 ? '#DC2626' : undefined,
|
{
|
||||||
},
|
key: 'total_price',
|
||||||
{
|
header: 'Total (Rp)',
|
||||||
key: 'status',
|
flex: 1.2,
|
||||||
value: item.status ? (
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.total_price),
|
||||||
|
footer: summary ? formatCurrency(summary.total_grand_amount || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'payment_amount',
|
||||||
|
header: 'Pembayaran (Rp)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.payment_amount),
|
||||||
|
footer: summary ? formatCurrency(summary.total_payment || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'accounts_receivable',
|
||||||
|
header: 'Saldo (Rp)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text style={{ color: row.accounts_receivable < 0 ? 'red' : 'black' }}>
|
||||||
|
{formatCurrency(row.accounts_receivable)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
footer: summary
|
||||||
|
? formatCurrency(summary.total_accounts_receivable || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
footerColor:
|
||||||
|
(summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
header: 'Keterangan',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.status ? (
|
||||||
<View style={{ alignItems: 'center' }}>
|
<View style={{ alignItems: 'center' }}>
|
||||||
<PdfStatusBadge
|
<PdfStatusBadge
|
||||||
backgroundColor={item.status === 'LUNAS' ? '#DBEAFE' : '#FEE2E2'}
|
style={{
|
||||||
textColor={item.status === 'LUNAS' ? '#1E40AF' : '#991B1B'}
|
backgroundColor: getPDFBadgeStyle(row.status, 'payment').bg,
|
||||||
borderColor={item.status === 'LUNAS' ? '#60A5FA' : '#F87171'}
|
color: getPDFBadgeStyle(row.status, 'payment').text,
|
||||||
|
borderColor: getPDFBadgeStyle(row.status, 'payment').border,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
|
{formatTitleCase(row.status)}
|
||||||
</PdfStatusBadge>
|
</PdfStatusBadge>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
'-'
|
'-'
|
||||||
),
|
),
|
||||||
},
|
footer: '',
|
||||||
{
|
|
||||||
key: 'pickup_info',
|
|
||||||
value:
|
|
||||||
Array.isArray(item.pickup_info) && item.pickup_info.length > 0
|
|
||||||
? item.pickup_info.join(', ')
|
|
||||||
: '-',
|
|
||||||
},
|
|
||||||
{ key: 'sales_person', value: item.sales_person || '-' },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTableFooter = (
|
|
||||||
summary: CustomerPaymentReport['summary']
|
|
||||||
): PdfTfootCell[] => [
|
|
||||||
{ key: 'no', value: 'Total' },
|
|
||||||
{ key: 'trans_date', value: '' },
|
|
||||||
{ key: 'delivery_date', value: '' },
|
|
||||||
{ key: 'aging', value: '' },
|
|
||||||
{ key: 'reference', value: '' },
|
|
||||||
{ key: 'vehicle_numbers', value: '' },
|
|
||||||
{ key: 'qty', value: formatNumber(summary?.total_qty || 0), align: 'right' },
|
|
||||||
{
|
|
||||||
key: 'weight',
|
|
||||||
value: formatNumber(summary?.total_weight || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'average_weight', value: '' },
|
|
||||||
{ key: 'unit_price', value: '' },
|
|
||||||
{
|
|
||||||
key: 'final_price',
|
|
||||||
value: formatCurrency(summary?.total_final_amount || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'total_price',
|
key: 'pickup_info',
|
||||||
value: formatCurrency(summary?.total_grand_amount || 0),
|
header: 'Pengambilan',
|
||||||
align: 'right',
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
Array.isArray(row.pickup_info) && row.pickup_info.length > 0
|
||||||
|
? row.pickup_info.join(', ')
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'payment_amount',
|
key: 'sales_person',
|
||||||
value: formatCurrency(summary?.total_payment || 0),
|
header: 'Sales',
|
||||||
align: 'right',
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.sales_person || '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'accounts_receivable',
|
|
||||||
value: formatCurrency(summary?.total_accounts_receivable || 0),
|
|
||||||
align: 'right',
|
|
||||||
color:
|
|
||||||
(summary?.total_accounts_receivable || 0) < 0 ? '#DC2626' : undefined,
|
|
||||||
},
|
|
||||||
{ key: 'status', value: '' },
|
|
||||||
{ key: 'pickup_info', value: '' },
|
|
||||||
{ key: 'sales_person', value: '' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||||
@@ -259,13 +283,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<PdfTable
|
<PdfTable
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns(customerReport.summary)}
|
||||||
data={getTableData(customerReport.rows)}
|
data={customerReport.rows}
|
||||||
footer={
|
showFooter={!!customerReport.summary}
|
||||||
customerReport.summary
|
|
||||||
? getTableFooter(customerReport.summary)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
firstRow={
|
firstRow={
|
||||||
typeof customerReport.initial_balance === 'number' &&
|
typeof customerReport.initial_balance === 'number' &&
|
||||||
customerReport.initial_balance !== 0
|
customerReport.initial_balance !== 0
|
||||||
@@ -273,8 +293,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
valueKey: 'accounts_receivable',
|
valueKey: 'accounts_receivable',
|
||||||
value: customerReport.initial_balance,
|
value: customerReport.initial_balance,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
color:
|
color: customerReport.initial_balance < 0 ? 'red' : 'black',
|
||||||
customerReport.initial_balance < 0 ? '#DC2626' : 'black',
|
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ export const generateCustomerPaymentExcel = async (
|
|||||||
{ header: 'Ekor/Qty', key: 'qty', width: 10 },
|
{ header: 'Ekor/Qty', key: 'qty', width: 10 },
|
||||||
{ header: 'Berat (Kg)', key: 'weight', width: 12 },
|
{ header: 'Berat (Kg)', key: 'weight', width: 12 },
|
||||||
{ header: 'AVG', key: 'avgWeight', width: 10 },
|
{ header: 'AVG', key: 'avgWeight', width: 10 },
|
||||||
{ header: 'Harga/Unit', key: 'unitPrice', width: 15 },
|
{ header: 'Harga/Unit (Rp)', key: 'unitPrice', width: 15 },
|
||||||
{ header: 'Harga Akhir', key: 'finalPrice', width: 15 },
|
{ header: 'Harga Akhir (Rp)', key: 'finalPrice', width: 15 },
|
||||||
{ header: 'Total', key: 'totalPrice', width: 15 },
|
{ header: 'Total (Rp)', key: 'totalPrice', width: 15 },
|
||||||
{ header: 'Pembayaran', key: 'paymentAmount', width: 15 },
|
{ header: 'Pembayaran (Rp)', key: 'paymentAmount', width: 15 },
|
||||||
{ header: 'Saldo Piutang', key: 'accountsReceivable', width: 15 },
|
{ header: 'Saldo Piutang (Rp)', key: 'accountsReceivable', width: 15 },
|
||||||
{ header: 'Keterangan', key: 'status', width: 20 },
|
{ header: 'Keterangan', key: 'status', width: 20 },
|
||||||
{ header: 'Pengambilan', key: 'pickupInfo', width: 15 },
|
{ header: 'Pengambilan', key: 'pickupInfo', width: 15 },
|
||||||
{ header: 'Sales/Marketing', key: 'salesPerson', width: 20 },
|
{ header: 'Sales/Marketing', key: 'salesPerson', width: 20 },
|
||||||
|
|||||||
@@ -14,59 +14,15 @@ import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
|||||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||||
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||||
import {
|
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||||
PdfTable,
|
import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge';
|
||||||
PdfColumn,
|
import { Text } from '@react-pdf/renderer';
|
||||||
PdfTbodyCell,
|
|
||||||
PdfTfootCell,
|
|
||||||
} from '@/components/helper/pdf/table';
|
|
||||||
|
|
||||||
Font.register({
|
Font.register({
|
||||||
family: 'Helvetica',
|
family: 'Helvetica',
|
||||||
src: 'helvetica',
|
src: 'helvetica',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status color mappings (same as in DebtSupplierTab)
|
|
||||||
const dueStatusColors: Record<
|
|
||||||
string,
|
|
||||||
{ bg: string; text: string; border: string }
|
|
||||||
> = {
|
|
||||||
'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red
|
|
||||||
'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
|
||||||
'Mendekati Jatuh Tempo': {
|
|
||||||
bg: '#FEF3C7',
|
|
||||||
text: '#92400E',
|
|
||||||
border: '#FBBF24',
|
|
||||||
}, // warning/yellow
|
|
||||||
};
|
|
||||||
|
|
||||||
const paymentStatusColors: Record<
|
|
||||||
string,
|
|
||||||
{ bg: string; text: string; border: string }
|
|
||||||
> = {
|
|
||||||
'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow
|
|
||||||
Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue
|
|
||||||
Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get badge style for PDF rendering
|
|
||||||
* @param statusText - The status text
|
|
||||||
* @param type - Type of status: 'due' or 'payment'
|
|
||||||
* @returns Style object with background and text colors
|
|
||||||
*/
|
|
||||||
const getPDFBadgeStyle = (
|
|
||||||
statusText: string,
|
|
||||||
type: 'due' | 'payment' = 'payment'
|
|
||||||
) => {
|
|
||||||
const colors =
|
|
||||||
type === 'due'
|
|
||||||
? dueStatusColors[statusText]
|
|
||||||
: paymentStatusColors[statusText];
|
|
||||||
|
|
||||||
return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback
|
|
||||||
};
|
|
||||||
|
|
||||||
const pdfStyles = StyleSheet.create({
|
const pdfStyles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -84,153 +40,225 @@ const pdfStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getTableColumns = (): PdfColumn[] => [
|
const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => {
|
||||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
type DebtRow = DebtSupplier['rows'][number];
|
||||||
{ 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 [
|
||||||
return rows.map((item, index) => [
|
{
|
||||||
{ key: 'no', value: index + 1 },
|
key: 'no',
|
||||||
{ key: 'pr_number', value: item.pr_number || '-' },
|
header: 'No',
|
||||||
{ key: 'po_number', value: item.po_number || '-' },
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
footer: 'Total',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pr_number',
|
||||||
|
header: 'No. PR',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => (row as unknown as DebtRow).pr_number || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'po_number',
|
||||||
|
header: 'No. PO',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => (row as unknown as DebtRow).po_number || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'received_date',
|
key: 'received_date',
|
||||||
value: item.received_date
|
header: 'Tgl Terima/Bayar',
|
||||||
? formatDate(item.received_date, 'DD MMM YY')
|
flex: 0.7,
|
||||||
: '-',
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
(row as unknown as DebtRow).received_date
|
||||||
|
? formatDate((row as unknown as DebtRow).received_date, 'DD MMM YY')
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'po_date',
|
key: 'po_date',
|
||||||
value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-',
|
header: 'Tgl PO',
|
||||||
|
flex: 0.7,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
(row as unknown as DebtRow).po_date
|
||||||
|
? formatDate((row as unknown as DebtRow).po_date, 'DD MMM YY')
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'aging',
|
key: 'aging',
|
||||||
value: item.aging != null ? `${formatNumber(item.aging)}` : '-',
|
header: 'Aging',
|
||||||
|
flex: 0.6,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
(row as unknown as DebtRow).aging != null
|
||||||
|
? `${formatNumber((row as unknown as DebtRow).aging)}`
|
||||||
|
: '-',
|
||||||
|
footer: total ? formatNumber(total.aging || 0) + ' Hari' : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'area',
|
||||||
|
header: 'Area',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => (row as unknown as DebtRow).area?.name || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'warehouse',
|
||||||
|
header: 'Gudang',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => (row as unknown as DebtRow).warehouse?.name || '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{ key: 'area', value: item.area?.name || '-' },
|
|
||||||
{ key: 'warehouse', value: item.warehouse?.name || '-' },
|
|
||||||
{
|
{
|
||||||
key: 'due_date',
|
key: 'due_date',
|
||||||
value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-',
|
header: 'Jatuh Tempo',
|
||||||
|
flex: 1,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
(row as unknown as DebtRow).due_date
|
||||||
|
? formatDate((row as unknown as DebtRow).due_date, 'DD MMM YY')
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'due_status',
|
key: 'due_status',
|
||||||
value:
|
header: 'Status Jatuh Tempo',
|
||||||
item.due_status && item.due_status !== '-' ? (
|
flex: 2,
|
||||||
<PdfStatusBadge
|
align: 'center',
|
||||||
backgroundColor={getPDFBadgeStyle(item.due_status, 'due').bg}
|
cell: ({ row }) =>
|
||||||
textColor={getPDFBadgeStyle(item.due_status, 'due').text}
|
(row as unknown as DebtRow).due_status &&
|
||||||
borderColor={getPDFBadgeStyle(item.due_status, 'due').border}
|
(row as unknown as DebtRow).due_status !== '-' ? (
|
||||||
>
|
|
||||||
{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' }}>
|
<View style={{ alignItems: 'center' }}>
|
||||||
<PdfStatusBadge
|
<PdfStatusBadge
|
||||||
backgroundColor={getPDFBadgeStyle(item.status, 'payment').bg}
|
style={{
|
||||||
textColor={getPDFBadgeStyle(item.status, 'payment').text}
|
backgroundColor: getPDFBadgeStyle(
|
||||||
borderColor={getPDFBadgeStyle(item.status, 'payment').border}
|
(row as unknown as DebtRow).due_status,
|
||||||
|
'due'
|
||||||
|
).bg,
|
||||||
|
color: getPDFBadgeStyle(
|
||||||
|
(row as unknown as DebtRow).due_status,
|
||||||
|
'due'
|
||||||
|
).text,
|
||||||
|
borderColor: getPDFBadgeStyle(
|
||||||
|
(row as unknown as DebtRow).due_status,
|
||||||
|
'due'
|
||||||
|
).border,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.status}
|
{(row as unknown as DebtRow).due_status}
|
||||||
</PdfStatusBadge>
|
</PdfStatusBadge>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
'-'
|
'-'
|
||||||
),
|
),
|
||||||
|
footer: '',
|
||||||
},
|
},
|
||||||
{ key: 'travel_number', value: item.travel_number || '-' },
|
{
|
||||||
]);
|
key: 'total_price',
|
||||||
|
header: 'Nominal Pembelian (Rp)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
(row as unknown as DebtRow).total_price < 0 ? 'red' : 'black',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency((row as unknown as DebtRow).total_price)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
footer: total ? formatCurrency(total.total_price || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'payment_price',
|
||||||
|
header: 'Pembayaran (Rp)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
(row as unknown as DebtRow).payment_price < 0 ? 'red' : 'black',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency((row as unknown as DebtRow).payment_price)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
footer: total ? formatCurrency(total.payment_price || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'balance',
|
||||||
|
header: 'Sisa Saldo Hutang (Rp)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: (row as unknown as DebtRow).balance < 0 ? 'red' : 'black',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency((row as unknown as DebtRow).balance)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
footer: total ? formatCurrency(total.debt_price || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
footerColor: (total?.debt_price || 0) < 0 ? 'red' : undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
(row as unknown as DebtRow).status &&
|
||||||
|
(row as unknown as DebtRow).status !== '-' ? (
|
||||||
|
<View style={{ alignItems: 'center' }}>
|
||||||
|
<PdfStatusBadge
|
||||||
|
style={{
|
||||||
|
backgroundColor: getPDFBadgeStyle(
|
||||||
|
(row as unknown as DebtRow).status,
|
||||||
|
'payment'
|
||||||
|
).bg,
|
||||||
|
color: getPDFBadgeStyle(
|
||||||
|
(row as unknown as DebtRow).status,
|
||||||
|
'payment'
|
||||||
|
).text,
|
||||||
|
borderColor: getPDFBadgeStyle(
|
||||||
|
(row as unknown as DebtRow).status,
|
||||||
|
'payment'
|
||||||
|
).border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(row as unknown as DebtRow).status}
|
||||||
|
</PdfStatusBadge>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'travel_number',
|
||||||
|
header: 'No. Perjalanan',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => (row as unknown as DebtRow).travel_number || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
interface DebtSupplierExportPDFParams {
|
||||||
data: DebtSupplier[];
|
data: DebtSupplier[];
|
||||||
params?: {
|
params?: {
|
||||||
@@ -296,13 +324,9 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<PdfTable
|
<PdfTable
|
||||||
columns={getTableColumns()}
|
columns={getTableColumns(supplierReport.total)}
|
||||||
data={getTableData(supplierReport.rows)}
|
data={supplierReport.rows as unknown as Record<string, unknown>[]}
|
||||||
footer={
|
showFooter={!!supplierReport.total}
|
||||||
supplierReport.total
|
|
||||||
? getTableFooter(supplierReport.total)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
firstRow={
|
firstRow={
|
||||||
typeof supplierReport.initial_balance === 'number' &&
|
typeof supplierReport.initial_balance === 'number' &&
|
||||||
supplierReport.initial_balance !== 0
|
supplierReport.initial_balance !== 0
|
||||||
|
|||||||
@@ -1,279 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
Document,
|
|
||||||
StyleSheet,
|
|
||||||
Font,
|
|
||||||
pdf,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|
||||||
import {
|
|
||||||
PdfTable,
|
|
||||||
PdfColumn,
|
|
||||||
PdfTbodyCell,
|
|
||||||
} from '@/components/helper/pdf/table';
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: 'Helvetica',
|
|
||||||
src: 'helvetica',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pdfStyles = StyleSheet.create({
|
|
||||||
page: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontFamily: 'Helvetica',
|
|
||||||
padding: 20,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
},
|
|
||||||
titleSection: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
mainTitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 5,
|
|
||||||
color: '#1f74bf',
|
|
||||||
},
|
|
||||||
supplierTitle: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 8,
|
|
||||||
color: '#1f74bf',
|
|
||||||
},
|
|
||||||
badge: {
|
|
||||||
backgroundColor: '#1f74bf',
|
|
||||||
color: '#FFFFFF',
|
|
||||||
padding: 2,
|
|
||||||
borderRadius: 2,
|
|
||||||
fontSize: 7,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
alignSelf: 'center',
|
|
||||||
marginRight: 4,
|
|
||||||
},
|
|
||||||
parameterBadge: {
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
color: '#333333',
|
|
||||||
padding: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
fontSize: 8,
|
|
||||||
marginRight: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
parameterContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
supplierSection: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
supplierSectionBreak: {
|
|
||||||
marginBottom: 15,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface PurchasesPerSupplierExportParams {
|
|
||||||
data: LogisticPurchasePerSupplierReport[];
|
|
||||||
params: {
|
|
||||||
area_name?: string;
|
|
||||||
supplier_name?: string;
|
|
||||||
product_name?: string;
|
|
||||||
product_category_name?: string;
|
|
||||||
received_date?: string;
|
|
||||||
po_date?: string;
|
|
||||||
start_date?: string;
|
|
||||||
end_date?: string;
|
|
||||||
sort_by?: string;
|
|
||||||
filter_by?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParameterText = (
|
|
||||||
params: PurchasesPerSupplierExportParams['params']
|
|
||||||
) => {
|
|
||||||
const paramsText = [];
|
|
||||||
|
|
||||||
if (params.supplier_name) {
|
|
||||||
paramsText.push(`Supplier: ${params.supplier_name}`);
|
|
||||||
} else {
|
|
||||||
paramsText.push('Semua Supplier');
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 'receive_date', header: 'Tanggal Terima', flex: 1, align: 'center' },
|
|
||||||
{ key: 'po_date', header: 'Tanggal PO', flex: 1, align: 'center' },
|
|
||||||
{ key: 'po_number', header: 'Referensi', flex: 1, align: 'left' },
|
|
||||||
{ key: 'product', header: 'Produk', flex: 1, align: 'left' },
|
|
||||||
{ key: 'warehouse', header: 'Tujuan', flex: 1, align: 'left' },
|
|
||||||
{ key: 'qty', header: 'Qty', flex: 0.8, align: 'right' },
|
|
||||||
{ key: 'unit_price', header: 'Harga Beli', flex: 1.2, align: 'right' },
|
|
||||||
{
|
|
||||||
key: 'purchase_value',
|
|
||||||
header: 'Nilai Pembelian',
|
|
||||||
flex: 1.5,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'transport_price',
|
|
||||||
header: 'Biaya Transport',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'total_amount', header: 'Total', flex: 1.5, align: 'right' },
|
|
||||||
{ key: 'expedition', header: 'Armada', flex: 1.2, align: 'center' },
|
|
||||||
{ key: 'delivery_number', header: 'Surat Jalan', flex: 1, align: 'left' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getTableData = (
|
|
||||||
rows: LogisticPurchasePerSupplierReport['rows']
|
|
||||||
): PdfTbodyCell[][] => {
|
|
||||||
return rows.map((item, index) => [
|
|
||||||
{ key: 'no', value: index + 1, align: 'center' },
|
|
||||||
{
|
|
||||||
key: 'receive_date',
|
|
||||||
value: formatDate(item.receive_date, 'DD-MMM-YYYY'),
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'po_date',
|
|
||||||
value: formatDate(item.po_date, 'DD-MMM-YYYY'),
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{ key: 'po_number', value: item.po_number || '-' },
|
|
||||||
{ key: 'product', value: item.product?.name || '-' },
|
|
||||||
{ key: 'warehouse', value: item.warehouse?.name || '-' },
|
|
||||||
{ key: 'qty', value: formatNumber(item.qty || 0), align: 'right' },
|
|
||||||
{
|
|
||||||
key: 'unit_price',
|
|
||||||
value: formatCurrency(item.unit_price || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'purchase_value',
|
|
||||||
value: formatCurrency(item.purchase_value || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'transport_price',
|
|
||||||
value: formatCurrency(item.transport_unit_price || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'total_amount',
|
|
||||||
value: formatCurrency(item.total_amount || 0),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'expedition',
|
|
||||||
value: (
|
|
||||||
<View style={pdfStyles.badge}>
|
|
||||||
<Text>{item.expedition || '-'}</Text>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{ key: 'delivery_number', value: item.delivery_number || '-' },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPDFDocument = (
|
|
||||||
supplierReports: LogisticPurchasePerSupplierReport[],
|
|
||||||
params: PurchasesPerSupplierExportParams['params']
|
|
||||||
) => (
|
|
||||||
<Document>
|
|
||||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
|
||||||
{/* Title and Parameters */}
|
|
||||||
<View style={pdfStyles.titleSection}>
|
|
||||||
<Text style={pdfStyles.mainTitle}>
|
|
||||||
Laporan > Rekapitulasi Pembelian Per Supplier
|
|
||||||
</Text>
|
|
||||||
<View style={pdfStyles.parameterContainer}>
|
|
||||||
<View style={pdfStyles.parameterBadge}>
|
|
||||||
<Text>
|
|
||||||
Jenis Tanggal:{' '}
|
|
||||||
{params.filter_by === 'received_date'
|
|
||||||
? 'Tanggal Terima'
|
|
||||||
: 'Tanggal PO'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{getParameterText(params).map((param, index) => (
|
|
||||||
<View key={index} style={pdfStyles.parameterBadge}>
|
|
||||||
<Text>{param}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Supplier Sections */}
|
|
||||||
{supplierReports.map(
|
|
||||||
(
|
|
||||||
supplierReport: LogisticPurchasePerSupplierReport,
|
|
||||||
supplierIndex: number
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
key={supplierReport.supplier.id}
|
|
||||||
style={[
|
|
||||||
pdfStyles.supplierSection,
|
|
||||||
supplierIndex < supplierReports.length - 1
|
|
||||||
? pdfStyles.supplierSectionBreak
|
|
||||||
: {},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Text style={pdfStyles.supplierTitle}>
|
|
||||||
{supplierReport.supplier.name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<PdfTable
|
|
||||||
columns={getTableColumns()}
|
|
||||||
data={getTableData(supplierReport.rows)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Page>
|
|
||||||
</Document>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const generatePurchasesPerSupplierPDF = async (
|
|
||||||
data: LogisticPurchasePerSupplierReport[],
|
|
||||||
params: PurchasesPerSupplierExportParams['params']
|
|
||||||
): Promise<void> => {
|
|
||||||
const PDFDocument = createPDFDocument(data, params);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const blob = await pdf(PDFDocument).toBlob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
View,
|
||||||
|
Document,
|
||||||
|
StyleSheet,
|
||||||
|
Font,
|
||||||
|
pdf,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
||||||
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { PdfTable, PdfColumn } 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',
|
||||||
|
src: 'helvetica',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PurchasesPerSupplierExportParams {
|
||||||
|
data: LogisticPurchasePerSupplierReport[];
|
||||||
|
params?: {
|
||||||
|
area_name?: string;
|
||||||
|
supplier_name?: string;
|
||||||
|
product_name?: string;
|
||||||
|
product_category_name?: string;
|
||||||
|
received_date?: string;
|
||||||
|
po_date?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableColumns = (
|
||||||
|
summary?: LogisticPurchasePerSupplierReport['summary']
|
||||||
|
): PdfColumn<LogisticPurchasePerSupplierReport['rows'][number]>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
footer: 'Total',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'receive_date',
|
||||||
|
header: 'Tanggal Terima',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.receive_date ? formatDate(row.receive_date, 'DD MMM YY') : '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'po_date',
|
||||||
|
header: 'Tanggal PO',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.po_date ? formatDate(row.po_date, 'DD MMM YY') : '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'po_number',
|
||||||
|
header: 'No. Referensi',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.po_number || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'product',
|
||||||
|
header: 'Nama Produk',
|
||||||
|
flex: 2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.product?.name || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'warehouse',
|
||||||
|
header: 'Tujuan',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.warehouse?.name || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'qty',
|
||||||
|
header: 'QTY',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.qty || 0),
|
||||||
|
footer: summary ? formatNumber(summary.total_qty || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unit_price',
|
||||||
|
header: 'Harga Beli (Rp)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.unit_price || 0),
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'purchase_value',
|
||||||
|
header: 'Value Harga Beli (Rp)',
|
||||||
|
flex: 1.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.purchase_value || 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_purchase_value || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'transport_unit_price',
|
||||||
|
header: 'Transport (Rp)',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.transport_unit_price || 0),
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'transport_value',
|
||||||
|
header: 'Value Transport (Rp)',
|
||||||
|
flex: 1.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.transport_value || 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_transport_value || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_amount',
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.total_amount || 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_amount || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expedition',
|
||||||
|
header: 'Ekspedisi',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.expedition ? (
|
||||||
|
<View style={{ alignItems: 'center' }}>
|
||||||
|
<PdfStatusBadge
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#DBEAFE',
|
||||||
|
color: '#1E40AF',
|
||||||
|
borderColor: '#60A5FA',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.expedition}
|
||||||
|
</PdfStatusBadge>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delivery_number',
|
||||||
|
header: 'Surat Jalan',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.delivery_number || '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createPDFDocument = (params: PurchasesPerSupplierExportParams) => {
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
{params.data.map((supplierReport, supplierIndex) => (
|
||||||
|
<Page
|
||||||
|
key={supplierIndex}
|
||||||
|
size='A3'
|
||||||
|
orientation='landscape'
|
||||||
|
style={pdfStyles.page}
|
||||||
|
>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<PdfTypography size='h1' variant='primary'>
|
||||||
|
Laporan > Rekapitulasi Pembelian Per Supplier
|
||||||
|
</PdfTypography>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Jenis Tanggal:{' '}
|
||||||
|
{params.params?.filter_by === 'received_date'
|
||||||
|
? 'Tanggal Terima'
|
||||||
|
: 'Tanggal PO'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<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>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Area: {params.params?.area_name || 'Semua Area'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Produk: {params.params?.product_name || 'Semua Produk'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
</View>
|
||||||
|
<PdfTypography size='h2' variant='primary'>
|
||||||
|
{supplierReport.supplier.name}
|
||||||
|
</PdfTypography>
|
||||||
|
{supplierReport.supplier.address && (
|
||||||
|
<PdfTypography size='label'>
|
||||||
|
Alamat: {supplierReport.supplier.address}
|
||||||
|
</PdfTypography>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<PdfTable
|
||||||
|
columns={getTableColumns(supplierReport.summary)}
|
||||||
|
data={supplierReport.rows}
|
||||||
|
showFooter={!!supplierReport.summary}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
))}
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePurchasesPerSupplierPDF = async (
|
||||||
|
params: PurchasesPerSupplierExportParams
|
||||||
|
): Promise<void> => {
|
||||||
|
const PDFDocument = createPDFDocument(params);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await pdf(PDFDocument).toBlob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock';
|
||||||
|
|
||||||
|
interface PurchasesPerSupplierExportExcelParams {
|
||||||
|
data: LogisticPurchasePerSupplierReport[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generatePurchasesPerSupplierExcel = async (
|
||||||
|
params: PurchasesPerSupplierExportExcelParams
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!params.data || params.data.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ header: 'No', key: 'no', width: 5 },
|
||||||
|
{ header: 'Tanggal Terima', key: 'receiveDate', width: 15 },
|
||||||
|
{ header: 'Tanggal PO', key: 'poDate', width: 15 },
|
||||||
|
{ header: 'No. Referensi', key: 'poNumber', width: 15 },
|
||||||
|
{ header: 'Nama Produk', key: 'productName', width: 30 },
|
||||||
|
{ header: 'Tujuan', key: 'warehouse', width: 20 },
|
||||||
|
{ header: 'QTY', key: 'qty', width: 10 },
|
||||||
|
{ header: 'Harga Beli (Rp)', key: 'unitPrice', width: 18 },
|
||||||
|
{ header: 'Value Harga Beli (Rp)', key: 'purchaseValue', width: 20 },
|
||||||
|
{ header: 'Transport (Rp)', key: 'transportUnitPrice', width: 15 },
|
||||||
|
{ header: 'Value Transport (Rp)', key: 'transportValue', width: 20 },
|
||||||
|
{ header: 'Jumlah (Rp)', key: 'totalAmount', width: 18 },
|
||||||
|
{ header: 'Ekspedisi', key: 'expedition', width: 15 },
|
||||||
|
{ header: 'Surat Jalan', key: 'deliveryNumber', width: 15 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const supplierReport of params.data) {
|
||||||
|
const supplierData = supplierReport.rows;
|
||||||
|
const supplierName = supplierReport.supplier?.name || 'Unknown Supplier';
|
||||||
|
|
||||||
|
const worksheet = workbook.addWorksheet(supplierName.substring(0, 31));
|
||||||
|
worksheet.columns = columns;
|
||||||
|
|
||||||
|
supplierData.forEach((item, index) => {
|
||||||
|
worksheet.addRow({
|
||||||
|
no: index + 1,
|
||||||
|
receiveDate: item.receive_date
|
||||||
|
? formatDate(item.receive_date, 'DD MMM YYYY')
|
||||||
|
: '',
|
||||||
|
poDate: item.po_date ? formatDate(item.po_date, 'DD MMM YYYY') : '',
|
||||||
|
poNumber: item.po_number || '',
|
||||||
|
productName: item.product?.name || '',
|
||||||
|
warehouse: item.warehouse?.name || '',
|
||||||
|
qty: formatNumber(item.qty || 0),
|
||||||
|
unitPrice: formatCurrency(item.unit_price || 0),
|
||||||
|
purchaseValue: formatCurrency(item.purchase_value || 0),
|
||||||
|
transportUnitPrice: formatCurrency(item.transport_unit_price || 0),
|
||||||
|
transportValue: formatCurrency(item.transport_value || 0),
|
||||||
|
totalAmount: formatCurrency(item.total_amount || 0),
|
||||||
|
expedition: item.expedition || '',
|
||||||
|
deliveryNumber: item.delivery_number || '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (supplierReport.summary) {
|
||||||
|
worksheet.addRow({
|
||||||
|
no: 'Total',
|
||||||
|
receiveDate: '',
|
||||||
|
poDate: '',
|
||||||
|
poNumber: '',
|
||||||
|
productName: '',
|
||||||
|
warehouse: '',
|
||||||
|
qty: formatNumber(supplierReport.summary.total_qty || 0),
|
||||||
|
unitPrice: '',
|
||||||
|
purchaseValue: formatCurrency(
|
||||||
|
supplierReport.summary.total_purchase_value || 0
|
||||||
|
),
|
||||||
|
transportUnitPrice: '',
|
||||||
|
transportValue: formatCurrency(
|
||||||
|
supplierReport.summary.total_transport_value || 0
|
||||||
|
),
|
||||||
|
totalAmount: formatCurrency(supplierReport.summary.total_amount || 0),
|
||||||
|
expedition: '',
|
||||||
|
deliveryNumber: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
|
||||||
|
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
@@ -26,9 +26,9 @@ import Button from '@/components/Button';
|
|||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport';
|
import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF';
|
||||||
|
import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const PurchasesPerSupplierTab = () => {
|
const PurchasesPerSupplierTab = () => {
|
||||||
@@ -355,98 +355,14 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
await generatePurchasesPerSupplierExcel({ data: allDataForExport });
|
||||||
|
|
||||||
allDataForExport.forEach((supplierReport) => {
|
|
||||||
const supplierData = supplierReport.rows;
|
|
||||||
const supplierName =
|
|
||||||
supplierReport.supplier?.name || 'Unknown Supplier';
|
|
||||||
|
|
||||||
const excelData: { [key: string]: string | number }[] =
|
|
||||||
supplierData.map((item, index) => ({
|
|
||||||
No: index + 1,
|
|
||||||
'Tanggal Terima': item.receive_date
|
|
||||||
? formatDate(item.receive_date, 'DD MMM YYYY')
|
|
||||||
: '',
|
|
||||||
'Tanggal PO': item.po_date
|
|
||||||
? formatDate(item.po_date, 'DD MMM YYYY')
|
|
||||||
: '',
|
|
||||||
'No. Referensi': item.po_number || '',
|
|
||||||
'Nama Produk': item.product?.name || '',
|
|
||||||
Tujuan: item.warehouse?.name || '',
|
|
||||||
QTY: item.qty || 0,
|
|
||||||
'Harga Beli (Rp)': item.unit_price || 0,
|
|
||||||
'Value Harga Beli (Rp)': item.purchase_value || 0,
|
|
||||||
'Transport (Rp)': item.transport_unit_price || 0,
|
|
||||||
'Value Transport (Rp)': item.transport_value || 0,
|
|
||||||
'Jumlah (Rp)': item.total_amount || 0,
|
|
||||||
Ekspedisi: item.expedition || '',
|
|
||||||
'Surat Jalan': item.delivery_number || '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (supplierReport.summary) {
|
|
||||||
excelData.push({
|
|
||||||
No: 'Total',
|
|
||||||
'Tanggal Terima': '',
|
|
||||||
'Tanggal PO': '',
|
|
||||||
'No. Referensi': '',
|
|
||||||
'Nama Produk': '',
|
|
||||||
Tujuan: '',
|
|
||||||
QTY: supplierReport.summary.total_qty || 0,
|
|
||||||
'Harga Beli (Rp)': '',
|
|
||||||
'Value Harga Beli (Rp)':
|
|
||||||
supplierReport.summary.total_purchase_value || 0,
|
|
||||||
'Transport (Rp)': '',
|
|
||||||
'Value Transport (Rp)':
|
|
||||||
supplierReport.summary.total_transport_value || 0,
|
|
||||||
'Jumlah (Rp)': supplierReport.summary.total_amount || 0,
|
|
||||||
Ekspedisi: '',
|
|
||||||
'Surat Jalan': '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
|
||||||
|
|
||||||
const colWidths = [
|
|
||||||
{ wch: 5 }, // No
|
|
||||||
{ wch: 15 }, // Tanggal Terima
|
|
||||||
{ wch: 15 }, // Tanggal PO
|
|
||||||
{ wch: 15 }, // No. Referensi
|
|
||||||
{ wch: 30 }, // Nama Produk
|
|
||||||
{ wch: 20 }, // Tujuan
|
|
||||||
{ wch: 10 }, // QTY
|
|
||||||
{ wch: 18 }, // Harga Beli
|
|
||||||
{ wch: 20 }, // Value Harga Beli
|
|
||||||
{ wch: 15 }, // Transport
|
|
||||||
{ wch: 20 }, // Value Transport
|
|
||||||
{ wch: 18 }, // Jumlah
|
|
||||||
{ wch: 15 }, // Ekspedisi
|
|
||||||
{ wch: 15 }, // Surat Jalan
|
|
||||||
];
|
|
||||||
worksheet['!cols'] = colWidths;
|
|
||||||
|
|
||||||
const sheetName =
|
|
||||||
supplierName.length > 31
|
|
||||||
? supplierName.substring(0, 31)
|
|
||||||
: supplierName;
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`;
|
|
||||||
|
|
||||||
XLSX.writeFile(workbook, filename);
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [logisticPurchasePerSupplierExport]);
|
||||||
logisticPurchasePerSupplierExport,
|
|
||||||
tableFilterState,
|
|
||||||
areaOptions,
|
|
||||||
supplierOptions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleExportPdf = useCallback(async () => {
|
const handleExportPdf = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -517,7 +433,10 @@ const PurchasesPerSupplierTab = () => {
|
|||||||
end_date: tableFilterState.end_date || '',
|
end_date: tableFilterState.end_date || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
await generatePurchasesPerSupplierPDF(allDataForExport, exportParams);
|
await generatePurchasesPerSupplierPDF({
|
||||||
|
data: allDataForExport,
|
||||||
|
params: exportParams,
|
||||||
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
|
|||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
import { JSX, useState } from 'react';
|
import { JSX, useState } from 'react';
|
||||||
|
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent';
|
||||||
import HppPerKandangTab from './sale/tab/HppPerKandangTab';
|
import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab';
|
||||||
|
|
||||||
type MarketingReportTabType =
|
type MarketingReportTabType =
|
||||||
| 'daily'
|
| 'daily'
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer';
|
||||||
|
import {
|
||||||
|
DailyMarketingReport,
|
||||||
|
SalesSummary,
|
||||||
|
} from '@/types/api/report/marketing';
|
||||||
|
import {
|
||||||
|
formatCurrency,
|
||||||
|
formatDate,
|
||||||
|
formatNumber,
|
||||||
|
formatTitleCase,
|
||||||
|
} from '@/lib/helper';
|
||||||
|
import { PdfTable, PdfColumn } 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';
|
||||||
|
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
||||||
|
|
||||||
|
Font.register({
|
||||||
|
family: 'Helvetica',
|
||||||
|
src: 'helvetica',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DailyMarketingReportPDFProps {
|
||||||
|
data?: DailyMarketingReport;
|
||||||
|
total?: SalesSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableColumns = (
|
||||||
|
summary?: SalesSummary
|
||||||
|
): PdfColumn<DailyMarketingReport[number]>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'so_date',
|
||||||
|
header: 'Tanggal Sales Order',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'do_date',
|
||||||
|
header: 'Tanggal Delivery Order',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.realization_date
|
||||||
|
? formatDate(row.realization_date, 'DD MMM YY')
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'aging',
|
||||||
|
header: 'Aging (Hari)',
|
||||||
|
flex: 0.7,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) => row.aging_days ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'warehouse',
|
||||||
|
header: 'Gudang',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.warehouse?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'customer',
|
||||||
|
header: 'Pelanggan',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.customer?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sales',
|
||||||
|
header: 'Sales',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.sales?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'product',
|
||||||
|
header: 'Produk',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.product?.name ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'do_number',
|
||||||
|
header: 'Nomor DO',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.do_number ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vehicle',
|
||||||
|
header: 'Nomor Polisi',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.vehicle_number ?? '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'marketing_type',
|
||||||
|
header: 'Tipe Marketing',
|
||||||
|
flex: 1,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.marketing_type ? (
|
||||||
|
<View style={{ alignItems: 'center' }}>
|
||||||
|
<PdfStatusBadge
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
row.marketing_type.toLowerCase() === 'ayam'
|
||||||
|
? '#FEF3C7'
|
||||||
|
: row.marketing_type.toLowerCase() === 'trading'
|
||||||
|
? '#DBEAFE'
|
||||||
|
: row.marketing_type.toLowerCase() === 'telur'
|
||||||
|
? '#D1FAE5'
|
||||||
|
: '#F5F5F5',
|
||||||
|
color:
|
||||||
|
row.marketing_type.toLowerCase() === 'ayam'
|
||||||
|
? '#92400E'
|
||||||
|
: row.marketing_type.toLowerCase() === 'trading'
|
||||||
|
? '#1E40AF'
|
||||||
|
: row.marketing_type.toLowerCase() === 'telur'
|
||||||
|
? '#065F46'
|
||||||
|
: '#333333',
|
||||||
|
borderColor:
|
||||||
|
row.marketing_type.toLowerCase() === 'ayam'
|
||||||
|
? '#FBBF24'
|
||||||
|
: row.marketing_type.toLowerCase() === 'trading'
|
||||||
|
? '#60A5FA'
|
||||||
|
: row.marketing_type.toLowerCase() === 'telur'
|
||||||
|
? '#34D399'
|
||||||
|
: '#E5E7EB',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatTitleCase(row.marketing_type)}
|
||||||
|
</PdfStatusBadge>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'qty',
|
||||||
|
header: 'Quantity',
|
||||||
|
flex: 0.7,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.qty ?? 0),
|
||||||
|
footer: summary ? formatNumber(summary.total_qty ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'avg_weight',
|
||||||
|
header: 'Rata-Rata (Kg)',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.average_weight_kg ?? 0),
|
||||||
|
footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_weight',
|
||||||
|
header: 'Total Berat (Kg)',
|
||||||
|
flex: 0.9,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.total_weight_kg ?? 0),
|
||||||
|
footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sales_price',
|
||||||
|
header: 'Harga Jual (Rp)',
|
||||||
|
flex: 0.9,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.sales_price_per_kg ?? 0),
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hpp_price',
|
||||||
|
header: 'HPP (Rp)',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.hpp_price_per_kg ?? 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_hpp_price_per_kg ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sales_amount',
|
||||||
|
header: 'Total Jual (Rp)',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.sales_amount ?? 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_sales_amount ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hpp_amount',
|
||||||
|
header: 'Total HPP (Rp)',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.hpp_amount ?? 0),
|
||||||
|
footer: summary ? formatCurrency(summary.total_hpp_amount ?? 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DailyMarketingReportPDF = ({
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
}: DailyMarketingReportPDFProps) => {
|
||||||
|
const rows = data || [];
|
||||||
|
const summary = total;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<PdfTypography size='h1' variant='primary'>
|
||||||
|
Laporan > Penjualan Harian
|
||||||
|
</PdfTypography>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<PdfTable
|
||||||
|
columns={getTableColumns(summary)}
|
||||||
|
data={rows}
|
||||||
|
showFooter={!!summary}
|
||||||
|
footerLabel='TOTAL'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PdfPageNumber />
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyMarketingReportPDF;
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
View,
|
||||||
|
Document,
|
||||||
|
StyleSheet,
|
||||||
|
Font,
|
||||||
|
pdf,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
import {
|
||||||
|
HppPerKandangReport,
|
||||||
|
HppPerKandangRow,
|
||||||
|
HppPerKandangPerWeightRange,
|
||||||
|
} from '@/types/api/report/hpp-per-kandang';
|
||||||
|
import { formatDate, formatNumber, formatCurrency } from '@/lib/helper';
|
||||||
|
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||||
|
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||||
|
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||||
|
|
||||||
|
Font.register({
|
||||||
|
family: 'Helvetica',
|
||||||
|
src: 'helvetica',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfStyles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
titleSection: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface HppPerKandangExportParams {
|
||||||
|
data: HppPerKandangReport;
|
||||||
|
params?: {
|
||||||
|
area_name?: string;
|
||||||
|
location_name?: string;
|
||||||
|
kandang_name?: string;
|
||||||
|
period?: string;
|
||||||
|
weight_min?: string;
|
||||||
|
weight_max?: string;
|
||||||
|
show_unrecorded?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSuppliers = (
|
||||||
|
suppliers: { alias?: string; name: string }[] | null | undefined
|
||||||
|
): string => {
|
||||||
|
if (!suppliers || suppliers.length === 0) return '-';
|
||||||
|
return suppliers.map((s) => s.alias || s.name).join(' | ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions for PdfTable - Rekapitulasi
|
||||||
|
const getRekapitulasiColumns = (): PdfColumn<HppPerKandangPerWeightRange>[] => [
|
||||||
|
{
|
||||||
|
key: 'rentang_bw',
|
||||||
|
header: 'Rentang BW',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) => row.label || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sisa_butir',
|
||||||
|
header: 'Sisa Butir',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.egg_production_pieces || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sisa_kg',
|
||||||
|
header: 'Sisa Kg',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.egg_production_kg || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rata_rata_bobot',
|
||||||
|
header: 'Rata-Rata Bobot (Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.avg_weight_kg || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'feed_supplier',
|
||||||
|
header: 'Feed (Supplier)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => formatSuppliers(row.feed_suppliers),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'doc_supplier',
|
||||||
|
header: 'DOC (Supplier)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => formatSuppliers(row.doc_suppliers),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rata_harga_doc',
|
||||||
|
header: 'Rata-Rata Harga DOC',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hpp_telur',
|
||||||
|
header: 'HPP Telur (Rp/Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nominal_sisa',
|
||||||
|
header: 'Nominal Sisa',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.egg_value_rp || 0),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper functions for PdfTable - Detail Per Kandang
|
||||||
|
const getDetailColumns = (
|
||||||
|
summary?: HppPerKandangReport['summary'],
|
||||||
|
allFeedSuppliers?: string,
|
||||||
|
allDocSuppliers?: string
|
||||||
|
): PdfColumn<HppPerKandangRow>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
footer: 'TOTAL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kandang',
|
||||||
|
header: 'Kandang',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => row.kandang?.name || '-',
|
||||||
|
footer: 'ALL',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rentang_bw',
|
||||||
|
header: 'Rentang BW',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.weight_range.weight_min.toFixed(2)} - ${row.weight_range.weight_max.toFixed(2)}`,
|
||||||
|
footer: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rata_rata_bobot',
|
||||||
|
header: 'Rata-Rata Bobot (Kg)',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.avg_weight_kg || 0),
|
||||||
|
footer: summary ? formatNumber(summary.total.average_weight_kg || 0) : '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sisa_butir',
|
||||||
|
header: 'Sisa Butir',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.egg_production_pieces || 0),
|
||||||
|
footer: summary
|
||||||
|
? formatNumber(summary.total.total_egg_production_pieces || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sisa_kg',
|
||||||
|
header: 'Sisa Kg (Telur)',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatNumber(row.egg_production_kg || 0),
|
||||||
|
footer: summary
|
||||||
|
? formatNumber(summary.total.total_egg_production_kg || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'feed_supplier',
|
||||||
|
header: 'Feed (Supplier)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => formatSuppliers(row.feed_suppliers),
|
||||||
|
footer: allFeedSuppliers || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'doc_supplier',
|
||||||
|
header: 'DOC (Supplier)',
|
||||||
|
flex: 1,
|
||||||
|
align: 'left',
|
||||||
|
cell: ({ row }) => formatSuppliers(row.doc_suppliers),
|
||||||
|
footer: allDocSuppliers || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rata_harga_doc',
|
||||||
|
header: 'Rata-Rata Harga DOC',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0),
|
||||||
|
footer: summary
|
||||||
|
? formatCurrency(summary.total.total_average_doc_price_rp || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hpp_telur',
|
||||||
|
header: 'HPP Telur (Rp/Kg)',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0),
|
||||||
|
footer: summary
|
||||||
|
? formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'nominal_sisa',
|
||||||
|
header: 'Nominal Sisa',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => formatCurrency(row.egg_value_rp || 0),
|
||||||
|
footer: summary
|
||||||
|
? formatCurrency(summary.total.total_egg_value_rp || 0)
|
||||||
|
: '',
|
||||||
|
footerAlign: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createPDFDocument = (
|
||||||
|
params: HppPerKandangExportParams,
|
||||||
|
allFeedSuppliers: string,
|
||||||
|
allDocSuppliers: string
|
||||||
|
) => {
|
||||||
|
const rekapitulasiByWeightRange = params.data.summary?.per_weight_range || [];
|
||||||
|
|
||||||
|
const weightRangeText =
|
||||||
|
params.params?.weight_min || params.params?.weight_max
|
||||||
|
? params.params.weight_min && params.params.weight_max
|
||||||
|
? `${params.params.weight_min} - ${params.params.weight_max} kg`
|
||||||
|
: params.params.weight_min
|
||||||
|
? `≥ ${params.params.weight_min} kg`
|
||||||
|
: `≤ ${params.params.weight_max} kg`
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<PdfTypography size='h1' variant='primary'>
|
||||||
|
Laporan > HPP Harian Kandang
|
||||||
|
</PdfTypography>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Area: {params.params?.area_name || 'Semua Area'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Lokasi: {params.params?.location_name || 'Semua Lokasi'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Kandang: {params.params?.kandang_name || 'Semua Kandang'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Periode:{' '}
|
||||||
|
{params.params?.period
|
||||||
|
? formatDate(params.params.period, 'DD MMM YYYY')
|
||||||
|
: '-'}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>Rentang Bobot: {weightRangeText}</PdfParamBadge>
|
||||||
|
{params.params?.show_unrecorded === 'true' && (
|
||||||
|
<PdfParamBadge>Tampilkan: Tanpa Recording</PdfParamBadge>
|
||||||
|
)}
|
||||||
|
<PdfParamBadge>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Rekapitulasi Section */}
|
||||||
|
<View style={pdfStyles.section}>
|
||||||
|
<PdfTypography size='h2' variant='primary'>
|
||||||
|
Rekapitulasi
|
||||||
|
</PdfTypography>
|
||||||
|
<PdfTable
|
||||||
|
columns={getRekapitulasiColumns()}
|
||||||
|
data={rekapitulasiByWeightRange}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Detail Per Kandang Section */}
|
||||||
|
<View style={pdfStyles.section}>
|
||||||
|
<PdfTypography size='h2' variant='primary'>
|
||||||
|
Detail Per Kandang
|
||||||
|
</PdfTypography>
|
||||||
|
<PdfTable
|
||||||
|
columns={getDetailColumns(
|
||||||
|
params.data.summary,
|
||||||
|
allFeedSuppliers,
|
||||||
|
allDocSuppliers
|
||||||
|
)}
|
||||||
|
data={params.data.rows}
|
||||||
|
showFooter={!!params.data.summary}
|
||||||
|
footerLabel='TOTAL'
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateHppPerKandangPDF = async (
|
||||||
|
params: HppPerKandangExportParams,
|
||||||
|
allFeedSuppliers: string,
|
||||||
|
allDocSuppliers: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const PDFDocument = createPDFDocument(
|
||||||
|
params,
|
||||||
|
allFeedSuppliers,
|
||||||
|
allDocSuppliers
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await pdf(PDFDocument).toBlob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const period =
|
||||||
|
params.params?.period || formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import {
|
||||||
|
HppPerKandangReport,
|
||||||
|
HppPerKandangRow,
|
||||||
|
HppPerKandangPerWeightRange,
|
||||||
|
} from '@/types/api/report/hpp-per-kandang';
|
||||||
|
|
||||||
|
interface HppPerKandangExportExcelParams {
|
||||||
|
data: HppPerKandangReport;
|
||||||
|
allFeedSuppliers: string;
|
||||||
|
allDocSuppliers: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatSuppliers = (
|
||||||
|
suppliers: { alias?: string; name: string }[] | null
|
||||||
|
): string => {
|
||||||
|
if (!suppliers || suppliers.length === 0) return '';
|
||||||
|
return suppliers.map((s) => s.alias || s.name).join(' | ');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateHppPerKandangExcel = async (
|
||||||
|
params: HppPerKandangExportExcelParams
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!params.data || !params.data.rows || params.data.rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
|
||||||
|
// ===== REKAPITULASI WORKSHEET =====
|
||||||
|
const rekapitulasiColumns = [
|
||||||
|
{ header: 'No', key: 'no', width: 5 },
|
||||||
|
{ header: 'Rentang BW', key: 'weightRange', width: 15 },
|
||||||
|
{ header: 'Sisa Butir', key: 'eggPieces', width: 15 },
|
||||||
|
{ header: 'Sisa Kg', key: 'eggKg', width: 12 },
|
||||||
|
{ header: 'Rata-Rata Bobot (Kg)', key: 'avgWeight', width: 18 },
|
||||||
|
{ header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 },
|
||||||
|
{ header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 },
|
||||||
|
{ header: 'Rata-Rata Harga DOC', key: 'avgDocPrice', width: 20 },
|
||||||
|
{ header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 },
|
||||||
|
{ header: 'Nominal Sisa', key: 'eggValue', width: 25 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rekapitulasiWorksheet = workbook.addWorksheet('Rekapitulasi');
|
||||||
|
rekapitulasiWorksheet.columns = rekapitulasiColumns;
|
||||||
|
|
||||||
|
const perWeightRangeSummary = params.data.summary.per_weight_range || [];
|
||||||
|
|
||||||
|
perWeightRangeSummary.forEach(
|
||||||
|
(item: HppPerKandangPerWeightRange, index: number) => {
|
||||||
|
rekapitulasiWorksheet.addRow({
|
||||||
|
no: index + 1,
|
||||||
|
weightRange: item.label || '',
|
||||||
|
eggPieces: formatNumber(item.egg_production_pieces || 0),
|
||||||
|
eggKg: formatNumber(item.egg_production_kg || 0),
|
||||||
|
avgWeight: formatNumber(item.avg_weight_kg || 0),
|
||||||
|
feedSuppliers: formatSuppliers(item.feed_suppliers),
|
||||||
|
docSuppliers: formatSuppliers(item.doc_suppliers),
|
||||||
|
avgDocPrice: formatCurrency(item.average_doc_price_rp || 0),
|
||||||
|
eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0),
|
||||||
|
eggValue: formatCurrency(item.egg_value_rp || 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== DETAIL PER KANDANG WORKSHEET =====
|
||||||
|
const detailColumns = [
|
||||||
|
{ header: 'No', key: 'no', width: 5 },
|
||||||
|
{ header: 'Kandang', key: 'kandang', width: 30 },
|
||||||
|
{ header: 'Rentang Bobot', key: 'weightRange', width: 15 },
|
||||||
|
{ header: 'Rata-Rata Bobot (KG)', key: 'avgWeightKg', width: 18 },
|
||||||
|
{ header: 'Sisa Telur (Butir)', key: 'eggPieces', width: 15 },
|
||||||
|
{ header: 'Sisa Telur (KG)', key: 'eggKg', width: 15 },
|
||||||
|
{ header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 },
|
||||||
|
{ header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 },
|
||||||
|
{ header: 'Rata-Rata Harga DOC (Rp)', key: 'avgDocPrice', width: 20 },
|
||||||
|
{ header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 },
|
||||||
|
{ header: 'Nilai Nominal Sisa Telur (Rp)', key: 'eggValue', width: 25 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const detailWorksheet = workbook.addWorksheet('Detail Per Kandang');
|
||||||
|
detailWorksheet.columns = detailColumns;
|
||||||
|
|
||||||
|
const allExportData = params.data.rows;
|
||||||
|
|
||||||
|
allExportData.forEach((item: HppPerKandangRow, index: number) => {
|
||||||
|
detailWorksheet.addRow({
|
||||||
|
no: index + 1,
|
||||||
|
kandang: item.kandang?.name || '',
|
||||||
|
weightRange: item.weight_range
|
||||||
|
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
||||||
|
: '',
|
||||||
|
avgWeightKg: formatNumber(item.avg_weight_kg || 0),
|
||||||
|
eggPieces: formatNumber(item.egg_production_pieces || 0),
|
||||||
|
eggKg: formatNumber(item.egg_production_kg || 0),
|
||||||
|
feedSuppliers: formatSuppliers(item.feed_suppliers),
|
||||||
|
docSuppliers: formatSuppliers(item.doc_suppliers),
|
||||||
|
avgDocPrice: formatCurrency(item.average_doc_price_rp || 0),
|
||||||
|
eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0),
|
||||||
|
eggValue: formatCurrency(item.egg_value_rp || 0),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add TOTAL row
|
||||||
|
const summaryTotal = params.data.summary.total;
|
||||||
|
detailWorksheet.addRow({
|
||||||
|
no: 'TOTAL',
|
||||||
|
kandang: 'ALL',
|
||||||
|
weightRange: '-',
|
||||||
|
avgWeightKg: formatNumber(summaryTotal?.average_weight_kg || 0),
|
||||||
|
eggPieces: formatNumber(summaryTotal?.total_egg_production_pieces || 0),
|
||||||
|
eggKg: formatNumber(summaryTotal?.total_egg_production_kg || 0),
|
||||||
|
feedSuppliers: params.allFeedSuppliers,
|
||||||
|
docSuppliers: params.allDocSuppliers,
|
||||||
|
avgDocPrice: formatCurrency(summaryTotal?.total_average_doc_price_rp || 0),
|
||||||
|
eggHpp: formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0),
|
||||||
|
eggValue: formatCurrency(summaryTotal?.total_egg_value_rp || 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const filename = `laporan-hpp-harian-kandang-periode-${params.data.period}.xlsx`;
|
||||||
|
|
||||||
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
const blob = new Blob([buffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
+2
-2
@@ -14,9 +14,9 @@ import SelectInput, {
|
|||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable';
|
import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF';
|
import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF';
|
||||||
|
|
||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import {
|
import {
|
||||||
+26
-135
@@ -26,9 +26,9 @@ import Button from '@/components/Button';
|
|||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import { generateHppPerKandangPDF } from '../export/HppPerkandangExport';
|
import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF';
|
||||||
|
import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
const HppPerKandangTab = () => {
|
const HppPerKandangTab = () => {
|
||||||
@@ -346,136 +346,18 @@ const HppPerKandangTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allExportData =
|
await generateHppPerKandangExcel({
|
||||||
allDataForExport.rows as HppPerKandangReport['rows'];
|
data: allDataForExport,
|
||||||
|
allFeedSuppliers,
|
||||||
const perWeightRangeSummary =
|
allDocSuppliers,
|
||||||
allDataForExport.summary.per_weight_range || [];
|
|
||||||
|
|
||||||
const summaryTotal = allDataForExport.summary.total;
|
|
||||||
|
|
||||||
const rekapitulasiData: { [key: string]: string | number }[] =
|
|
||||||
perWeightRangeSummary.map(
|
|
||||||
(item: HppPerKandangPerWeightRange, index: number) => ({
|
|
||||||
No: index + 1,
|
|
||||||
'Rentang BW': item.label || '',
|
|
||||||
'Sisa Butir': item.egg_production_pieces || 0,
|
|
||||||
'Sisa Kg': item.egg_production_kg || 0,
|
|
||||||
'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0,
|
|
||||||
'Feed (Supplier)':
|
|
||||||
item.feed_suppliers
|
|
||||||
?.map(
|
|
||||||
(s: { alias?: string; name: string }) => s.alias || s.name
|
|
||||||
)
|
|
||||||
.join(' | ') || '',
|
|
||||||
'DOC (Supplier)':
|
|
||||||
item.doc_suppliers
|
|
||||||
?.map(
|
|
||||||
(s: { alias?: string; name: string }) => s.alias || s.name
|
|
||||||
)
|
|
||||||
.join(' | ') || '',
|
|
||||||
'Rata-Rata Harga DOC': item.average_doc_price_rp || 0,
|
|
||||||
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
|
||||||
'Nominal Sisa': item.egg_value_rp || 0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData);
|
|
||||||
|
|
||||||
const rekapitulasiColWidths = [
|
|
||||||
{ wch: 5 }, // No
|
|
||||||
{ wch: 15 }, // Rentang BW
|
|
||||||
{ wch: 15 }, // Sisa Butir
|
|
||||||
{ wch: 12 }, // Sisa Kg
|
|
||||||
{ wch: 18 }, // Rata-Rata Bobot (Kg)
|
|
||||||
{ wch: 20 }, // Feed (Supplier)
|
|
||||||
{ wch: 20 }, // DOC (Supplier)
|
|
||||||
{ wch: 20 }, // Rata-Rata Harga DOC
|
|
||||||
{ wch: 18 }, // HPP Telur (RP/KG)
|
|
||||||
{ wch: 25 }, // Nominal Sisa
|
|
||||||
];
|
|
||||||
rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths;
|
|
||||||
|
|
||||||
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
|
||||||
(item: HppPerKandangRow, index: number) => ({
|
|
||||||
No: index + 1,
|
|
||||||
Kandang: item.kandang?.name || '',
|
|
||||||
'Rentang Bobot': item.weight_range
|
|
||||||
? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
|
|
||||||
: '',
|
|
||||||
'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0,
|
|
||||||
'Sisa Telur (Butir)': item.egg_production_pieces || 0,
|
|
||||||
'Sisa Telur (KG)': item.egg_production_kg || 0,
|
|
||||||
'Feed (Supplier)':
|
|
||||||
item.feed_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '',
|
|
||||||
'DOC (Supplier)':
|
|
||||||
item.doc_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '',
|
|
||||||
'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0,
|
|
||||||
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
|
||||||
'Nilai Nominal Sisa Telur (RP)': item.egg_value_rp || 0,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
excelData.push({
|
|
||||||
No: 'TOTAL',
|
|
||||||
Kandang: 'ALL',
|
|
||||||
'Rentang Bobot': '-',
|
|
||||||
'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0,
|
|
||||||
'Sisa Telur (Butir)': summaryTotal?.total_egg_production_pieces || 0,
|
|
||||||
'Sisa Telur (KG)': summaryTotal?.total_egg_production_kg || 0,
|
|
||||||
'Feed (Supplier)': allFeedSuppliers,
|
|
||||||
'DOC (Supplier)': allDocSuppliers,
|
|
||||||
'Rata-Rata Harga DOC (RP)':
|
|
||||||
summaryTotal?.total_average_doc_price_rp || 0,
|
|
||||||
'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0,
|
|
||||||
'Nilai Nominal Sisa Telur (RP)': summaryTotal?.total_egg_value_rp || 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
|
||||||
|
|
||||||
const colWidths = [
|
|
||||||
{ wch: 5 }, // No
|
|
||||||
{ wch: 30 }, // Kandang
|
|
||||||
{ wch: 15 }, // Rentang Bobot
|
|
||||||
{ wch: 18 }, // Rata-Rata Bobot (KG)
|
|
||||||
{ wch: 15 }, // Sisa Telur (Butir)
|
|
||||||
{ wch: 15 }, // Sisa Telur (KG)
|
|
||||||
{ wch: 20 }, // Feed (Supplier)
|
|
||||||
{ wch: 20 }, // DOC (Supplier)
|
|
||||||
{ wch: 20 }, // Rata-Rata Harga DOC (RP)
|
|
||||||
{ wch: 18 }, // HPP Telur (RP/KG)
|
|
||||||
{ wch: 25 }, // Nilai Nominal Sisa Telur (RP)
|
|
||||||
];
|
|
||||||
worksheet['!cols'] = colWidths;
|
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
XLSX.utils.book_append_sheet(
|
|
||||||
workbook,
|
|
||||||
rekapitulasiWorksheet,
|
|
||||||
'Rekapitulasi'
|
|
||||||
);
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang');
|
|
||||||
|
|
||||||
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
|
||||||
|
|
||||||
XLSX.writeFile(workbook, filename);
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]);
|
||||||
hppPerKandangExport,
|
|
||||||
tableFilterState,
|
|
||||||
areaOptions,
|
|
||||||
locationOptions,
|
|
||||||
kandangOptions,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
@@ -524,16 +406,23 @@ const HppPerKandangTab = () => {
|
|||||||
.join(', ') || 'Semua Kandang'
|
.join(', ') || 'Semua Kandang'
|
||||||
: 'Semua Kandang';
|
: 'Semua Kandang';
|
||||||
|
|
||||||
await generateHppPerKandangPDF(allDataForExport, {
|
await generateHppPerKandangPDF(
|
||||||
area_name: areaName,
|
{
|
||||||
location_name: locationName,
|
data: allDataForExport,
|
||||||
kandang_name: kandangName,
|
params: {
|
||||||
period: tableFilterState.period,
|
area_name: areaName,
|
||||||
weight_min: tableFilterState.weight_min,
|
location_name: locationName,
|
||||||
weight_max: tableFilterState.weight_max,
|
kandang_name: kandangName,
|
||||||
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
period: tableFilterState.period,
|
||||||
sort_by: tableFilterState.sort_by,
|
weight_min: tableFilterState.weight_min,
|
||||||
});
|
weight_max: tableFilterState.weight_max,
|
||||||
|
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
||||||
|
sort_by: tableFilterState.sort_by,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allFeedSuppliers,
|
||||||
|
allDocSuppliers
|
||||||
|
);
|
||||||
|
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
@@ -547,6 +436,8 @@ const HppPerKandangTab = () => {
|
|||||||
areaOptions,
|
areaOptions,
|
||||||
locationOptions,
|
locationOptions,
|
||||||
kandangOptions,
|
kandangOptions,
|
||||||
|
allFeedSuppliers,
|
||||||
|
allDocSuppliers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
|
||||||
Document,
|
|
||||||
Page,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
Image,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
|
|
||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
import { ProductionResult } from '@/types/api/report/production-result';
|
import { ProductionResult } from '@/types/api/report/production-result';
|
||||||
|
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||||
|
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||||
|
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
|
||||||
|
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||||
|
|
||||||
type MappedProductionResultsItem = {
|
type MappedProductionResultsItem = {
|
||||||
projectFlockKandang: BaseProjectFlockKandang;
|
projectFlockKandang: BaseProjectFlockKandang;
|
||||||
@@ -25,132 +22,28 @@ interface ProductionResultReportPDFProps {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
page: {
|
page: {
|
||||||
paddingTop: 24,
|
|
||||||
paddingBottom: 52,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
},
|
|
||||||
|
|
||||||
companyInfoHeader: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
companyLogo: {
|
|
||||||
width: 64,
|
|
||||||
height: 'auto',
|
|
||||||
},
|
|
||||||
companyInfoHeaderDate: {
|
|
||||||
paddingTop: 8,
|
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
fontFamily: 'Helvetica',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
},
|
},
|
||||||
companyName: {
|
titleSection: {
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
companyAddress: {
|
|
||||||
fontSize: 8,
|
|
||||||
maxWidth: 420,
|
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
doubleDivider: {
|
parameterContainer: {
|
||||||
width: '100%',
|
|
||||||
height: 6,
|
|
||||||
borderTopWidth: 2,
|
|
||||||
borderTopColor: '#000',
|
|
||||||
borderBottomWidth: 2,
|
|
||||||
borderBottomColor: '#000',
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
marginTop: 14,
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: '150%',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: 'Times-Roman',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
footer: {
|
|
||||||
width: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
flexWrap: 'wrap',
|
||||||
alignItems: 'center',
|
marginBottom: 8,
|
||||||
paddingHorizontal: 16,
|
|
||||||
position: 'absolute',
|
|
||||||
fontSize: 8,
|
|
||||||
bottom: 22,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'grey',
|
|
||||||
},
|
},
|
||||||
|
tableSection: {
|
||||||
section: {
|
marginBottom: 12,
|
||||||
marginTop: 12,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000',
|
|
||||||
padding: 8,
|
|
||||||
},
|
},
|
||||||
|
tableTitle: {
|
||||||
sectionHeader: {
|
|
||||||
marginBottom: 6,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 6,
|
||||||
|
color: '#333',
|
||||||
},
|
},
|
||||||
sectionSubtitle: {
|
|
||||||
fontSize: 8,
|
|
||||||
color: '#444',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Simple grid table (label/value pairs)
|
|
||||||
grid: {
|
|
||||||
width: '100%',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: '#000',
|
|
||||||
},
|
|
||||||
gridRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: '#000',
|
|
||||||
},
|
|
||||||
gridRowLast: {
|
|
||||||
borderBottomWidth: 0,
|
|
||||||
},
|
|
||||||
gridCellLabel: {
|
|
||||||
width: '40%',
|
|
||||||
paddingVertical: 3,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
fontSize: 8,
|
|
||||||
borderRightWidth: 1,
|
|
||||||
borderRightColor: '#000',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
gridCellValue: {
|
|
||||||
width: '60%',
|
|
||||||
paddingVertical: 3,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
fontSize: 8,
|
|
||||||
textAlign: 'right',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Subsection headings
|
|
||||||
groupTitle: {
|
|
||||||
marginTop: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
fontSize: 9,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
emptyText: {
|
emptyText: {
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
@@ -158,136 +51,347 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function safeNum(v: unknown): number {
|
|
||||||
const n = typeof v === 'number' ? v : Number(v);
|
|
||||||
return Number.isFinite(n) ? n : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function valueText(v: unknown) {
|
function valueText(v: unknown) {
|
||||||
if (v === null || v === undefined) return '-';
|
if (v === null || v === undefined) return '-';
|
||||||
if (typeof v === 'number') return formatNumber(v);
|
if (typeof v === 'number') return formatNumber(v);
|
||||||
return String(v);
|
return String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ========================================
|
||||||
* Render label/value table for one ProductionResult.
|
// TABLE 1: WOA & BW
|
||||||
* Uses a compact grid to keep page readable.
|
// ========================================
|
||||||
*/
|
const getBwTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
function ProductionResultGrid({ pr }: { pr: ProductionResult }) {
|
{
|
||||||
const rows: Array<[string, string]> = [
|
key: 'no',
|
||||||
['WOA', valueText(pr.woa)],
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'woa',
|
||||||
|
header: 'Week of Age',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row }) => valueText(row.woa),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bw',
|
||||||
|
header: 'Body Weight',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.bw),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'std_bw',
|
||||||
|
header: 'Std Body Weight',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.std_bw),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'uniformity',
|
||||||
|
header: 'Uniformity',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.uniformity),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'std_uniformity',
|
||||||
|
header: 'Std Uniformity',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.std_uniformity),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// BW
|
// ========================================
|
||||||
['BW', valueText(pr.bw)],
|
// TABLE 2: DEPLESI
|
||||||
['Std BW', valueText(pr.std_bw)],
|
// ========================================
|
||||||
['Uniformity', valueText(pr.uniformity)],
|
const getDepTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
['Std Uniformity', valueText(pr.std_uniformity)],
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dep_kum',
|
||||||
|
header: 'Depletion Cummulative',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.dep_kum),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dep_std',
|
||||||
|
header: 'Depletion Std',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.dep_std),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Dep
|
// ========================================
|
||||||
['Dep Kum', valueText(pr.dep_kum)],
|
// TABLE 3: BUTIRAN
|
||||||
['Dep Std', valueText(pr.dep_std)],
|
// ========================================
|
||||||
|
const getButiranTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'butiran_utuh',
|
||||||
|
header: 'Utuh',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.butiran_utuh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'butiran_putih',
|
||||||
|
header: 'Putih',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.butiran_putih),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'butiran_retak',
|
||||||
|
header: 'Retak',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.butiran_retak),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'butiran_pecah',
|
||||||
|
header: 'Pecah',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.butiran_pecah),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'butiran_jumlah',
|
||||||
|
header: 'Jumlah',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.butiran_jumlah),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_butir',
|
||||||
|
header: 'Total Butir',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.total_butir),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Butiran
|
// ========================================
|
||||||
['Butiran Utuh', valueText(pr.butiran_utuh)],
|
// TABLE 4: BERAT (KG)
|
||||||
['Butiran Putih', valueText(pr.butiran_putih)],
|
// ========================================
|
||||||
['Butiran Retak', valueText(pr.butiran_retak)],
|
const getKgTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
['Butiran Pecah', valueText(pr.butiran_pecah)],
|
{
|
||||||
['Butiran Jumlah', valueText(pr.butiran_jumlah)],
|
key: 'no',
|
||||||
['Total Butir', valueText(pr.total_butir)],
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kg_utuh',
|
||||||
|
header: 'Utuh (Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.kg_utuh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kg_putih',
|
||||||
|
header: 'Putih (Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.kg_putih),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kg_retak',
|
||||||
|
header: 'Retak (Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.kg_retak),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kg_pecah',
|
||||||
|
header: 'Pecah (Kg)',
|
||||||
|
flex: 1.2,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.kg_pecah),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kg_jumlah',
|
||||||
|
header: 'Jumlah (Kg)',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.kg_jumlah),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'total_kg',
|
||||||
|
header: 'Total (Kg)',
|
||||||
|
flex: 1.3,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.total_kg),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Kg
|
// ========================================
|
||||||
['Kg Utuh', valueText(pr.kg_utuh)],
|
// TABLE 5: PERSENTASE
|
||||||
['Kg Putih', valueText(pr.kg_putih)],
|
// ========================================
|
||||||
['Kg Retak', valueText(pr.kg_retak)],
|
const getPersenTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
['Kg Pecah', valueText(pr.kg_pecah)],
|
{
|
||||||
['Kg Jumlah', valueText(pr.kg_jumlah)],
|
key: 'no',
|
||||||
['Total Kg', valueText(pr.total_kg)],
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'persen_utuh',
|
||||||
|
header: 'Utuh (%)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.persen_utuh),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'persen_putih',
|
||||||
|
header: 'Putih (%)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.persen_putih),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'persen_retak',
|
||||||
|
header: '% Retak (%)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.persen_retak),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'persen_pecah',
|
||||||
|
header: '% Pecah (%)',
|
||||||
|
flex: 1.5,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.persen_pecah),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// %
|
// ========================================
|
||||||
['% Utuh', valueText(pr.persen_utuh)],
|
// TABLE 6: PRODUKSI (HD, FI, EM, EW)
|
||||||
['% Putih', valueText(pr.persen_putih)],
|
// ========================================
|
||||||
['% Retak', valueText(pr.persen_retak)],
|
const getProduksi1TableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
['% Pecah', valueText(pr.persen_pecah)],
|
{
|
||||||
|
key: 'no',
|
||||||
|
header: 'No',
|
||||||
|
flex: 0.5,
|
||||||
|
align: 'center',
|
||||||
|
cell: ({ row, index }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hd',
|
||||||
|
header: 'Hen Day',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.hd),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hd_std',
|
||||||
|
header: 'Hen Day Std',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.hd_std),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fi',
|
||||||
|
header: 'Feed Intake',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.fi),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fi_std',
|
||||||
|
header: 'Feed Intake Std',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.fi_std),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'em',
|
||||||
|
header: 'Egg Mass',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.em),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'em_std',
|
||||||
|
header: 'Egg Mass Std',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.em_std),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ew',
|
||||||
|
header: 'Egg Weight',
|
||||||
|
flex: 0.8,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.ew),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ew_std',
|
||||||
|
header: 'Egg Weight Std',
|
||||||
|
flex: 1,
|
||||||
|
align: 'right',
|
||||||
|
cell: ({ row }) => valueText(row.ew_std),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Produksi
|
// ========================================
|
||||||
['HD', valueText(pr.hd)],
|
// TABLE 7: PRODUKSI (FCR, HH)
|
||||||
['HD Std', valueText(pr.hd_std)],
|
// ========================================
|
||||||
['FI', valueText(pr.fi)],
|
const getProduksi2TableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||||
['FI Std', valueText(pr.fi_std)],
|
{
|
||||||
['EM', valueText(pr.em)],
|
key: 'no',
|
||||||
['EM Std', valueText(pr.em_std)],
|
header: 'No',
|
||||||
['EW', valueText(pr.ew)],
|
flex: 0.5,
|
||||||
['EW Std', valueText(pr.ew_std)],
|
align: 'center',
|
||||||
['FCR', valueText(pr.fcr)],
|
cell: ({ row, index }) => index + 1,
|
||||||
['FCR Std', valueText(pr.fcr_std)],
|
},
|
||||||
['HH', valueText(pr.hh)],
|
{
|
||||||
['HH Std', valueText(pr.hh_std)],
|
key: 'fcr',
|
||||||
];
|
header: 'FCR',
|
||||||
|
flex: 1,
|
||||||
return (
|
align: 'right',
|
||||||
<View style={styles.grid}>
|
cell: ({ row }) => valueText(row.fcr),
|
||||||
{rows.map(([label, value], idx) => {
|
},
|
||||||
const isLast = idx === rows.length - 1;
|
{
|
||||||
return (
|
key: 'fcr_std',
|
||||||
<View
|
header: 'FCR Std',
|
||||||
key={label}
|
flex: 1.2,
|
||||||
style={[styles.gridRow, ...(isLast ? [styles.gridRowLast] : [])]}
|
align: 'right',
|
||||||
>
|
cell: ({ row }) => valueText(row.fcr_std),
|
||||||
<Text style={styles.gridCellLabel}>{label}</Text>
|
},
|
||||||
<Text style={styles.gridCellValue}>{value}</Text>
|
{
|
||||||
</View>
|
key: 'hh',
|
||||||
);
|
header: 'Hen House',
|
||||||
})}
|
flex: 1,
|
||||||
</View>
|
align: 'right',
|
||||||
);
|
cell: ({ row }) => valueText(row.hh),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
/**
|
key: 'hh_std',
|
||||||
* If there are multiple ProductionResult entries for a kandang,
|
header: 'Hen House Std',
|
||||||
* we show them sequentially with a small header per result.
|
flex: 1.2,
|
||||||
*
|
align: 'right',
|
||||||
* You can later change this to render only the latest WOA, or group by week.
|
cell: ({ row }) => valueText(row.hh_std),
|
||||||
*/
|
},
|
||||||
function ProductionResultList({
|
];
|
||||||
productionResults,
|
|
||||||
}: {
|
|
||||||
productionResults: ProductionResult[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
{productionResults.map((pr, idx) => {
|
|
||||||
const kandangName =
|
|
||||||
pr.project_flock?.kandang?.name ||
|
|
||||||
pr.project_flock?.kandang?.id?.toString() ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
// Optional: show a compact subheader
|
|
||||||
const headerLeft = `Data #${idx + 1}`;
|
|
||||||
const headerRight =
|
|
||||||
kandangName && pr.woa !== undefined
|
|
||||||
? `${kandangName} • WOA ${safeNum(pr.woa)}`
|
|
||||||
: pr.woa !== undefined
|
|
||||||
? `WOA ${safeNum(pr.woa)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
key={`${pr.project_flock?.id ?? 'pf'}-${idx}`}
|
|
||||||
style={{ marginTop: idx === 0 ? 0 : 10 }}
|
|
||||||
wrap={false}
|
|
||||||
>
|
|
||||||
<View style={styles.sectionHeader}>
|
|
||||||
<Text style={styles.sectionTitle}>{headerLeft}</Text>
|
|
||||||
<Text style={styles.sectionSubtitle}>{headerRight}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<ProductionResultGrid pr={pr} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ✅ Main PDF Component
|
* ✅ Main PDF Component
|
||||||
@@ -297,90 +401,148 @@ const ProductionResultReportPDF = ({
|
|||||||
}: ProductionResultReportPDFProps) => {
|
}: ProductionResultReportPDFProps) => {
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
<Page style={styles.page} size='A4'>
|
{mappedProductionResults.length === 0 ? (
|
||||||
{/* Header */}
|
<Page style={styles.page} size='A4'>
|
||||||
<View>
|
{/* Title and Parameters */}
|
||||||
<View style={styles.companyInfoHeader}>
|
<View style={styles.titleSection}>
|
||||||
<Image style={styles.companyLogo} src='/assets/img/lti-logo.png' />
|
<PdfTypography size='h1' variant='primary'>
|
||||||
<Text style={styles.companyInfoHeaderDate}>
|
Laporan > Production Result
|
||||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
</PdfTypography>
|
||||||
</Text>
|
<View style={styles.parameterContainer}>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View>
|
|
||||||
<Text style={styles.companyName}>PT LUMBUNG TELUR INDONESIA</Text>
|
|
||||||
<Text style={styles.companyAddress}>
|
|
||||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
|
||||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.doubleDivider} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text style={styles.title}>Laporan Production Result</Text>
|
|
||||||
|
|
||||||
{/* Sections per ProjectFlockKandang */}
|
|
||||||
{mappedProductionResults.length === 0 ? (
|
|
||||||
<View style={{ marginTop: 16 }}>
|
<View style={{ marginTop: 16 }}>
|
||||||
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
|
||||||
mappedProductionResults.map((item, idx) => {
|
|
||||||
const pfk = item.projectFlockKandang;
|
|
||||||
|
|
||||||
// Try to display meaningful identifiers.
|
<PdfPageNumber />
|
||||||
// Adjust these fields based on your real BaseProjectFlockKandang structure.
|
</Page>
|
||||||
const kandangName =
|
) : (
|
||||||
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
mappedProductionResults.map((item, idx) => {
|
||||||
|
const pfk = item.projectFlockKandang;
|
||||||
|
|
||||||
const projectName = pfk?.project_flock?.name ?? '';
|
const kandangName =
|
||||||
|
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
||||||
|
|
||||||
const locationName = pfk?.project_flock?.location?.name ?? '';
|
const projectName = pfk?.project_flock?.name ?? '';
|
||||||
|
|
||||||
const areaName = pfk?.project_flock?.area?.name ?? '';
|
const locationName = pfk?.project_flock?.location?.name ?? '';
|
||||||
|
|
||||||
return (
|
const areaName = pfk?.project_flock?.area?.name ?? '';
|
||||||
<View
|
|
||||||
key={`pfk-${pfk?.id ?? idx}`}
|
const hasData =
|
||||||
style={styles.section}
|
item.productionResult && item.productionResult.length > 0;
|
||||||
break={idx > 0} // each kandang starts on a new page for clarity
|
|
||||||
>
|
return (
|
||||||
<View style={styles.sectionHeader}>
|
<Page key={`pfk-${pfk?.id ?? idx}`} style={styles.page} size='A4'>
|
||||||
<Text style={styles.sectionTitle}>
|
{/* Title and Parameters */}
|
||||||
{projectName
|
<View style={styles.titleSection}>
|
||||||
? `${projectName} • ${kandangName}`
|
<PdfTypography size='h1' variant='primary'>
|
||||||
: kandangName}
|
Laporan > Production Result
|
||||||
</Text>
|
</PdfTypography>
|
||||||
<Text style={styles.sectionSubtitle}>
|
<View style={styles.parameterContainer}>
|
||||||
{[areaName, locationName].filter(Boolean).join(' • ')}
|
<PdfParamBadge>
|
||||||
</Text>
|
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</PdfParamBadge>
|
||||||
|
<PdfParamBadge>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</PdfParamBadge>
|
||||||
</View>
|
</View>
|
||||||
|
<PdfTypography size='h2' variant='primary'>
|
||||||
{item.productionResult && item.productionResult.length > 0 ? (
|
{projectName
|
||||||
<ProductionResultList
|
? `${projectName} • ${kandangName}`
|
||||||
productionResults={item.productionResult}
|
: kandangName}
|
||||||
/>
|
</PdfTypography>
|
||||||
) : (
|
<PdfTypography size='label'>
|
||||||
<Text style={styles.emptyText}>
|
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||||
Tidak ada production result untuk kandang ini.
|
</PdfTypography>
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Footer */}
|
{hasData ? (
|
||||||
<View style={styles.footer} fixed>
|
<>
|
||||||
<Text
|
{/* Table 1: WOA & BW */}
|
||||||
render={({ pageNumber, totalPages }) =>
|
<View style={styles.tableSection}>
|
||||||
`${pageNumber} / ${totalPages}`
|
<Text style={styles.tableTitle}>1. WOA & Body Weight</Text>
|
||||||
}
|
<PdfTable
|
||||||
fixed
|
columns={getBwTableColumns()}
|
||||||
/>
|
data={item.productionResult!}
|
||||||
</View>
|
/>
|
||||||
</Page>
|
</View>
|
||||||
|
|
||||||
|
{/* Table 2: Deplesi */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>2. Deplesi</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getDepTableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table 3: Butiran */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>3. Butiran</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getButiranTableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table 4: Berat (Kg) */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>4. Berat (Kg)</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getKgTableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table 5: Persentase */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>5. Persentase</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getPersenTableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table 6: Produksi (HD, FI, EM, EW) */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>
|
||||||
|
6. Produksi (HD, FI, EM, EW)
|
||||||
|
</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getProduksi1TableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table 7: Produksi (FCR, HH) */}
|
||||||
|
<View style={styles.tableSection}>
|
||||||
|
<Text style={styles.tableTitle}>7. Produksi (FCR, HH)</Text>
|
||||||
|
<PdfTable
|
||||||
|
columns={getProduksi2TableColumns()}
|
||||||
|
data={item.productionResult!}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Tidak ada production result untuk kandang ini.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PdfPageNumber />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import Tabs from '@/components/Tabs';
|
|
||||||
import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab';
|
|
||||||
|
|
||||||
const SaleReportTabs = () => {
|
|
||||||
const tabs = [
|
|
||||||
// {
|
|
||||||
// id: '1',
|
|
||||||
// label: 'Penjualan Harian',
|
|
||||||
// content: 'Penjualan Harian Tab',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2',
|
|
||||||
// label: 'Transaksi Penjualan DO',
|
|
||||||
// content: 'Transaksi Penjualan DO Tab',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '3',
|
|
||||||
// label: 'Perbandingan HPP Per Rentang BW',
|
|
||||||
// content: 'Perbandingan HPP Per Rentang BW Tab',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
label: 'HPP Harian Kandang',
|
|
||||||
content: <HppPerKandangTab />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className='w-full p-4'>
|
|
||||||
<Tabs tabs={tabs} variant='lifted' />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SaleReportTabs;
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Text,
|
|
||||||
View,
|
|
||||||
Document,
|
|
||||||
StyleSheet,
|
|
||||||
Font,
|
|
||||||
pdf,
|
|
||||||
} from '@react-pdf/renderer';
|
|
||||||
import {
|
|
||||||
HppPerKandangReport,
|
|
||||||
HppPerKandangRow,
|
|
||||||
HppPerKandangPerWeightRange,
|
|
||||||
} from '@/types/api/report/hpp-per-kandang';
|
|
||||||
import { formatDate, formatNumber, formatCurrency } from '@/lib/helper';
|
|
||||||
import {
|
|
||||||
PdfTable,
|
|
||||||
PdfColumn,
|
|
||||||
PdfTbodyCell,
|
|
||||||
PdfTfootCell,
|
|
||||||
} from '@/components/helper/pdf/table';
|
|
||||||
|
|
||||||
Font.register({
|
|
||||||
family: 'Helvetica',
|
|
||||||
src: 'helvetica',
|
|
||||||
});
|
|
||||||
|
|
||||||
const pdfStyles = StyleSheet.create({
|
|
||||||
page: {
|
|
||||||
fontSize: 10,
|
|
||||||
fontFamily: 'Helvetica',
|
|
||||||
padding: 20,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
},
|
|
||||||
titleSection: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
mainTitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 5,
|
|
||||||
color: '#1f74bf',
|
|
||||||
},
|
|
||||||
supplierTitle: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 8,
|
|
||||||
color: '#1f74bf',
|
|
||||||
},
|
|
||||||
parameterBadge: {
|
|
||||||
backgroundColor: '#F5F5F5',
|
|
||||||
color: '#333333',
|
|
||||||
padding: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
fontSize: 8,
|
|
||||||
marginRight: 8,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
parameterContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
marginBottom: 15,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface HppPerKandangExportParams {
|
|
||||||
data: HppPerKandangReport;
|
|
||||||
params: {
|
|
||||||
area_name?: string;
|
|
||||||
location_name?: string;
|
|
||||||
kandang_name?: string;
|
|
||||||
period?: string;
|
|
||||||
weight_min?: string;
|
|
||||||
weight_max?: string;
|
|
||||||
show_unrecorded?: string;
|
|
||||||
sort_by?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParameterText = (params: HppPerKandangExportParams['params']) => {
|
|
||||||
const paramsText = [];
|
|
||||||
|
|
||||||
if (params.area_name && params.area_name !== 'Semua Area') {
|
|
||||||
paramsText.push(`Area: ${params.area_name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.location_name && params.location_name !== 'Semua Lokasi') {
|
|
||||||
paramsText.push(`Lokasi: ${params.location_name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
|
|
||||||
paramsText.push(`Kandang: ${params.kandang_name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.period) {
|
|
||||||
const formattedDate = formatDate(params.period, 'DD MMM YYYY');
|
|
||||||
paramsText.push(`Tanggal: ${formattedDate}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.weight_min || params.weight_max) {
|
|
||||||
const weightRange =
|
|
||||||
params.weight_min && params.weight_max
|
|
||||||
? `${params.weight_min} - ${params.weight_max} kg`
|
|
||||||
: params.weight_min
|
|
||||||
? `≥ ${params.weight_min} kg`
|
|
||||||
: `≤ ${params.weight_max} kg`;
|
|
||||||
paramsText.push(`Rentang Bobot: ${weightRange}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.show_unrecorded === 'true') {
|
|
||||||
paramsText.push('Tampilkan: Tanpa Recording');
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
|
|
||||||
paramsText.push(`Dicetak: ${currentDate}`);
|
|
||||||
|
|
||||||
return paramsText;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions for PdfTable - Rekapitulasi
|
|
||||||
const getRekapitulasiColumns = (): PdfColumn[] => [
|
|
||||||
{ key: 'rentang_bw', header: 'Rentang BW', flex: 1.2, align: 'center' },
|
|
||||||
{ key: 'sisa_butir', header: 'Sisa Butir', flex: 1, align: 'right' },
|
|
||||||
{ key: 'sisa_kg', header: 'Sisa Kg', flex: 1, align: 'right' },
|
|
||||||
{
|
|
||||||
key: 'rata_rata_bobot',
|
|
||||||
header: 'Rata-Rata Bobot (Kg)',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left' },
|
|
||||||
{ key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left' },
|
|
||||||
{
|
|
||||||
key: 'rata_harga_doc',
|
|
||||||
header: 'Rata-Rata Harga DOC',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1.2, align: 'right' },
|
|
||||||
{ key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getRekapitulasiData = (
|
|
||||||
perWeightRange: HppPerKandangPerWeightRange[]
|
|
||||||
): PdfTbodyCell[][] => {
|
|
||||||
return perWeightRange.map((group) => [
|
|
||||||
{ key: 'rentang_bw', value: group.label, align: 'center' },
|
|
||||||
{
|
|
||||||
key: 'sisa_butir',
|
|
||||||
value: formatNumber(group.egg_production_pieces),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sisa_kg',
|
|
||||||
value: formatNumber(group.egg_production_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rata_rata_bobot',
|
|
||||||
value: formatNumber(group.avg_weight_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'feed_supplier',
|
|
||||||
value:
|
|
||||||
group.feed_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'doc_supplier',
|
|
||||||
value:
|
|
||||||
group.doc_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rata_harga_doc',
|
|
||||||
value: formatCurrency(group.average_doc_price_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'hpp_telur',
|
|
||||||
value: formatCurrency(group.egg_hpp_rp_per_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nominal_sisa',
|
|
||||||
value: formatCurrency(group.egg_value_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions for PdfTable - Detail Per Kandang
|
|
||||||
const getDetailColumns = (): PdfColumn[] => [
|
|
||||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
|
||||||
{ key: 'kandang', header: 'Kandang', flex: 1.5, align: 'left' },
|
|
||||||
{ key: 'rentang_bw', header: 'Rentang BW', flex: 1, align: 'left' },
|
|
||||||
{
|
|
||||||
key: 'rata_rata_bobot',
|
|
||||||
header: 'Rata-Rata Bobot (Kg)',
|
|
||||||
flex: 1,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' },
|
|
||||||
{ key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' },
|
|
||||||
{ key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left' },
|
|
||||||
{ key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left' },
|
|
||||||
{
|
|
||||||
key: 'rata_harga_doc',
|
|
||||||
header: 'Rata-Rata Harga DOC',
|
|
||||||
flex: 1.2,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1, align: 'right' },
|
|
||||||
{ key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => {
|
|
||||||
return rows.map((item, index) => [
|
|
||||||
{ key: 'no', value: index + 1, align: 'center' },
|
|
||||||
{ key: 'kandang', value: item.kandang?.name || '-' },
|
|
||||||
{
|
|
||||||
key: 'rentang_bw',
|
|
||||||
value: `${item.weight_range.weight_min.toFixed(2)} - ${item.weight_range.weight_max.toFixed(2)}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rata_rata_bobot',
|
|
||||||
value: formatNumber(item.avg_weight_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sisa_butir',
|
|
||||||
value: formatNumber(item.egg_production_pieces),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sisa_kg',
|
|
||||||
value: formatNumber(item.egg_production_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'feed_supplier',
|
|
||||||
value:
|
|
||||||
item.feed_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'doc_supplier',
|
|
||||||
value:
|
|
||||||
item.doc_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rata_harga_doc',
|
|
||||||
value: formatCurrency(item.average_doc_price_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'hpp_telur',
|
|
||||||
value: formatCurrency(item.egg_hpp_rp_per_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nominal_sisa',
|
|
||||||
value: formatCurrency(item.egg_value_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDetailFooter = (
|
|
||||||
summary: HppPerKandangReport['summary']
|
|
||||||
): PdfTfootCell[] => {
|
|
||||||
if (!summary?.total) return [];
|
|
||||||
|
|
||||||
const allFeedSuppliers =
|
|
||||||
summary.total.feed_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-';
|
|
||||||
|
|
||||||
const allDocSuppliers =
|
|
||||||
summary.total.doc_suppliers
|
|
||||||
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
|
|
||||||
.join(' | ') || '-';
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ key: 'no', value: 'TOTAL' },
|
|
||||||
{ key: 'kandang', value: 'ALL' },
|
|
||||||
{ key: 'rentang_bw', value: '-' },
|
|
||||||
{
|
|
||||||
key: 'rata_rata_bobot',
|
|
||||||
value: formatNumber(summary.total.average_weight_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sisa_butir',
|
|
||||||
value: formatNumber(summary.total.total_egg_production_pieces),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'sisa_kg',
|
|
||||||
value: formatNumber(summary.total.total_egg_production_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{ key: 'feed_supplier', value: allFeedSuppliers },
|
|
||||||
{ key: 'doc_supplier', value: allDocSuppliers },
|
|
||||||
{
|
|
||||||
key: 'rata_harga_doc',
|
|
||||||
value: formatCurrency(summary.total.total_average_doc_price_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'hpp_telur',
|
|
||||||
value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'nominal_sisa',
|
|
||||||
value: formatCurrency(summary.total.total_egg_value_rp),
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPDFDocument = (
|
|
||||||
data: HppPerKandangExportParams['data'],
|
|
||||||
params: HppPerKandangExportParams['params']
|
|
||||||
) => {
|
|
||||||
const rekapitulasiByWeightRange = data.summary?.per_weight_range || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Document>
|
|
||||||
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
|
||||||
{/* Title and Parameters */}
|
|
||||||
<View style={pdfStyles.titleSection}>
|
|
||||||
<Text style={pdfStyles.mainTitle}>
|
|
||||||
Laporan > HPP Harian Kandang
|
|
||||||
</Text>
|
|
||||||
<View style={pdfStyles.parameterContainer}>
|
|
||||||
{getParameterText(params).map((param, index) => (
|
|
||||||
<View key={index} style={pdfStyles.parameterBadge}>
|
|
||||||
<Text>{param}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Rekapitulasi Section */}
|
|
||||||
<View style={pdfStyles.section}>
|
|
||||||
<Text style={pdfStyles.supplierTitle}>Rekapitulasi</Text>
|
|
||||||
<PdfTable
|
|
||||||
columns={getRekapitulasiColumns()}
|
|
||||||
data={getRekapitulasiData(rekapitulasiByWeightRange)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Detail Per Kandang Section */}
|
|
||||||
<View style={pdfStyles.section}>
|
|
||||||
<Text style={pdfStyles.supplierTitle}>Detail Per Kandang</Text>
|
|
||||||
<PdfTable
|
|
||||||
columns={getDetailColumns()}
|
|
||||||
data={getDetailData(data.rows)}
|
|
||||||
footer={data.summary ? getDetailFooter(data.summary) : undefined}
|
|
||||||
footerLabel='TOTAL'
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Page>
|
|
||||||
</Document>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const generateHppPerKandangPDF = async (
|
|
||||||
data: HppPerKandangExportParams['data'],
|
|
||||||
params: HppPerKandangExportParams['params']
|
|
||||||
): Promise<void> => {
|
|
||||||
const PDFDocument = createPDFDocument(data, params);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const blob = await pdf(PDFDocument).toBlob();
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
|
|
||||||
const period = params.period || formatDate(new Date(), 'YYYY-MM-DD');
|
|
||||||
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user