mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +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 = () => {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import ProductionResultContent from '@/components/pages/report/production-result
|
||||
|
||||
const ProductionResultReportPage = () => {
|
||||
return (
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<section className='w-full max-w-full'>
|
||||
<ProductionResultContent />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfParamBadgeProps = {
|
||||
children: React.ReactNode;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -16,9 +18,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => {
|
||||
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
||||
return (
|
||||
<View style={styles.parameterBadge}>
|
||||
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
||||
<Text>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfStatusBadgeProps = {
|
||||
children: React.ReactNode;
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
borderColor?: string;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -16,30 +15,38 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
statusBadgeText: {
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
color: '#333333',
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfStatusBadge = ({
|
||||
children,
|
||||
backgroundColor = '#F5F5F5',
|
||||
textColor = '#333333',
|
||||
borderColor = '#E5E7EB',
|
||||
}: PdfStatusBadgeProps) => {
|
||||
export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => {
|
||||
const styleRecord = style as Record<string, unknown>;
|
||||
const color = styleRecord?.color as string | undefined;
|
||||
|
||||
const viewStyle = Object.entries(styleRecord || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key !== 'color') {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
},
|
||||
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.statusBadgeText, { color: textColor }]}>
|
||||
<Text style={[styles.statusBadgeText, ...(color ? [{ color }] : [])]}>
|
||||
{children}
|
||||
</Text>
|
||||
</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';
|
||||
|
||||
import { View, StyleSheet } from '@react-pdf/renderer';
|
||||
import { PdfThead, PdfColumn } from './PdfThead';
|
||||
import { PdfTbody, PdfTbodyCell } from './PdfTbody';
|
||||
import { PdfTfoot, PdfTfootCell } from './PdfTfoot';
|
||||
import type { PdfColumn } from './types';
|
||||
import { PdfThead } from './PdfThead';
|
||||
import { PdfTbody } from './PdfTbody';
|
||||
import { PdfTfoot } from './PdfTfoot';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
table: {
|
||||
@@ -13,10 +14,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTableProps {
|
||||
columns: PdfColumn[];
|
||||
data: PdfTbodyCell[][];
|
||||
footer?: PdfTfootCell[];
|
||||
interface PdfTableProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
showFooter?: boolean;
|
||||
footerLabel?: string;
|
||||
firstRow?: {
|
||||
valueKey: string;
|
||||
@@ -26,20 +27,26 @@ interface PdfTableProps {
|
||||
};
|
||||
}
|
||||
|
||||
export const PdfTable = ({
|
||||
export const PdfTable = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
footer,
|
||||
showFooter = false,
|
||||
footerLabel = 'Total',
|
||||
firstRow,
|
||||
}: PdfTableProps) => {
|
||||
}: PdfTableProps<TData>) => {
|
||||
// Check if any column has footer defined
|
||||
const hasFooter =
|
||||
showFooter || columns.some((col) => col.footer !== undefined);
|
||||
|
||||
return (
|
||||
<View style={styles.table}>
|
||||
<PdfThead columns={columns} />
|
||||
<PdfTbody columns={columns} rows={data} firstRow={firstRow} />
|
||||
{footer && footer.length > 0 && (
|
||||
<PdfTfoot columns={columns} cells={footer} label={footerLabel} />
|
||||
<PdfThead columns={columns} data={data} />
|
||||
<PdfTbody columns={columns} data={data} firstRow={firstRow} />
|
||||
{hasFooter && data.length > 0 && (
|
||||
<PdfTfoot columns={columns} data={data} label={footerLabel} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export interface PdfTbodyCell {
|
||||
key: string;
|
||||
value: string | number | React.ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
color?: string;
|
||||
formatAs?: 'text' | 'date' | 'currency' | 'number';
|
||||
formatDate?: string;
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -71,21 +57,22 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTbodyProps {
|
||||
columns: PdfColumn[];
|
||||
rows: PdfTbodyCell[][];
|
||||
interface PdfTbodyProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
firstRow?: {
|
||||
valueKey: string;
|
||||
value: number;
|
||||
align?: 'right';
|
||||
color?: string;
|
||||
};
|
||||
formatDate?: (date: string, format: string) => string;
|
||||
formatNumber?: (num: number) => string;
|
||||
formatCurrency?: (num: number) => string;
|
||||
}
|
||||
|
||||
export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
export const PdfTbody = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
firstRow,
|
||||
}: PdfTbodyProps<TData>) => {
|
||||
return (
|
||||
<>
|
||||
{/* First Row */}
|
||||
@@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
<View style={[styles.tableRow, styles.tableBorderBottom]}>
|
||||
{columns.map((column, index) => {
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
const isfirstRowColumn = column.key === firstRow.valueKey;
|
||||
const align = column.align || 'center';
|
||||
const isFirstRowColumn = column.key === firstRow.valueKey;
|
||||
const align = column.align || 'left';
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [styles.tableCellNo, { flex: column.flex }]
|
||||
: isfirstRowColumn
|
||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||
: isFirstRowColumn
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
color: firstRow.color || 'black',
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellLast,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
]
|
||||
: [styles.tableCell, { flex: column.flex }];
|
||||
: [styles.tableCell, { flex: column.flex || 1 }];
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
<Text>{isfirstRowColumn ? firstRow.value : ''}</Text>
|
||||
<Text>{isFirstRowColumn ? firstRow.value : ''}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
)}
|
||||
|
||||
{/* Data Rows */}
|
||||
{rows.map((row, rowIndex) => {
|
||||
const isLastRow = rowIndex === rows.length - 1;
|
||||
{data.map((row, rowIndex) => {
|
||||
const isLastRow = rowIndex === data.length - 1;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -156,19 +143,27 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
]}
|
||||
>
|
||||
{columns.map((column, colIndex) => {
|
||||
const cell = row.find((c) => c.key === column.key);
|
||||
const isLastColumn = colIndex === columns.length - 1;
|
||||
const align = cell?.align || column.align || 'center';
|
||||
const align = column.align || 'left';
|
||||
|
||||
// Get cell content from column.cell function or fallback to row value
|
||||
let cellContent: ReactNode;
|
||||
if (column.cell) {
|
||||
cellContent = column.cell({ row, index: rowIndex });
|
||||
} else {
|
||||
cellContent =
|
||||
((row as Record<string, unknown>)[column.key] as ReactNode) ??
|
||||
'-';
|
||||
}
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [styles.tableCellNo, { flex: column.flex }]
|
||||
? [styles.tableCellNo, { flex: column.flex || 1 }]
|
||||
: align === 'right'
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
@@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: isLastColumn
|
||||
? [
|
||||
styles.tableCellLast,
|
||||
{ flex: column.flex, borderRightWidth: 0 },
|
||||
{ flex: column.flex || 1, borderRightWidth: 0 },
|
||||
]
|
||||
: [
|
||||
styles.tableCell,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cell?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
{cell?.value !== undefined &&
|
||||
cell?.value !== null &&
|
||||
cell?.value !== '' ? (
|
||||
typeof cell.value === 'object' ? (
|
||||
cell.value
|
||||
) : (
|
||||
<Text>{String(cell.value)}</Text>
|
||||
)
|
||||
{typeof cellContent === 'string' ||
|
||||
typeof cellContent === 'number' ? (
|
||||
<Text>{String(cellContent)}</Text>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
cellContent
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
@@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
export interface PdfTfootCell {
|
||||
key: string;
|
||||
value: string | number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
flex?: number;
|
||||
color?: string;
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -69,63 +56,86 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTfootProps {
|
||||
columns: PdfColumn[];
|
||||
cells: PdfTfootCell[];
|
||||
interface PdfTfootProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data: TData[];
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const PdfTfoot = ({
|
||||
export const PdfTfoot = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
cells,
|
||||
data,
|
||||
label = 'Total',
|
||||
}: PdfTfootProps) => {
|
||||
}: PdfTfootProps<TData>) => {
|
||||
return (
|
||||
<View style={[styles.tableRow, styles.summaryRow]}>
|
||||
{columns.map((column, index) => {
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
const cellData = cells.find((c) => c.key === column.key);
|
||||
|
||||
// Get footer content from column definition
|
||||
let footerContent: ReactNode;
|
||||
if (typeof column.footer === 'function') {
|
||||
footerContent = column.footer(data);
|
||||
} else {
|
||||
footerContent = column.footer;
|
||||
}
|
||||
|
||||
// Use label for first column (usually 'no' column)
|
||||
const displayContent = column.key === 'no' ? label : footerContent;
|
||||
|
||||
// Determine alignment
|
||||
const align = column.footerAlign || column.align || 'left';
|
||||
const color = column.footerColor || 'black';
|
||||
|
||||
const cellStyle =
|
||||
column.key === 'no'
|
||||
? [
|
||||
styles.tableCellNo,
|
||||
{ flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 },
|
||||
{
|
||||
flex: column.flex || 1,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
color,
|
||||
},
|
||||
]
|
||||
: cellData?.align === 'right'
|
||||
: align === 'right'
|
||||
? [
|
||||
styles.tableCellRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
color,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: cellData?.align === 'center'
|
||||
: align === 'center'
|
||||
? [
|
||||
styles.tableCellCenter,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
flex: column.flex || 1,
|
||||
color,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
]
|
||||
: isLastColumn
|
||||
? [styles.tableCellLast, { flex: column.flex }]
|
||||
: [
|
||||
styles.tableCell,
|
||||
{
|
||||
flex: column.flex,
|
||||
color: cellData?.color || 'black',
|
||||
},
|
||||
];
|
||||
? [styles.tableCellLast, { flex: column.flex || 1, color }]
|
||||
: [styles.tableCell, { flex: column.flex || 1, color }];
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
|
||||
export interface PdfColumn {
|
||||
key: string;
|
||||
header: string;
|
||||
flex: number;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
import { ReactNode } from 'react';
|
||||
import type { PdfColumn } from './types';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tableRow: {
|
||||
@@ -48,23 +43,37 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
interface PdfTheadProps {
|
||||
columns: PdfColumn[];
|
||||
interface PdfTheadProps<TData = Record<string, unknown>> {
|
||||
columns: PdfColumn<TData>[];
|
||||
data?: TData[];
|
||||
}
|
||||
|
||||
export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
export const PdfThead = <TData = Record<string, unknown>,>({
|
||||
columns,
|
||||
data,
|
||||
}: PdfTheadProps<TData>) => {
|
||||
return (
|
||||
<View style={[styles.tableRow, styles.tableHeader]}>
|
||||
{columns.map((column, index) => {
|
||||
const align = column.align || 'center';
|
||||
const isLastColumn = index === columns.length - 1;
|
||||
|
||||
// Get header content from column definition
|
||||
let headerContent: ReactNode;
|
||||
if (typeof column.header === 'function') {
|
||||
headerContent = column.header(data || []);
|
||||
} else {
|
||||
headerContent = column.header || column.key;
|
||||
}
|
||||
|
||||
// Determine alignment - columns align right by default for numeric data
|
||||
const align = column.align || 'left';
|
||||
|
||||
const cellStyle =
|
||||
align === 'right'
|
||||
? [
|
||||
styles.tableCellHeaderRight,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
textAlign: 'right' as const,
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
: [
|
||||
styles.tableCellHeader,
|
||||
{
|
||||
flex: column.flex,
|
||||
flex: column.flex || 1,
|
||||
textAlign: align as 'left' | 'center' | 'right',
|
||||
borderRightWidth: isLastColumn ? 0 : 1,
|
||||
},
|
||||
@@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => {
|
||||
|
||||
return (
|
||||
<View key={column.key} style={cellStyle}>
|
||||
<Text>{column.header}</Text>
|
||||
{typeof headerContent === 'string' ? (
|
||||
<Text>{headerContent}</Text>
|
||||
) : (
|
||||
headerContent
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export type { PdfColumn };
|
||||
|
||||
@@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable';
|
||||
export { PdfThead } from './PdfThead';
|
||||
export { PdfTbody } from './PdfTbody';
|
||||
export { PdfTfoot } from './PdfTfoot';
|
||||
export type { PdfColumn } from './PdfThead';
|
||||
export type { PdfTbodyCell } from './PdfTbody';
|
||||
export type { PdfTfootCell } from './PdfTfoot';
|
||||
export type { PdfColumn } from './types';
|
||||
|
||||
@@ -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 { Text, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label';
|
||||
|
||||
@@ -10,7 +11,7 @@ type PdfTypographyProps = {
|
||||
size?: TypographySize;
|
||||
variant?: TypographyVariant;
|
||||
color?: string;
|
||||
marginBottom?: number;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -66,17 +67,13 @@ export const PdfTypography = ({
|
||||
size = 'p',
|
||||
variant = 'default',
|
||||
color,
|
||||
marginBottom,
|
||||
style,
|
||||
}: PdfTypographyProps) => {
|
||||
const sizeStyle = styles[size];
|
||||
const textColor = color || variantColors[variant];
|
||||
|
||||
const customStyle = {
|
||||
...(marginBottom !== undefined && { marginBottom }),
|
||||
};
|
||||
|
||||
return (
|
||||
<Text style={[sizeStyle, { color: textColor }, customStyle]}>
|
||||
<Text style={[sizeStyle, { color: textColor }, ...(style ? [style] : [])]}>
|
||||
{children}
|
||||
</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,
|
||||
Font,
|
||||
pdf,
|
||||
Text,
|
||||
} from '@react-pdf/renderer';
|
||||
|
||||
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { CustomerPaymentReport } from '@/types/api/report/customer-payment';
|
||||
import {
|
||||
PdfTable,
|
||||
PdfColumn,
|
||||
PdfTbodyCell,
|
||||
PdfTfootCell,
|
||||
} from '@/components/helper/pdf/table';
|
||||
formatDate,
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatTitleCase,
|
||||
} 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 { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
@@ -55,161 +57,183 @@ interface CustomerPaymentExportPDFParams {
|
||||
};
|
||||
}
|
||||
|
||||
const getTableColumns = (): PdfColumn[] => [
|
||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
||||
{ key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' },
|
||||
const getTableColumns = (
|
||||
summary?: CustomerPaymentReport['summary']
|
||||
): 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',
|
||||
header: 'Tanggal Realisasi',
|
||||
flex: 1.2,
|
||||
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: 'vehicle_numbers', header: 'No Polisi', flex: 1.2, align: 'left' },
|
||||
{ key: 'qty', header: 'Qty', flex: 0.8, align: 'right' },
|
||||
{ key: 'weight', header: 'Berat', flex: 1, align: 'right' },
|
||||
{ key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' },
|
||||
{ key: 'unit_price', header: 'Harga/Unit', flex: 1.2, align: 'right' },
|
||||
{ key: 'final_price', header: 'Harga Akhir', flex: 1.2, align: 'right' },
|
||||
{ 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: 'status', header: 'Keterangan', flex: 1.5, align: 'center' },
|
||||
{ key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' },
|
||||
{ key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' },
|
||||
];
|
||||
|
||||
const getTableData = (
|
||||
rows: CustomerPaymentReport['rows']
|
||||
): PdfTbodyCell[][] => {
|
||||
return rows.map((item, index) => [
|
||||
{ key: 'no', value: index + 1 },
|
||||
{
|
||||
key: 'trans_date',
|
||||
value: item.trans_date ? formatDate(item.trans_date, 'DD MMM YY') : '-',
|
||||
},
|
||||
{
|
||||
key: 'delivery_date',
|
||||
value: item.delivery_date
|
||||
? formatDate(item.delivery_date, 'DD MMM YY')
|
||||
{
|
||||
key: 'aging',
|
||||
header: 'Aging',
|
||||
flex: 0.8,
|
||||
align: 'center',
|
||||
cell: ({ row }) =>
|
||||
row.aging_day != null ? `${formatNumber(row.aging_day)} hari` : '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'reference',
|
||||
header: 'Referensi',
|
||||
flex: 1.5,
|
||||
align: 'left',
|
||||
cell: ({ row }) => row.reference || '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'vehicle_numbers',
|
||||
header: 'No Polisi',
|
||||
flex: 1.2,
|
||||
align: 'left',
|
||||
cell: ({ row }) =>
|
||||
Array.isArray(row.vehicle_numbers) && row.vehicle_numbers.length > 0
|
||||
? row.vehicle_numbers.join(', ')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
key: 'aging',
|
||||
value:
|
||||
item.aging_day != null ? `${formatNumber(item.aging_day)} hari` : '-',
|
||||
},
|
||||
{ key: 'reference', value: item.reference || '-' },
|
||||
{
|
||||
key: 'vehicle_numbers',
|
||||
value:
|
||||
Array.isArray(item.vehicle_numbers) && item.vehicle_numbers.length > 0
|
||||
? item.vehicle_numbers.join(', ')
|
||||
: '-',
|
||||
},
|
||||
{ key: 'qty', value: formatNumber(item.qty), align: 'right' },
|
||||
{ key: 'weight', value: formatNumber(item.weight), align: 'right' },
|
||||
{
|
||||
key: 'average_weight',
|
||||
value: formatNumber(item.average_weight),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'unit_price',
|
||||
value: formatCurrency(item.unit_price),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'final_price',
|
||||
value: formatCurrency(item.final_price),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'total_price',
|
||||
value: formatCurrency(item.total_price),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'payment_amount',
|
||||
value: formatCurrency(item.payment_amount),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'accounts_receivable',
|
||||
value: formatCurrency(item.accounts_receivable),
|
||||
align: 'right',
|
||||
color: item.accounts_receivable < 0 ? '#DC2626' : undefined,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
value: item.status ? (
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'qty',
|
||||
header: 'Qty',
|
||||
flex: 0.8,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatNumber(row.qty),
|
||||
footer: summary ? formatNumber(summary.total_qty || 0) : '',
|
||||
footerAlign: 'right',
|
||||
},
|
||||
{
|
||||
key: 'weight',
|
||||
header: 'Berat',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatNumber(row.weight),
|
||||
footer: summary ? formatNumber(summary.total_weight || 0) : '',
|
||||
footerAlign: 'right',
|
||||
},
|
||||
{
|
||||
key: 'average_weight',
|
||||
header: 'Rata-Rata',
|
||||
flex: 0.8,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatNumber(row.average_weight),
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'unit_price',
|
||||
header: 'Harga/Unit (Rp)',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatCurrency(row.unit_price),
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'final_price',
|
||||
header: 'Harga Akhir (Rp)',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => formatCurrency(row.final_price),
|
||||
footer: summary ? formatCurrency(summary.total_final_amount || 0) : '',
|
||||
footerAlign: 'right',
|
||||
},
|
||||
{
|
||||
key: 'total_price',
|
||||
header: 'Total (Rp)',
|
||||
flex: 1.2,
|
||||
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' }}>
|
||||
<PdfStatusBadge
|
||||
backgroundColor={item.status === 'LUNAS' ? '#DBEAFE' : '#FEE2E2'}
|
||||
textColor={item.status === 'LUNAS' ? '#1E40AF' : '#991B1B'}
|
||||
borderColor={item.status === 'LUNAS' ? '#60A5FA' : '#F87171'}
|
||||
style={{
|
||||
backgroundColor: getPDFBadgeStyle(row.status, 'payment').bg,
|
||||
color: getPDFBadgeStyle(row.status, 'payment').text,
|
||||
borderColor: getPDFBadgeStyle(row.status, 'payment').border,
|
||||
}}
|
||||
>
|
||||
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
|
||||
{formatTitleCase(row.status)}
|
||||
</PdfStatusBadge>
|
||||
</View>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
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',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'total_price',
|
||||
value: formatCurrency(summary?.total_grand_amount || 0),
|
||||
align: 'right',
|
||||
key: 'pickup_info',
|
||||
header: 'Pengambilan',
|
||||
flex: 1,
|
||||
align: 'left',
|
||||
cell: ({ row }) =>
|
||||
Array.isArray(row.pickup_info) && row.pickup_info.length > 0
|
||||
? row.pickup_info.join(', ')
|
||||
: '-',
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
key: 'payment_amount',
|
||||
value: formatCurrency(summary?.total_payment || 0),
|
||||
align: 'right',
|
||||
key: 'sales_person',
|
||||
header: 'Sales',
|
||||
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) => {
|
||||
@@ -259,13 +283,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
|
||||
{/* Table */}
|
||||
<PdfTable
|
||||
columns={getTableColumns()}
|
||||
data={getTableData(customerReport.rows)}
|
||||
footer={
|
||||
customerReport.summary
|
||||
? getTableFooter(customerReport.summary)
|
||||
: undefined
|
||||
}
|
||||
columns={getTableColumns(customerReport.summary)}
|
||||
data={customerReport.rows}
|
||||
showFooter={!!customerReport.summary}
|
||||
firstRow={
|
||||
typeof customerReport.initial_balance === 'number' &&
|
||||
customerReport.initial_balance !== 0
|
||||
@@ -273,8 +293,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
valueKey: 'accounts_receivable',
|
||||
value: customerReport.initial_balance,
|
||||
align: 'right',
|
||||
color:
|
||||
customerReport.initial_balance < 0 ? '#DC2626' : 'black',
|
||||
color: customerReport.initial_balance < 0 ? 'red' : 'black',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ export const generateCustomerPaymentExcel = async (
|
||||
{ header: 'Ekor/Qty', key: 'qty', width: 10 },
|
||||
{ header: 'Berat (Kg)', key: 'weight', width: 12 },
|
||||
{ header: 'AVG', key: 'avgWeight', width: 10 },
|
||||
{ header: 'Harga/Unit', key: 'unitPrice', width: 15 },
|
||||
{ header: 'Harga Akhir', key: 'finalPrice', width: 15 },
|
||||
{ header: 'Total', key: 'totalPrice', width: 15 },
|
||||
{ header: 'Pembayaran', key: 'paymentAmount', width: 15 },
|
||||
{ header: 'Saldo Piutang', key: 'accountsReceivable', width: 15 },
|
||||
{ header: 'Harga/Unit (Rp)', key: 'unitPrice', width: 15 },
|
||||
{ header: 'Harga Akhir (Rp)', key: 'finalPrice', width: 15 },
|
||||
{ header: 'Total (Rp)', key: 'totalPrice', width: 15 },
|
||||
{ header: 'Pembayaran (Rp)', key: 'paymentAmount', width: 15 },
|
||||
{ header: 'Saldo Piutang (Rp)', key: 'accountsReceivable', width: 15 },
|
||||
{ header: 'Keterangan', key: 'status', width: 20 },
|
||||
{ header: 'Pengambilan', key: 'pickupInfo', width: 15 },
|
||||
{ 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 { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||
import {
|
||||
PdfTable,
|
||||
PdfColumn,
|
||||
PdfTbodyCell,
|
||||
PdfTfootCell,
|
||||
} from '@/components/helper/pdf/table';
|
||||
import { PdfTable, PdfColumn } from '@/components/helper/pdf/table';
|
||||
import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge';
|
||||
import { Text } from '@react-pdf/renderer';
|
||||
|
||||
Font.register({
|
||||
family: '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({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
@@ -84,153 +40,225 @@ const pdfStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const getTableColumns = (): PdfColumn[] => [
|
||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
||||
{ key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' },
|
||||
{ key: 'po_number', header: 'No. PO', flex: 1, align: 'left' },
|
||||
{
|
||||
key: 'received_date',
|
||||
header: 'Tgl Terima/Bayar',
|
||||
flex: 0.7,
|
||||
align: 'center',
|
||||
},
|
||||
{ key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' },
|
||||
{ key: 'aging', header: 'Aging', flex: 0.6, align: 'center' },
|
||||
{ key: 'area', header: 'Area', flex: 1, align: 'left' },
|
||||
{ key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' },
|
||||
{ key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' },
|
||||
{ key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' },
|
||||
{
|
||||
key: 'total_price',
|
||||
header: 'Nominal Pembelian (Rp)',
|
||||
flex: 1.5,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'payment_price',
|
||||
header: 'Pembayaran (Rp)',
|
||||
flex: 1.5,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
key: 'balance',
|
||||
header: 'Sisa Saldo Hutang (Rp)',
|
||||
flex: 1.5,
|
||||
align: 'right',
|
||||
},
|
||||
{ key: 'status', header: 'Status', flex: 1.2, align: 'center' },
|
||||
{ key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' },
|
||||
];
|
||||
const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => {
|
||||
type DebtRow = DebtSupplier['rows'][number];
|
||||
|
||||
const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => {
|
||||
return rows.map((item, index) => [
|
||||
{ key: 'no', value: index + 1 },
|
||||
{ key: 'pr_number', value: item.pr_number || '-' },
|
||||
{ key: 'po_number', value: item.po_number || '-' },
|
||||
return [
|
||||
{
|
||||
key: 'no',
|
||||
header: 'No',
|
||||
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',
|
||||
value: item.received_date
|
||||
? formatDate(item.received_date, 'DD MMM YY')
|
||||
: '-',
|
||||
header: 'Tgl Terima/Bayar',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
value:
|
||||
item.due_status && item.due_status !== '-' ? (
|
||||
<PdfStatusBadge
|
||||
backgroundColor={getPDFBadgeStyle(item.due_status, 'due').bg}
|
||||
textColor={getPDFBadgeStyle(item.due_status, 'due').text}
|
||||
borderColor={getPDFBadgeStyle(item.due_status, 'due').border}
|
||||
>
|
||||
{item.due_status}
|
||||
</PdfStatusBadge>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'total_price',
|
||||
value: formatCurrency(item.total_price),
|
||||
align: 'right',
|
||||
color: item.total_price < 0 ? 'red' : undefined,
|
||||
},
|
||||
{
|
||||
key: 'payment_price',
|
||||
value: formatCurrency(item.payment_price),
|
||||
align: 'right',
|
||||
color: item.payment_price < 0 ? 'red' : undefined,
|
||||
},
|
||||
{
|
||||
key: 'balance',
|
||||
value: formatCurrency(item.balance),
|
||||
align: 'right',
|
||||
color: item.balance < 0 ? 'red' : undefined,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
value:
|
||||
item.status && item.status !== '-' ? (
|
||||
header: 'Status Jatuh Tempo',
|
||||
flex: 2,
|
||||
align: 'center',
|
||||
cell: ({ row }) =>
|
||||
(row as unknown as DebtRow).due_status &&
|
||||
(row as unknown as DebtRow).due_status !== '-' ? (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<PdfStatusBadge
|
||||
backgroundColor={getPDFBadgeStyle(item.status, 'payment').bg}
|
||||
textColor={getPDFBadgeStyle(item.status, 'payment').text}
|
||||
borderColor={getPDFBadgeStyle(item.status, 'payment').border}
|
||||
style={{
|
||||
backgroundColor: getPDFBadgeStyle(
|
||||
(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>
|
||||
</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 {
|
||||
data: DebtSupplier[];
|
||||
params?: {
|
||||
@@ -296,13 +324,9 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
|
||||
{/* Table */}
|
||||
<PdfTable
|
||||
columns={getTableColumns()}
|
||||
data={getTableData(supplierReport.rows)}
|
||||
footer={
|
||||
supplierReport.total
|
||||
? getTableFooter(supplierReport.total)
|
||||
: undefined
|
||||
}
|
||||
columns={getTableColumns(supplierReport.total)}
|
||||
data={supplierReport.rows as unknown as Record<string, unknown>[]}
|
||||
showFooter={!!supplierReport.total}
|
||||
firstRow={
|
||||
typeof supplierReport.initial_balance === 'number' &&
|
||||
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 MenuItem from '@/components/menu/MenuItem';
|
||||
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 * as XLSX from 'xlsx';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const PurchasesPerSupplierTab = () => {
|
||||
@@ -355,98 +355,14 @@ const PurchasesPerSupplierTab = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
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);
|
||||
await generatePurchasesPerSupplierExcel({ data: allDataForExport });
|
||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
logisticPurchasePerSupplierExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
supplierOptions,
|
||||
]);
|
||||
}, [logisticPurchasePerSupplierExport]);
|
||||
|
||||
const handleExportPdf = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
@@ -517,7 +433,10 @@ const PurchasesPerSupplierTab = () => {
|
||||
end_date: tableFilterState.end_date || '',
|
||||
};
|
||||
|
||||
await generatePurchasesPerSupplierPDF(allDataForExport, exportParams);
|
||||
await generatePurchasesPerSupplierPDF({
|
||||
data: allDataForExport,
|
||||
params: exportParams,
|
||||
});
|
||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
import { JSX, useState } from 'react';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent';
|
||||
import HppPerKandangTab from './sale/tab/HppPerKandangTab';
|
||||
import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent';
|
||||
import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab';
|
||||
|
||||
type MarketingReportTabType =
|
||||
| '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';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
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 DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF';
|
||||
import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF';
|
||||
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import {
|
||||
+26
-135
@@ -26,9 +26,9 @@ import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
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 * as XLSX from 'xlsx';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
const HppPerKandangTab = () => {
|
||||
@@ -346,136 +346,18 @@ const HppPerKandangTab = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const allExportData =
|
||||
allDataForExport.rows as HppPerKandangReport['rows'];
|
||||
|
||||
const perWeightRangeSummary =
|
||||
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,
|
||||
await generateHppPerKandangExcel({
|
||||
data: allDataForExport,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers,
|
||||
});
|
||||
|
||||
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.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
||||
} finally {
|
||||
setIsExcelExportLoading(false);
|
||||
}
|
||||
}, [
|
||||
hppPerKandangExport,
|
||||
tableFilterState,
|
||||
areaOptions,
|
||||
locationOptions,
|
||||
kandangOptions,
|
||||
]);
|
||||
}, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]);
|
||||
|
||||
const handleExportPDF = useCallback(async () => {
|
||||
setIsPdfExportLoading(true);
|
||||
@@ -524,16 +406,23 @@ const HppPerKandangTab = () => {
|
||||
.join(', ') || 'Semua Kandang'
|
||||
: 'Semua Kandang';
|
||||
|
||||
await generateHppPerKandangPDF(allDataForExport, {
|
||||
area_name: areaName,
|
||||
location_name: locationName,
|
||||
kandang_name: kandangName,
|
||||
period: tableFilterState.period,
|
||||
weight_min: tableFilterState.weight_min,
|
||||
weight_max: tableFilterState.weight_max,
|
||||
show_unrecorded: tableFilterState.show_unrecorded.toString(),
|
||||
sort_by: tableFilterState.sort_by,
|
||||
});
|
||||
await generateHppPerKandangPDF(
|
||||
{
|
||||
data: allDataForExport,
|
||||
params: {
|
||||
area_name: areaName,
|
||||
location_name: locationName,
|
||||
kandang_name: kandangName,
|
||||
period: tableFilterState.period,
|
||||
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.');
|
||||
} catch {
|
||||
@@ -547,6 +436,8 @@ const HppPerKandangTab = () => {
|
||||
areaOptions,
|
||||
locationOptions,
|
||||
kandangOptions,
|
||||
allFeedSuppliers,
|
||||
allDocSuppliers,
|
||||
]);
|
||||
|
||||
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {
|
||||
@@ -1,18 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Image,
|
||||
} from '@react-pdf/renderer';
|
||||
import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
|
||||
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
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 = {
|
||||
projectFlockKandang: BaseProjectFlockKandang;
|
||||
@@ -25,132 +22,28 @@ interface ProductionResultReportPDFProps {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
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,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
companyName: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
companyAddress: {
|
||||
fontSize: 8,
|
||||
maxWidth: 420,
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
doubleDivider: {
|
||||
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',
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
position: 'absolute',
|
||||
fontSize: 8,
|
||||
bottom: 22,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: 'grey',
|
||||
flexWrap: 'wrap',
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
section: {
|
||||
marginTop: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000',
|
||||
padding: 8,
|
||||
tableSection: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
|
||||
sectionHeader: {
|
||||
marginBottom: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
},
|
||||
sectionTitle: {
|
||||
tableTitle: {
|
||||
fontSize: 10,
|
||||
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: {
|
||||
fontSize: 8,
|
||||
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) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
if (typeof v === 'number') return formatNumber(v);
|
||||
return String(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render label/value table for one ProductionResult.
|
||||
* Uses a compact grid to keep page readable.
|
||||
*/
|
||||
function ProductionResultGrid({ pr }: { pr: ProductionResult }) {
|
||||
const rows: Array<[string, string]> = [
|
||||
['WOA', valueText(pr.woa)],
|
||||
// ========================================
|
||||
// TABLE 1: WOA & BW
|
||||
// ========================================
|
||||
const getBwTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
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)],
|
||||
['Std BW', valueText(pr.std_bw)],
|
||||
['Uniformity', valueText(pr.uniformity)],
|
||||
['Std Uniformity', valueText(pr.std_uniformity)],
|
||||
// ========================================
|
||||
// TABLE 2: DEPLESI
|
||||
// ========================================
|
||||
const getDepTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
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)],
|
||||
['Dep Std', valueText(pr.dep_std)],
|
||||
// ========================================
|
||||
// TABLE 3: BUTIRAN
|
||||
// ========================================
|
||||
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)],
|
||||
['Butiran Putih', valueText(pr.butiran_putih)],
|
||||
['Butiran Retak', valueText(pr.butiran_retak)],
|
||||
['Butiran Pecah', valueText(pr.butiran_pecah)],
|
||||
['Butiran Jumlah', valueText(pr.butiran_jumlah)],
|
||||
['Total Butir', valueText(pr.total_butir)],
|
||||
// ========================================
|
||||
// TABLE 4: BERAT (KG)
|
||||
// ========================================
|
||||
const getKgTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
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)],
|
||||
['Kg Putih', valueText(pr.kg_putih)],
|
||||
['Kg Retak', valueText(pr.kg_retak)],
|
||||
['Kg Pecah', valueText(pr.kg_pecah)],
|
||||
['Kg Jumlah', valueText(pr.kg_jumlah)],
|
||||
['Total Kg', valueText(pr.total_kg)],
|
||||
// ========================================
|
||||
// TABLE 5: PERSENTASE
|
||||
// ========================================
|
||||
const getPersenTableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
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)],
|
||||
['% Putih', valueText(pr.persen_putih)],
|
||||
['% Retak', valueText(pr.persen_retak)],
|
||||
['% Pecah', valueText(pr.persen_pecah)],
|
||||
// ========================================
|
||||
// TABLE 6: PRODUKSI (HD, FI, EM, EW)
|
||||
// ========================================
|
||||
const getProduksi1TableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
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)],
|
||||
['HD Std', valueText(pr.hd_std)],
|
||||
['FI', valueText(pr.fi)],
|
||||
['FI Std', valueText(pr.fi_std)],
|
||||
['EM', valueText(pr.em)],
|
||||
['EM Std', valueText(pr.em_std)],
|
||||
['EW', valueText(pr.ew)],
|
||||
['EW Std', valueText(pr.ew_std)],
|
||||
['FCR', valueText(pr.fcr)],
|
||||
['FCR Std', valueText(pr.fcr_std)],
|
||||
['HH', valueText(pr.hh)],
|
||||
['HH Std', valueText(pr.hh_std)],
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.grid}>
|
||||
{rows.map(([label, value], idx) => {
|
||||
const isLast = idx === rows.length - 1;
|
||||
return (
|
||||
<View
|
||||
key={label}
|
||||
style={[styles.gridRow, ...(isLast ? [styles.gridRowLast] : [])]}
|
||||
>
|
||||
<Text style={styles.gridCellLabel}>{label}</Text>
|
||||
<Text style={styles.gridCellValue}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are multiple ProductionResult entries for a kandang,
|
||||
* we show them sequentially with a small header per result.
|
||||
*
|
||||
* You can later change this to render only the latest WOA, or group by week.
|
||||
*/
|
||||
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>
|
||||
);
|
||||
}
|
||||
// ========================================
|
||||
// TABLE 7: PRODUKSI (FCR, HH)
|
||||
// ========================================
|
||||
const getProduksi2TableColumns = (): PdfColumn<ProductionResult>[] => [
|
||||
{
|
||||
key: 'no',
|
||||
header: 'No',
|
||||
flex: 0.5,
|
||||
align: 'center',
|
||||
cell: ({ row, index }) => index + 1,
|
||||
},
|
||||
{
|
||||
key: 'fcr',
|
||||
header: 'FCR',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.fcr),
|
||||
},
|
||||
{
|
||||
key: 'fcr_std',
|
||||
header: 'FCR Std',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.fcr_std),
|
||||
},
|
||||
{
|
||||
key: 'hh',
|
||||
header: 'Hen House',
|
||||
flex: 1,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.hh),
|
||||
},
|
||||
{
|
||||
key: 'hh_std',
|
||||
header: 'Hen House Std',
|
||||
flex: 1.2,
|
||||
align: 'right',
|
||||
cell: ({ row }) => valueText(row.hh_std),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* ✅ Main PDF Component
|
||||
@@ -297,90 +401,148 @@ const ProductionResultReportPDF = ({
|
||||
}: ProductionResultReportPDFProps) => {
|
||||
return (
|
||||
<Document>
|
||||
<Page style={styles.page} size='A4'>
|
||||
{/* Header */}
|
||||
<View>
|
||||
<View style={styles.companyInfoHeader}>
|
||||
<Image style={styles.companyLogo} src='/assets/img/lti-logo.png' />
|
||||
<Text style={styles.companyInfoHeaderDate}>
|
||||
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||
</Text>
|
||||
{mappedProductionResults.length === 0 ? (
|
||||
<Page style={styles.page} size='A4'>
|
||||
{/* Title and Parameters */}
|
||||
<View style={styles.titleSection}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > Production Result
|
||||
</PdfTypography>
|
||||
<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>
|
||||
<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 }}>
|
||||
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
||||
</View>
|
||||
) : (
|
||||
mappedProductionResults.map((item, idx) => {
|
||||
const pfk = item.projectFlockKandang;
|
||||
|
||||
// Try to display meaningful identifiers.
|
||||
// Adjust these fields based on your real BaseProjectFlockKandang structure.
|
||||
const kandangName =
|
||||
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
||||
<PdfPageNumber />
|
||||
</Page>
|
||||
) : (
|
||||
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 (
|
||||
<View
|
||||
key={`pfk-${pfk?.id ?? idx}`}
|
||||
style={styles.section}
|
||||
break={idx > 0} // each kandang starts on a new page for clarity
|
||||
>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>
|
||||
{projectName
|
||||
? `${projectName} • ${kandangName}`
|
||||
: kandangName}
|
||||
</Text>
|
||||
<Text style={styles.sectionSubtitle}>
|
||||
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||
</Text>
|
||||
const areaName = pfk?.project_flock?.area?.name ?? '';
|
||||
|
||||
const hasData =
|
||||
item.productionResult && item.productionResult.length > 0;
|
||||
|
||||
return (
|
||||
<Page key={`pfk-${pfk?.id ?? idx}`} style={styles.page} size='A4'>
|
||||
{/* Title and Parameters */}
|
||||
<View style={styles.titleSection}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > Production Result
|
||||
</PdfTypography>
|
||||
<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>
|
||||
|
||||
{item.productionResult && item.productionResult.length > 0 ? (
|
||||
<ProductionResultList
|
||||
productionResults={item.productionResult}
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.emptyText}>
|
||||
Tidak ada production result untuk kandang ini.
|
||||
</Text>
|
||||
)}
|
||||
<PdfTypography size='h2' variant='primary'>
|
||||
{projectName
|
||||
? `${projectName} • ${kandangName}`
|
||||
: kandangName}
|
||||
</PdfTypography>
|
||||
<PdfTypography size='label'>
|
||||
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||
</PdfTypography>
|
||||
</View>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer} fixed>
|
||||
<Text
|
||||
render={({ pageNumber, totalPages }) =>
|
||||
`${pageNumber} / ${totalPages}`
|
||||
}
|
||||
fixed
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
{hasData ? (
|
||||
<>
|
||||
{/* Table 1: WOA & BW */}
|
||||
<View style={styles.tableSection}>
|
||||
<Text style={styles.tableTitle}>1. WOA & Body Weight</Text>
|
||||
<PdfTable
|
||||
columns={getBwTableColumns()}
|
||||
data={item.productionResult!}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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