mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into 'staging'
Development See merge request mbugroup/lti-web-client!323
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
const AddFcr = () => {
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
<FcrForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddFcr;
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||
|
||||
const FcrEdit = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const fcrId = searchParams.get('fcrId');
|
||||
|
||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||
fcrId,
|
||||
(id: number) =>
|
||||
FcrApi.getSingle(id) as Promise<
|
||||
BaseApiResponse<FcrWithStandards> | undefined
|
||||
>
|
||||
);
|
||||
|
||||
if (!fcrId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||
<FcrForm type='edit' initialValues={fcr.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrEdit;
|
||||
@@ -1,11 +0,0 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm';
|
||||
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { FcrWithStandards } from '@/types/api/master-data/fcr';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
const FcrDetail = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const fcrId = searchParams.get('fcrId');
|
||||
|
||||
const { data: fcr, isLoading: isLoadingFcr } = useSWR(
|
||||
fcrId,
|
||||
(id: number) =>
|
||||
FcrApi.getSingle(id) as Promise<
|
||||
BaseApiResponse<FcrWithStandards> | undefined
|
||||
>
|
||||
);
|
||||
|
||||
if (!fcrId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingFcr && (!fcr || isResponseError(fcr))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
{isLoadingFcr && <span className='loading loading-spinner loading-xl' />}
|
||||
{!isLoadingFcr && isResponseSuccess(fcr) && (
|
||||
<FcrForm type='detail' initialValues={fcr.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrDetail;
|
||||
@@ -1,11 +0,0 @@
|
||||
import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable';
|
||||
|
||||
const Fcr = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<FcrsTable />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fcr;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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({
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => {
|
||||
return (
|
||||
<View style={[styles.parameterBadge, ...(style ? [style] : [])]}>
|
||||
<Text>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Text, View, StyleSheet } from '@react-pdf/renderer';
|
||||
import type { Style } from '@react-pdf/types';
|
||||
|
||||
type PdfStatusBadgeProps = {
|
||||
children: React.ReactNode;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
statusBadge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 12,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
statusBadgeText: {
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
color: '#333333',
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
...(Object.keys(viewStyle).length > 0 ? [viewStyle as Style] : []),
|
||||
]}
|
||||
>
|
||||
<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 };
|
||||
@@ -0,0 +1,80 @@
|
||||
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';
|
||||
|
||||
type TypographyVariant = Color | 'default';
|
||||
|
||||
type PdfTypographyProps = {
|
||||
children: React.ReactNode;
|
||||
size?: TypographySize;
|
||||
variant?: TypographyVariant;
|
||||
color?: string;
|
||||
style?: Style;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
h1: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
},
|
||||
h2: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
h3: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
h4: {
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 3,
|
||||
},
|
||||
p: {
|
||||
fontSize: 10,
|
||||
marginBottom: 4,
|
||||
},
|
||||
small: {
|
||||
fontSize: 8,
|
||||
marginBottom: 2,
|
||||
},
|
||||
label: {
|
||||
fontSize: 9,
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const variantColors: Record<TypographyVariant, string> = {
|
||||
default: '#333333',
|
||||
primary: '#1f74bf',
|
||||
secondary: '#6B7280',
|
||||
accent: '#8B5CF6',
|
||||
neutral: '#6B7280',
|
||||
info: '#3B82F6',
|
||||
success: '#065F46',
|
||||
warning: '#92400E',
|
||||
error: '#DC2626',
|
||||
none: '#333333',
|
||||
};
|
||||
|
||||
export const PdfTypography = ({
|
||||
children,
|
||||
size = 'p',
|
||||
variant = 'default',
|
||||
color,
|
||||
style,
|
||||
}: PdfTypographyProps) => {
|
||||
const sizeStyle = styles[size];
|
||||
const textColor = color || variantColors[variant];
|
||||
|
||||
return (
|
||||
<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;
|
||||
};
|
||||
@@ -246,8 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||
'bg-gray-50 border-base-content/10': isDisabled,
|
||||
'bg-base-100 border-base-content/10': !isDisabled,
|
||||
'bg-base-200 border-base-content/10': isDisabled,
|
||||
'border-error': isError,
|
||||
},
|
||||
className?.inputPrefix
|
||||
@@ -278,15 +278,16 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn('w-full flex-1', className?.select)}
|
||||
classNames={{
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
cn('w-full border bg-white transition-shadow', 'rounded-lg!', {
|
||||
cn('w-full border transition-shadow', 'rounded-lg!', {
|
||||
'bg-base-100!': !isDisabled && !readOnly,
|
||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-red-500! ring-2 ring-red-200': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
'rounded-l-none!': inputPrefix && !startAdornment,
|
||||
'rounded-r-none!': inputSuffix && !startAdornment,
|
||||
}),
|
||||
@@ -370,8 +371,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !isDisabled,
|
||||
'bg-gray-50 border-base-content/10': isDisabled,
|
||||
'bg-base-100 border-base-content/10': !isDisabled,
|
||||
'bg-base-200 border-base-content/10': isDisabled,
|
||||
'border-error': isError,
|
||||
},
|
||||
className?.inputSuffix
|
||||
@@ -404,18 +405,19 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
classNames={{
|
||||
control: ({ isFocused, isDisabled }) =>
|
||||
cn(
|
||||
'w-full border bg-white transition-shadow',
|
||||
'w-full border transition-shadow',
|
||||
// Gunakan rounded-lg untuk semua kasus
|
||||
'rounded-lg!',
|
||||
{
|
||||
'bg-base-100!': !isDisabled && !readOnly,
|
||||
'bg-base-200! text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
'cursor-pointer!': !readOnly && !isDisabled,
|
||||
'border-red-500! ring-2 ring-red-200': isError,
|
||||
'border-indigo-500 ring-2 ring-indigo-200':
|
||||
isFocused && !startAdornment,
|
||||
'border-base-content/10!': !isError && !isFocused,
|
||||
'bg-gray-100 text-gray-400 cursor-not-allowed':
|
||||
isDisabled && !readOnly,
|
||||
'bg-transparent! cursor-not-allowed!': readOnly,
|
||||
}
|
||||
),
|
||||
valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'),
|
||||
|
||||
@@ -104,8 +104,8 @@ const TextInput = ({
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !disabled,
|
||||
'bg-gray-50 border-base-content/10': disabled,
|
||||
'bg-base-100 border-base-content/10': !disabled,
|
||||
'bg-base-200 border-base-content/10': disabled,
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
@@ -118,7 +118,7 @@ const TextInput = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center bg-white border-base-content/10',
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 flex-1 rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
@@ -126,7 +126,8 @@ const TextInput = ({
|
||||
'rounded-r-none!': inputSuffix,
|
||||
'input-disabled': disabled,
|
||||
'cursor-not-allowed': disabled,
|
||||
'bg-gray-50': disabled,
|
||||
'bg-base-100': !disabled,
|
||||
'bg-base-200': disabled,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
@@ -167,8 +168,8 @@ const TextInput = ({
|
||||
className={cn(
|
||||
'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200',
|
||||
{
|
||||
'bg-gray-100 border-base-content/10': !disabled,
|
||||
'bg-gray-50 border-base-content/10': disabled,
|
||||
'bg-base-100 border-base-content/10': !disabled,
|
||||
'bg-base-200 border-base-content/10': disabled,
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
},
|
||||
@@ -182,10 +183,12 @@ const TextInput = ({
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 bg-white border-base-content/10',
|
||||
'input h-fit px-3 py-2.5 gap-1.5 text-sm font-normal leading-6 w-full rounded-lg! outline-none! transition-all duration-200 flex items-center border-base-content/10',
|
||||
{
|
||||
'border-error': isError,
|
||||
'border-success!': isValid,
|
||||
'bg-base-100': !disabled,
|
||||
'bg-base-200': disabled,
|
||||
},
|
||||
className?.inputWrapper
|
||||
)}
|
||||
@@ -201,7 +204,14 @@ const TextInput = ({
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
className={cn('grow', className?.input)}
|
||||
className={cn(
|
||||
'grow bg-transparent outline-none',
|
||||
{
|
||||
'cursor-not-allowed': disabled,
|
||||
'text-gray-500': disabled,
|
||||
},
|
||||
className?.input
|
||||
)}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
|
||||
|
||||
@@ -276,6 +276,13 @@ const ExpensesTable = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'reference_number',
|
||||
header: 'Nomor Referensi',
|
||||
cell: (props) => {
|
||||
return props.row.original.reference_number ?? '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'transaction_date',
|
||||
header: 'Tanggal Pengajuan',
|
||||
|
||||
@@ -59,6 +59,10 @@ const DeliveryOrderFormModal = ({
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add_delivery' ||
|
||||
modalAction === 'edit_delivery' ||
|
||||
@@ -107,6 +111,7 @@ const DeliveryOrderFormModal = ({
|
||||
const successModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -329,6 +334,33 @@ const DeliveryOrderFormModal = ({
|
||||
refreshApproval();
|
||||
};
|
||||
|
||||
const approveMarketingHandler = async (notes: string) => {
|
||||
if (!marketingId) {
|
||||
toast.error(`Tidak ada data yang valid untuk di approve.`);
|
||||
approveModal.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const approveMarketingRes = await SalesOrderApi.singleApproval(
|
||||
Number(marketingId),
|
||||
'APPROVED',
|
||||
notes
|
||||
);
|
||||
|
||||
if (isResponseSuccess(approveMarketingRes)) {
|
||||
approveModal.closeModal();
|
||||
toast.success(approveMarketingRes?.message as string);
|
||||
closeModalHandler();
|
||||
router.push('/marketing');
|
||||
}
|
||||
if (isResponseError(approveMarketingRes)) {
|
||||
approveModal.closeModal();
|
||||
toast.error(approveMarketingRes?.message as string);
|
||||
}
|
||||
refreshMarketing();
|
||||
refreshApproval();
|
||||
};
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
@@ -376,7 +408,77 @@ const DeliveryOrderFormModal = ({
|
||||
},
|
||||
[prevButtonHandler]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
|
||||
const isApprovalStep3Approved = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
marketing.data.latest_approval?.step_number === 3 &&
|
||||
marketing.data.latest_approval?.action === 'APPROVED'
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
const handleUpdateDOWithAPI = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
if (!marketingId) {
|
||||
toast.error('Marketing ID tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const updatedDeliveryValues = deliveryOrderValues.map((product) =>
|
||||
product.id === id ? { ...product, ...values } : product
|
||||
);
|
||||
|
||||
const payload = {
|
||||
marketing_id: Number(marketingId),
|
||||
delivery_products: updatedDeliveryValues
|
||||
.map((product) => {
|
||||
if (Boolean(product.delivery_date)) {
|
||||
return {
|
||||
marketing_product_id: product.marketing_product_id as number,
|
||||
unit_price: parseFloat(product.unit_price as string),
|
||||
total_weight: parseFloat(product.total_weight as string),
|
||||
qty: parseFloat(product.qty as string),
|
||||
avg_weight: parseFloat(product.avg_weight as string),
|
||||
total_price: parseFloat(product.total_price as string),
|
||||
delivery_date: formatDate(
|
||||
product.delivery_date as string,
|
||||
'yyyy-MM-DD'
|
||||
),
|
||||
vehicle_number: product.vehicle_number,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => Boolean(item)),
|
||||
} as UpdateDeliveryOrderPayload;
|
||||
|
||||
const updateDeliveryRes = await DeliveryOrderApi.update(
|
||||
Number(marketingId),
|
||||
payload
|
||||
);
|
||||
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
closeModalHandler();
|
||||
}
|
||||
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
setFormErrorMessage(updateDeliveryRes?.message as string);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[
|
||||
marketingId,
|
||||
deliveryOrderValues,
|
||||
formik.values.sales_order,
|
||||
prevButtonHandler,
|
||||
refreshMarketing,
|
||||
]
|
||||
);
|
||||
|
||||
const handleUpdateDOLocal = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.map((product) =>
|
||||
@@ -420,17 +522,7 @@ const DeliveryOrderFormModal = ({
|
||||
const deliveryRejected = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
((marketing.data.latest_approval.step_number === 3 &&
|
||||
marketing.data.latest_approval.action === 'REJECTED') ||
|
||||
(marketing.data.latest_approval.step_number === 2 &&
|
||||
marketing.data.latest_approval.action === 'REJECTED'))
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
const isPending = useMemo(() => {
|
||||
return (
|
||||
isResponseSuccess(marketing) &&
|
||||
marketing.data.latest_approval.step_number === 1
|
||||
marketing.data.latest_approval.action === 'REJECTED'
|
||||
);
|
||||
}, [marketing]);
|
||||
|
||||
@@ -441,6 +533,7 @@ const DeliveryOrderFormModal = ({
|
||||
modalAction === 'edit_delivery' ||
|
||||
modalAction === 'detail'
|
||||
) {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
}
|
||||
}, [modalAction]);
|
||||
@@ -468,7 +561,26 @@ const DeliveryOrderFormModal = ({
|
||||
);
|
||||
|
||||
formik.setValues(filledInitialValues);
|
||||
setStep(1);
|
||||
|
||||
if (modalAction === 'add_delivery') {
|
||||
// add delivery
|
||||
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
|
||||
if (firstDeliveryItem) {
|
||||
setSelectedDeliveryProduct(firstDeliveryItem);
|
||||
}
|
||||
setStep(2); // Langsung ke form delivery
|
||||
} else if (modalAction === 'edit_delivery') {
|
||||
// edit delivery
|
||||
const firstDeliveryItem = filledInitialValues.delivery_order?.[0];
|
||||
if (firstDeliveryItem) {
|
||||
setSelectedDeliveryProduct(firstDeliveryItem);
|
||||
setStep(2); // Langsung ke form edit
|
||||
} else {
|
||||
setStep(1); // Jika belum ada data, tampilkan detail view
|
||||
}
|
||||
} else {
|
||||
setStep(1); // Detail view
|
||||
}
|
||||
}
|
||||
|
||||
if (isResponseError(marketing)) {
|
||||
@@ -479,7 +591,7 @@ const DeliveryOrderFormModal = ({
|
||||
};
|
||||
|
||||
getFilledInitialValues();
|
||||
}, [marketingId, marketing]);
|
||||
}, [marketingId, marketing, modalAction]);
|
||||
|
||||
// Reset error message when step changes
|
||||
useEffect(() => {
|
||||
@@ -562,9 +674,11 @@ const DeliveryOrderFormModal = ({
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Sales Order</td>
|
||||
<td className='text-sm px-4 py-3'>No. Order</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{marketing.data.so_number}
|
||||
{marketing.data.do_number
|
||||
? marketing.data.do_number
|
||||
: marketing.data.so_number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -667,13 +781,7 @@ const DeliveryOrderFormModal = ({
|
||||
<div className='px-4'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={marketing.data}
|
||||
formType={
|
||||
deliveryRejected
|
||||
? 'rejected'
|
||||
: isPending
|
||||
? 'pending'
|
||||
: modalAction
|
||||
}
|
||||
formType={deliveryRejected ? 'rejected' : modalAction}
|
||||
data={deliveryOrderValues}
|
||||
onEdit={handleEditDO}
|
||||
onDelete={handleDeleteDO}
|
||||
@@ -688,7 +796,12 @@ const DeliveryOrderFormModal = ({
|
||||
exisitingValues={deliveryOrderValues}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
onUpdateForm={
|
||||
isApprovalStep3Approved
|
||||
? handleUpdateDOWithAPI
|
||||
: handleUpdateDOLocal
|
||||
}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -715,31 +828,40 @@ const DeliveryOrderFormModal = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 && (
|
||||
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected || isPending}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected || isPending}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 &&
|
||||
marketing?.data?.latest_approval?.step_number !== 3 && (
|
||||
<div className='w-full px-4 py-3 grid grid-cols-2 items-center justify-between gap-3 border-t border-base-content/10'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
color='none'
|
||||
onClick={() => rejectModal.openModal()}
|
||||
disabled={deliveryRejected}
|
||||
className='p-3 border-base-content/10 shadow-button-soft rounded-lg text-sm text-base-content/50 font-semibold'
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
// Jika masih di step 1 approval, gunakan single approval API
|
||||
if (
|
||||
marketing?.data?.latest_approval?.step_number === 1
|
||||
) {
|
||||
approveModal.openModal();
|
||||
} else {
|
||||
// Jika sudah di step 2/3, gunakan form submit (delivery products)
|
||||
formRef.current?.requestSubmit();
|
||||
}
|
||||
}}
|
||||
className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold'
|
||||
disabled={deliveryRejected}
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -749,8 +871,8 @@ const DeliveryOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add_delivery' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add_delivery' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
@@ -760,14 +882,18 @@ const DeliveryOrderFormModal = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={isResponseSuccess(marketing) ? marketing.data : undefined}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
marketing={
|
||||
isResponseSuccess(marketing) ? marketing.data : undefined
|
||||
}
|
||||
formType={'success'}
|
||||
data={deliveryOrderValues}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModalWithNotes
|
||||
@@ -799,6 +925,21 @@ const DeliveryOrderFormModal = ({
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModalWithNotes
|
||||
ref={approveModal.ref}
|
||||
type={'success'}
|
||||
text={`Apakah anda yakin ingin approve data penjualan?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
onClick: approveModal.closeModal,
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'success',
|
||||
onClick: approveMarketingHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { RefObject } from 'react';
|
||||
import { RefObject, useMemo } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Modal from '@/components/Modal';
|
||||
@@ -9,10 +9,12 @@ import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import { MarketingFilter } from '@/types/api/marketing/marketing';
|
||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||
import { MarketingApi } from '@/services/api/marketing/marketing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||
|
||||
interface MarketingFilterModal {
|
||||
ref: RefObject<HTMLDialogElement | null>;
|
||||
@@ -31,25 +33,59 @@ const MarketingFilterModal = ({
|
||||
|
||||
// ===== OPTIONS =====
|
||||
const {
|
||||
options: productsOptions,
|
||||
rawData: productsRawData,
|
||||
isLoadingOptions: isLoadingProductsOptions,
|
||||
setInputValue: setProductsInputValue,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect<BaseMarketing>(MarketingApi.basePath, 'id', 'so_number', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
|
||||
const productsOptions = useMemo(() => {
|
||||
if (!productsRawData || !isResponseSuccess(productsRawData)) return [];
|
||||
|
||||
const productsMap = new Map<number, { value: number; label: string }>();
|
||||
|
||||
productsRawData.data.forEach((deliveryOrder: BaseMarketing) => {
|
||||
deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => {
|
||||
const product = so.product_warehouse?.product;
|
||||
if (product?.id && product?.name) {
|
||||
productsMap.set(product.id, {
|
||||
value: product.id,
|
||||
label: product.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(productsMap.values());
|
||||
}, [productsRawData]);
|
||||
|
||||
const {
|
||||
options: customersOptions,
|
||||
isLoadingOptions: isLoadingCustomersOptions,
|
||||
setInputValue: setCustomersInputValue,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||
} = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({
|
||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||
label: item.step_name,
|
||||
}));
|
||||
|
||||
const uniqueCustomersOptions = useMemo(() => {
|
||||
const seen = new Set();
|
||||
return customersOptions.filter((customer) => {
|
||||
if (seen.has(customer.value)) return false;
|
||||
seen.add(customer.value);
|
||||
return true;
|
||||
});
|
||||
}, [customersOptions]);
|
||||
|
||||
const statusOptions = [
|
||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||
label: item.step_name,
|
||||
})),
|
||||
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||
];
|
||||
|
||||
const formik = useFormik<{
|
||||
product_ids: OptionType[];
|
||||
@@ -151,7 +187,7 @@ const MarketingFilterModal = ({
|
||||
label='Customer'
|
||||
isClearable
|
||||
placeholder='Pilih customer'
|
||||
options={customersOptions}
|
||||
options={uniqueCustomersOptions}
|
||||
isLoading={isLoadingCustomersOptions}
|
||||
value={formik.values.customer_id}
|
||||
onChange={customerChangeHandler}
|
||||
|
||||
@@ -109,7 +109,9 @@ const RowsOptionsMenu = ({
|
||||
className='p-3 justify-start text-sm font-semibold w-full'
|
||||
>
|
||||
<Icon icon='heroicons:truck' width={20} height={20} />
|
||||
Deliver Item
|
||||
{props.row.original.latest_approval.step_number == 2
|
||||
? 'Deliver Item'
|
||||
: 'Edit Delivery'}
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</>
|
||||
@@ -379,8 +381,13 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_number',
|
||||
accessorKey: 'so_do_number',
|
||||
header: 'No. Order',
|
||||
cell: (props) => {
|
||||
return props.row.original.do_number
|
||||
? props.row.original.do_number
|
||||
: props.row.original.so_number;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'so_date',
|
||||
@@ -408,7 +415,7 @@ const MarketingTable = () => {
|
||||
: approval?.step_number == 2
|
||||
? 'info'
|
||||
: approval?.step_number == 3
|
||||
? 'warning'
|
||||
? 'success'
|
||||
: 'neutral'
|
||||
: 'neutral'
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ const SalesOrderFormModal = ({
|
||||
const modalAction = searchParams.get('action');
|
||||
const marketingId = searchParams.get('id');
|
||||
|
||||
const [currentModalAction, setCurrentModalAction] = useState<string | null>(
|
||||
modalAction
|
||||
);
|
||||
|
||||
const isModalActionForForm =
|
||||
modalAction === 'add' ||
|
||||
modalAction === 'edit' ||
|
||||
@@ -194,6 +198,13 @@ const SalesOrderFormModal = ({
|
||||
: 'KG' // termasuk "QTY" dan "KG"
|
||||
: undefined;
|
||||
|
||||
// Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM"
|
||||
let marketingTypeValue =
|
||||
product.marketing_type?.value?.toUpperCase() || '';
|
||||
if (marketingTypeValue === 'AYAM,AYAM_PULLET') {
|
||||
marketingTypeValue = product.week ? 'AYAM_PULLET' : 'AYAM';
|
||||
}
|
||||
|
||||
return {
|
||||
vehicle_number: product.vehicle_number as string,
|
||||
kandang_id: product.kandang_id as number,
|
||||
@@ -203,12 +214,11 @@ const SalesOrderFormModal = ({
|
||||
qty: parseFloat(String(product.qty || 0)),
|
||||
avg_weight: parseFloat(String(product.avg_weight || 0)),
|
||||
total_price: parseFloat(String(product.total_price || 0)),
|
||||
marketing_type:
|
||||
product.marketing_type?.value?.toUpperCase() || '',
|
||||
marketing_type: marketingTypeValue,
|
||||
convertion_unit: normalizedConvertionUnit,
|
||||
weight_per_convertion:
|
||||
product.weight_per_convertion ?? undefined,
|
||||
week: product.week?.value ?? undefined,
|
||||
week: product.week ?? undefined,
|
||||
} as CreateSalesOrderProductPayload;
|
||||
}),
|
||||
} as CreateSalesOrderPayload)
|
||||
@@ -390,7 +400,7 @@ const SalesOrderFormModal = ({
|
||||
}
|
||||
|
||||
formik.setFieldValue('sales_order', updatedProducts);
|
||||
console.log(formik.values);
|
||||
setSelectedMarketingProduct(null);
|
||||
nextButtonHandler();
|
||||
},
|
||||
[memoSalesOrder, nextButtonHandler]
|
||||
@@ -412,7 +422,17 @@ const SalesOrderFormModal = ({
|
||||
// ================== EFFECT ==================
|
||||
useEffect(() => {
|
||||
if (modalAction === 'add' || modalAction === 'edit') {
|
||||
setCurrentModalAction(modalAction);
|
||||
formModal.openModal();
|
||||
|
||||
if (modalAction === 'add') {
|
||||
formik.resetForm();
|
||||
setStep(1);
|
||||
setSelectedMarketingProduct(null);
|
||||
setSelectedDeliveryProduct(null);
|
||||
setFormErrorMessage('');
|
||||
setFormErrorList([]);
|
||||
}
|
||||
}
|
||||
}, [modalAction]);
|
||||
|
||||
@@ -724,8 +744,8 @@ const SalesOrderFormModal = ({
|
||||
ref={successModal.ref}
|
||||
iconPosition='left'
|
||||
type='success'
|
||||
text={`${modalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${modalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
text={`${currentModalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`}
|
||||
subtitleText={`${currentModalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`}
|
||||
primaryButton={{
|
||||
text: 'Oke',
|
||||
color: 'primary',
|
||||
@@ -735,13 +755,15 @@ const SalesOrderFormModal = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
<div className='max-h-[50vh] overflow-y-auto'>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={'success'}
|
||||
data={memoSalesOrder}
|
||||
onDelete={handleDeleteSO}
|
||||
onEdit={handleEditSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
@@ -128,12 +128,7 @@ export const SalesProductToFieldValues = (
|
||||
label: formatTitleCase(product.convertion_unit),
|
||||
}
|
||||
: null,
|
||||
week: product.week
|
||||
? {
|
||||
value: product.week,
|
||||
label: `Week ${product.week}`,
|
||||
}
|
||||
: null,
|
||||
week: product.week ?? null,
|
||||
total_peti: product.total_peti,
|
||||
weight_per_convertion: product.weight_per_convertion,
|
||||
uom: product.product_warehouse.product.uom.name,
|
||||
|
||||
+7
-21
@@ -30,13 +30,7 @@ type DeliveryOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSchemaType> =
|
||||
@@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+46
-16
@@ -36,6 +36,7 @@ const DeliveryOrderProductForm = ({
|
||||
exisitingValues,
|
||||
onSubmitForm,
|
||||
onUpdateForm,
|
||||
isLoading,
|
||||
}: {
|
||||
formState: 'add' | 'edit';
|
||||
salesOrders: BaseSalesOrder[];
|
||||
@@ -46,6 +47,7 @@ const DeliveryOrderProductForm = ({
|
||||
id: number,
|
||||
value: DeliveryOrderProductFormValues
|
||||
) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}) => {
|
||||
const [formikErrorMessage, setFormErrorMessage] = useState('');
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
@@ -178,6 +180,25 @@ const DeliveryOrderProductForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const hasWeekField = useMemo(() => {
|
||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||
if (marketingType === 'ayam_pullet') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (formik.values.marketing_product?.product_warehouse_data) {
|
||||
return Boolean(
|
||||
formik.values.marketing_product?.product_warehouse_data?.week !==
|
||||
undefined &&
|
||||
formik.values.marketing_product?.product_warehouse_data?.week !==
|
||||
null &&
|
||||
formik.values.marketing_product?.product_warehouse_data?.week > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [formik.values.marketing_product, formik.values.marketing_type]);
|
||||
|
||||
const handleResetForm = () => {
|
||||
setFormErrorMessage('');
|
||||
formik.resetForm({
|
||||
@@ -362,20 +383,24 @@ const DeliveryOrderProductForm = ({
|
||||
avg_weight: '',
|
||||
total_weight: '',
|
||||
vehicle_number: '',
|
||||
week: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const soFieldValues = SalesProductToFieldValues(so);
|
||||
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: selected.value as number,
|
||||
marketing_product: SalesProductToFieldValues(so),
|
||||
marketing_product: soFieldValues,
|
||||
qty: so.qty,
|
||||
unit_price: so.unit_price,
|
||||
total_price: so.total_price,
|
||||
avg_weight: so.avg_weight,
|
||||
total_weight: so.total_weight,
|
||||
vehicle_number: so.vehicle_number,
|
||||
week: soFieldValues.week ?? null,
|
||||
});
|
||||
}}
|
||||
startAdornment={
|
||||
@@ -509,21 +534,26 @@ const DeliveryOrderProductForm = ({
|
||||
)}
|
||||
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
{(formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' ||
|
||||
hasWeekField) && (
|
||||
<NumberInput
|
||||
required={
|
||||
formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet'
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
label='Minggu'
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
placeholder='Pilih Week'
|
||||
onBlur={() => handleBlurField('week')}
|
||||
isError={formik.touched.week && Boolean(formik.errors.week)}
|
||||
errorMessage={formik.errors.week as string}
|
||||
placeholder='Masukan Minggu'
|
||||
decimalScale={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -792,8 +822,8 @@ const DeliveryOrderProductForm = ({
|
||||
<div className='absolute sm:w-full bottom-0 right-0 p-4'>
|
||||
<Button
|
||||
type='submit'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
isLoading={formik.isSubmitting || isLoading}
|
||||
disabled={formik.isSubmitting || isLoading}
|
||||
className='w-full p-3 rounded-lg text-base-100 text-sm font-semibold'
|
||||
>
|
||||
Submit
|
||||
|
||||
+7
-21
@@ -37,13 +37,7 @@ type SalesOrderProductSchemaType = {
|
||||
/** Harga per butir telur untuk TELUR + QTY */
|
||||
price_per_qty?: number | null | undefined;
|
||||
/** Week untuk ayam pullet */
|
||||
week?:
|
||||
| {
|
||||
value?: number;
|
||||
label?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
week?: number | null | undefined;
|
||||
};
|
||||
|
||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||
@@ -102,26 +96,18 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
||||
sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_sisa_berat: Yup.number().nullable().optional().notRequired(),
|
||||
price_per_qty: Yup.number().nullable().optional().notRequired(),
|
||||
week: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
week: Yup.number()
|
||||
.nullable()
|
||||
.default(null)
|
||||
.optional()
|
||||
.notRequired()
|
||||
.when('marketing_type', {
|
||||
is: (marketingType: { value: string } | null | undefined) =>
|
||||
marketingType?.value?.toLowerCase() === 'ayam_pullet',
|
||||
then: (schema) =>
|
||||
schema
|
||||
.shape({
|
||||
value: Yup.number().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
label: Yup.string().required(
|
||||
'Week wajib diisi untuk Ayam Pullet!'
|
||||
),
|
||||
})
|
||||
.required('Week wajib diisi untuk Ayam Pullet!'),
|
||||
.min(1, 'Week wajib diisi untuk Ayam Pullet!')
|
||||
.required('Week wajib diisi untuk Ayam Pullet!')
|
||||
.typeError('Week harus berupa angka!'),
|
||||
otherwise: (schema) => schema.optional().notRequired(),
|
||||
}),
|
||||
});
|
||||
|
||||
+41
-13
@@ -117,6 +117,19 @@ const SalesOrderProductForm = ({
|
||||
isInitialValid: false,
|
||||
});
|
||||
|
||||
const hasWeekField = useMemo(() => {
|
||||
const marketingType = formik.values.marketing_type?.value?.toLowerCase();
|
||||
if (marketingType === 'ayam_pullet') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Boolean(
|
||||
selectedProductWarehouse?.week !== undefined &&
|
||||
selectedProductWarehouse?.week !== null &&
|
||||
selectedProductWarehouse?.week > 0
|
||||
);
|
||||
}, [selectedProductWarehouse, formik.values.marketing_type]);
|
||||
|
||||
// ===== Options =====
|
||||
const {
|
||||
options: kandangSourceOptions,
|
||||
@@ -180,10 +193,20 @@ const SalesOrderProductForm = ({
|
||||
setSelectedProductWarehouse(productWarehouse || null);
|
||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||
formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || '');
|
||||
if (
|
||||
productWarehouse?.week !== undefined &&
|
||||
productWarehouse?.week !== null &&
|
||||
productWarehouse?.week > 0
|
||||
) {
|
||||
formik.setFieldValue('week', productWarehouse.week);
|
||||
} else {
|
||||
formik.setFieldValue('week', null);
|
||||
}
|
||||
handleBlurField('qty');
|
||||
} else {
|
||||
formik.setFieldValue('qty', '');
|
||||
formik.setFieldValue('uom', '');
|
||||
formik.setFieldValue('week', null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -465,21 +488,26 @@ const SalesOrderProductForm = ({
|
||||
)}
|
||||
|
||||
{/* Konversi Satuan Week Pullet */}
|
||||
{formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<SelectInputRadio
|
||||
required
|
||||
label='Minggu'
|
||||
options={optionsWeek}
|
||||
value={
|
||||
formik.values.week?.value
|
||||
? (formik.values.week as { value: number; label: string })
|
||||
: null
|
||||
{(formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' ||
|
||||
hasWeekField) && (
|
||||
<NumberInput
|
||||
required={
|
||||
formik.values.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet'
|
||||
}
|
||||
onChange={(val) => {
|
||||
formik.setFieldValue('week', val);
|
||||
label='Minggu'
|
||||
name='week'
|
||||
value={formik.values.week ?? undefined}
|
||||
onChange={(e) => {
|
||||
formik.setFieldValue('week', Number(e.target.value));
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
placeholder='Pilih Week'
|
||||
onBlur={() => handleBlurField('week')}
|
||||
isError={formik.touched.week && Boolean(formik.errors.week)}
|
||||
errorMessage={formik.errors.week as string}
|
||||
placeholder='Masukan Minggu'
|
||||
decimalScale={0}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useRef } from 'react';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
@@ -18,6 +19,7 @@ type DeliveryOrderProductTableProps = {
|
||||
| 'detail'
|
||||
| 'rejected'
|
||||
| 'pending'
|
||||
| 'success'
|
||||
| string
|
||||
| null;
|
||||
marketing?: Marketing;
|
||||
@@ -31,7 +33,6 @@ const DeliveryOrderProductTable = ({
|
||||
formType,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAddProductClick,
|
||||
marketing,
|
||||
}: DeliveryOrderProductTableProps) => {
|
||||
const onEditRef = useRef(onEdit);
|
||||
@@ -39,161 +40,195 @@ const DeliveryOrderProductTable = ({
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
|
||||
const approvalStepNumber = marketing?.latest_approval?.step_number;
|
||||
|
||||
const renderTableContent = (item: DeliveryOrderProductFormValues) => {
|
||||
const doItem = marketing?.delivery_order?.find(
|
||||
(doItem) => doItem.do_number === item.do_number
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' &&
|
||||
(formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail') && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{doItem?.warehouse?.name ||
|
||||
item.marketing_product?.product_warehouse_data?.warehouse?.name}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Qty</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.qty
|
||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
{Number(item.avg_weight ?? 0) > 0 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(Number(item.avg_weight))} Kg
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{Number(item.total_weight ?? 0) > 0 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(Number(item.total_weight))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
<tr className='border-b border-t border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
{approvalStepNumber !== 1 && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.delivery_date ? (
|
||||
formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||
) : formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail' ? (
|
||||
<span
|
||||
className='text-error hover:text-error/70 cursor-pointer hover:underline underline-offset-4'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
>
|
||||
Belum diisi
|
||||
</span>
|
||||
) : (
|
||||
<span className='text-error'>Belum diisi</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.do_number && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>{item.do_number}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
{doItem && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Dokumen Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
<DeliveryOrderExport data={marketing} deliveryOrder={doItem} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||
{data.map((item) => {
|
||||
const doItem = marketing?.delivery_order?.find(
|
||||
(doItem) => doItem.do_number === item.do_number
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
key={`table-${item.id}`}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
{data.map((item) => (
|
||||
<div key={`table-${item.id}`}>
|
||||
{formType === 'success' ? (
|
||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
key={`table-${item.id}`}
|
||||
title={
|
||||
item.marketing_product?.product_warehouse?.label || 'Produk'
|
||||
}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full mt-2'>
|
||||
<div>Value</div>
|
||||
{(formType === 'add_delivery' ||
|
||||
formType === 'edit_delivery' ||
|
||||
formType === 'detail') && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number, item);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:pencil'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:trash'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tanggal Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.delivery_date ? (
|
||||
formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||
) : (
|
||||
<span className='text-error'>Belum diisi</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{item.do_number && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Pengiriman</td>
|
||||
<td className='text-sm px-4 py-3'>{item.do_number}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.vehicle_number}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_product?.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Qty</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.qty
|
||||
? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.avg_weight as string)
|
||||
) + ' Kg'
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatNumber(parseFloat(item.total_weight as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
{doItem && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
Dokumen Pengiriman
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
<DeliveryOrderExport
|
||||
data={marketing}
|
||||
deliveryOrder={doItem}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import {
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { useRef } from 'react';
|
||||
|
||||
type SalesOrderProductTableProps = {
|
||||
data: SalesOrderProductFormValues[];
|
||||
@@ -32,268 +27,167 @@ const SalesOrderProductTable = ({
|
||||
const onEditRef = useRef(onEdit);
|
||||
onEditRef.current = onEdit;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({
|
||||
table,
|
||||
}: {
|
||||
table: TanStack.Table<SalesOrderProductFormValues>;
|
||||
}) => (
|
||||
<div className='w-full flex flex-row justify-center'>
|
||||
<CheckboxInput
|
||||
name='allRow'
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
indeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
const renderTableContent = (item: SalesOrderProductFormValues) => (
|
||||
<>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:pencil' width={20} height={20} />
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon icon='heroicons:trash' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) => (
|
||||
<div>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
value={`${row.original.product_warehouse_id}${row.original.kandang_id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
header: 'No. Polisi',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label,
|
||||
header: 'Kandang',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
row.product_warehouse?.label,
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
header: 'Harga Satuan (Rp)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5),
|
||||
header: 'Total Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
header: 'Kuantitas',
|
||||
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) =>
|
||||
formatNumber(
|
||||
parseFloat(row.original.qty as string),
|
||||
undefined,
|
||||
0,
|
||||
5
|
||||
) +
|
||||
' ' +
|
||||
(row.original.uom ?? ''),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5),
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
header: 'Total Penjualan (Rp)',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<Button
|
||||
color='warning'
|
||||
className='p-1'
|
||||
onClick={() => onEditRef.current(props.row.original.id as number)}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||
<td className='text-sm px-4 py-3'>{item.marketing_type?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>{item.product_warehouse?.label}</td>
|
||||
</tr>
|
||||
{item.marketing_type?.value.toLowerCase() === 'telur' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.convertion_unit?.label}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() === 'ayam_pullet' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>Week {item.week}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_peti} {item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() !== 'trading' && (
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_weight
|
||||
? formatNumber(parseFloat(item.total_weight as string)) +
|
||||
' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(parseFloat(item.avg_weight as string)) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.value === 'telur'
|
||||
? 'Total Butir Telur'
|
||||
: 'Qty'}
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='size-full flex flex-col relative overflow-x-hidden gap-3'>
|
||||
{data.map((item) => (
|
||||
<div
|
||||
className='rounded-lg border border-tools-table-outline border-base-content/5'
|
||||
key={`table-${item.id}`}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>
|
||||
<tr className='border-b border-tools-table-outline border-base-content/5'>
|
||||
<th className='w-1/3 text-start not-first:font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
Label
|
||||
</th>
|
||||
<th className='text-start font-medium text-base-content/50 text-sm px-4 py-3'>
|
||||
<div className='flex w-full flex-row gap-1 items-center justify-between h-full mt-2'>
|
||||
<div>Value</div>
|
||||
{formType !== 'success' && (
|
||||
<div className='flex flex-row gap-1.5 items-center'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onEditRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:pencil'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
color='none'
|
||||
onClick={() => {
|
||||
onDeleteRef.current(item.id as number);
|
||||
}}
|
||||
className='p-0 text-error hover:text-base-content'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:trash'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>No. Polisi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.vehicle_number}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Gudang</td>
|
||||
<td className='text-sm px-4 py-3'>{item.kandang?.label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Kategori</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.label}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Produk</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.product_warehouse?.label}
|
||||
</td>
|
||||
</tr>
|
||||
{item.marketing_type?.value.toLowerCase() === 'telur' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() ===
|
||||
'ayam_pullet' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Tipe Konversi</td>
|
||||
<td className='text-sm px-4 py-3'>{item.week?.label}</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.convertion_unit?.value.toLowerCase() === 'peti' && (
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Peti</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_peti} {item.convertion_unit?.label}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{item.marketing_type?.value.toLowerCase() !== 'trading' && (
|
||||
<>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.total_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.total_weight as string)
|
||||
) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Avg Bobot</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.avg_weight
|
||||
? formatNumber(
|
||||
parseFloat(item.avg_weight as string)
|
||||
) + ' Kg'
|
||||
: '0 Kg'}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{item.marketing_type?.value === 'telur'
|
||||
? 'Total Butir Telur'
|
||||
: 'Qty'}
|
||||
</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Harga Satuan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.unit_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='text-sm px-4 py-3'>Total Penjualan</td>
|
||||
<td className='text-sm px-4 py-3'>
|
||||
{formatCurrency(parseFloat(item.total_price as string))}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
<div key={`table-${item.id}`}>
|
||||
{formType === 'success' ? (
|
||||
<div className='rounded-lg border border-tools-table-outline border-base-content/5'>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
title={item.product_warehouse?.label || 'Produk'}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full rounded-lg',
|
||||
body: 'p-0',
|
||||
title: 'px-2 py-1.5 font-normal text-sm',
|
||||
collapsible: 'rounded-lg',
|
||||
}}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
borderRadius: '0.5rem',
|
||||
}}
|
||||
className='border-none w-full'
|
||||
>
|
||||
<tbody className='w-full'>{renderTableContent(item)}</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{formType != 'add_deliver' &&
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Fcr, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<RequirePermission permissions='lti.master.fcr.detail'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const FcrsTable = () => {
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: { search: '', nameSort: '' },
|
||||
paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' },
|
||||
});
|
||||
|
||||
const {
|
||||
data: fcrs,
|
||||
isLoading,
|
||||
mutate: refreshFcrs,
|
||||
} = useSWR(
|
||||
`${FcrApi.basePath}${getTableFilterQueryString()}`,
|
||||
FcrApi.getAllFetcher
|
||||
);
|
||||
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [selectedFcr, setSelectedFcr] = useState<Fcr | undefined>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const fcrsColumns: ColumnDef<Fcr>[] = [
|
||||
{
|
||||
header: '#',
|
||||
cell: (props) =>
|
||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||
props.row.index +
|
||||
1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Nama',
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props) => {
|
||||
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||
const currentRowRelativeIndex =
|
||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedFcr(props.row.original);
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageSize > 2 && (
|
||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||
<RowOptionsMenu
|
||||
type='dropdown'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
|
||||
{currentPageSize <= 2 && (
|
||||
<RowCollapseOptions>
|
||||
<RowOptionsMenu
|
||||
type='collapse'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const deleteResponse = await FcrApi.delete(selectedFcr?.id as number);
|
||||
|
||||
if (isResponseError(deleteResponse)) {
|
||||
toast.error(deleteResponse.message);
|
||||
setIsDeleteLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshFcrs();
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete FCR!');
|
||||
setIsDeleteLoading(false);
|
||||
};
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
|
||||
setPageSize(newVal.value as number);
|
||||
};
|
||||
|
||||
// track sorting
|
||||
useEffect(() => {
|
||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||
|
||||
if (!isNameSorted) {
|
||||
updateFilter('nameSort', '');
|
||||
} else {
|
||||
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||
}
|
||||
}, [sorting]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||
<div className='w-full flex flex-row'>
|
||||
<RequirePermission permissions='lti.master.fcr.create'>
|
||||
<Button
|
||||
href='/master-data/fcr/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari FCR'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row justify-end'>
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
value={{
|
||||
label: String(tableFilterState.pageSize),
|
||||
value: tableFilterState.pageSize,
|
||||
}}
|
||||
onChange={pageSizeChangeHandler}
|
||||
className={{ wrapper: 'max-w-28' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<Fcr>
|
||||
data={isResponseSuccess(fcrs) ? fcrs?.data : []}
|
||||
columns={fcrsColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
page={isResponseSuccess(fcrs) ? fcrs?.meta?.page : 0}
|
||||
totalItems={isResponseSuccess(fcrs) ? fcrs?.meta?.total_results : 0}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoading}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20': isResponseSuccess(fcrs) && fcrs?.data?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data FCR ini (${selectedFcr?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrsTable;
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const FcrStandardSchema: Yup.ObjectSchema<{
|
||||
weight: number | string;
|
||||
fcr_number: number | string;
|
||||
mortality: number | string;
|
||||
}> = Yup.object({
|
||||
weight: Yup.number().nullable().required('Bobot wajib diisi!'),
|
||||
fcr_number: Yup.number()
|
||||
.nullable()
|
||||
.typeError('FCR harus angka!')
|
||||
.required('FCR harus diisi!'),
|
||||
mortality: Yup.number().nullable().required('Mortalitas wajib diisi!'),
|
||||
});
|
||||
|
||||
export const FcrFormSchema = Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
fcrStandards: Yup.array()
|
||||
.of(FcrStandardSchema)
|
||||
.min(1, 'Minimal 1 FCR Standard diisi1')
|
||||
.required('FCR wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdateFcrFormSchema = FcrFormSchema;
|
||||
|
||||
export type FcrFormValues = Yup.InferType<typeof FcrFormSchema>;
|
||||
@@ -1,401 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useFormik } from 'formik';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
|
||||
import {
|
||||
FcrFormSchema,
|
||||
FcrFormValues,
|
||||
UpdateFcrFormSchema,
|
||||
} from '@/components/pages/master-data/fcr/form/FcrForm.schema';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import {
|
||||
CreateFcrPayload,
|
||||
Fcr,
|
||||
FcrWithStandards,
|
||||
UpdateFcrPayload,
|
||||
} from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
|
||||
interface FcrFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
initialValues?: FcrWithStandards;
|
||||
}
|
||||
|
||||
const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [fcrFormErrorMessage, setFcrFormErrorMessage] = useState('');
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const createFcrHandler = useCallback(
|
||||
async (payload: CreateFcrPayload) => {
|
||||
const createFcrRes = await FcrApi.create(payload);
|
||||
|
||||
if (isResponseError(createFcrRes)) {
|
||||
setFcrFormErrorMessage(createFcrRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(createFcrRes?.message as string);
|
||||
router.push('/master-data/fcr');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const updateFcrHandler = useCallback(
|
||||
async (fcrId: number, payload: UpdateFcrPayload) => {
|
||||
const updateFcrRes = await FcrApi.update(fcrId, payload);
|
||||
|
||||
if (updateFcrRes?.status === 'error') {
|
||||
setFcrFormErrorMessage(updateFcrRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(updateFcrRes?.message as string);
|
||||
router.refresh();
|
||||
router.push('/master-data/fcr');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const formikInitialValues = useMemo<FcrFormValues>(() => {
|
||||
return {
|
||||
name: initialValues?.name ?? '',
|
||||
fcrStandards: initialValues?.fcr_standards
|
||||
? initialValues?.fcr_standards
|
||||
: [
|
||||
{
|
||||
weight: '',
|
||||
fcr_number: '',
|
||||
mortality: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<FcrFormValues>({
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: type === 'edit' ? UpdateFcrFormSchema : FcrFormSchema,
|
||||
onSubmit: async (values) => {
|
||||
setFcrFormErrorMessage('');
|
||||
|
||||
const fcrPayload: CreateFcrPayload = {
|
||||
name: values.name,
|
||||
fcr_standards: values.fcrStandards as CreateFcrPayload['fcr_standards'],
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'add':
|
||||
await createFcrHandler(fcrPayload);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
await updateFcrHandler(initialValues?.id as number, fcrPayload);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
const addFcrStandard = () =>
|
||||
formik.setFieldValue('fcrStandards', [
|
||||
...formik.values.fcrStandards,
|
||||
{
|
||||
weight: '',
|
||||
fcr_number: '',
|
||||
mortality: '',
|
||||
},
|
||||
]);
|
||||
|
||||
const removeFcrStandard = (i: number) =>
|
||||
formik.setFieldValue(
|
||||
'fcrStandards',
|
||||
formik.values.fcrStandards.filter((_, idx) => idx !== i)
|
||||
);
|
||||
|
||||
const deleteFcrClickHandler = () => {
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await FcrApi.delete(initialValues?.id as number);
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete FCR!');
|
||||
setIsDeleteLoading(false);
|
||||
router.push('/master-data/fcr');
|
||||
};
|
||||
|
||||
const isRepeaterInputError = (
|
||||
column: keyof CreateFcrPayload['fcr_standards'][0],
|
||||
idx: number
|
||||
) => {
|
||||
return (
|
||||
formik.touched.fcrStandards?.[idx]?.[column] &&
|
||||
Boolean(
|
||||
formik.errors.fcrStandards?.[idx] instanceof Object &&
|
||||
formik.errors.fcrStandards?.[idx]?.[column]
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formikSetValues(formikInitialValues);
|
||||
}, [formikSetValues, formikInitialValues]);
|
||||
|
||||
// ===== Formik Error List =====
|
||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-5xl'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/master-data/fcr'
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||
Kembali
|
||||
</Button>
|
||||
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
{type === 'add' && 'Tambah FCR'}
|
||||
{type === 'edit' && 'Edit FCR'}
|
||||
{type === 'detail' && 'Detail FCR'}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<form
|
||||
onSubmit={handleFormSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<TextInput
|
||||
required
|
||||
label='Nama'
|
||||
name='name'
|
||||
placeholder='Masukkan nama FCR'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.name && Boolean(formik.errors.name)}
|
||||
errorMessage={formik.errors.name}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bobot</th>
|
||||
<th>FCR</th>
|
||||
<th>Mortalitas</th>
|
||||
{type !== 'detail' && <th>Aksi</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{formik.values.fcrStandards.map((fcrStandard, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].weight`}
|
||||
placeholder='Masukkan bobot'
|
||||
value={fcrStandard.weight}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('weight', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].fcr_number`}
|
||||
placeholder='Masukkan FCR'
|
||||
value={fcrStandard.fcr_number}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('fcr_number', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
type='number'
|
||||
name={`fcrStandards[${idx}].mortality`}
|
||||
placeholder='Masukkan mortalitas'
|
||||
value={fcrStandard.mortality}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={isRepeaterInputError('mortality', idx)}
|
||||
readOnly={type === 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-24',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{type !== 'detail' && (
|
||||
<td>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeFcrStandard(idx)}
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type !== 'detail' && (
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addFcrStandard}
|
||||
className='w-fit mx-auto'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah FCR
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deleteFcrClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
|
||||
{type !== 'edit' && (
|
||||
<RequirePermission permissions='lti.master.fcr.update'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type !== 'detail' && (
|
||||
<div
|
||||
className={cn('flex flex-row justify-end gap-2', {
|
||||
'w-full': type === 'add',
|
||||
})}
|
||||
>
|
||||
<Button type='reset' color='warning' className='px-4'>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
color='primary'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
className='px-4'
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{fcrFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{fcrFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{type !== 'add' && (
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data FCR ini (${initialValues?.name})?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FcrForm;
|
||||
@@ -33,6 +33,9 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal';
|
||||
import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store';
|
||||
import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
props,
|
||||
@@ -137,6 +140,15 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||
const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess);
|
||||
const createdProjectFlock = useProjectFlockStore(
|
||||
(s) => s.createdProjectFlock
|
||||
);
|
||||
const setCreatedProjectFlock = useProjectFlockStore(
|
||||
(s) => s.setCreatedProjectFlock
|
||||
);
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -180,6 +192,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const deleteModal = useModal();
|
||||
const confirmModal = useModal();
|
||||
const successModal = useModal();
|
||||
const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>(
|
||||
'APPROVED'
|
||||
);
|
||||
@@ -275,6 +288,64 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
refreshProjectFlocks();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
successModal.openModal();
|
||||
}
|
||||
}, [isSuccess, successModal]);
|
||||
|
||||
const handleSuccessModalClose = () => {
|
||||
successModal.closeModal();
|
||||
setIsSuccess(false);
|
||||
setCreatedProjectFlock(null);
|
||||
};
|
||||
|
||||
const projectFlockFormValues = useMemo(() => {
|
||||
if (!createdProjectFlock) return undefined;
|
||||
|
||||
return {
|
||||
flock: {
|
||||
value: 0,
|
||||
label: createdProjectFlock.flock_name || '',
|
||||
},
|
||||
flock_name: createdProjectFlock.flock_name || '',
|
||||
area: {
|
||||
value: createdProjectFlock.area_id,
|
||||
label: createdProjectFlock.area?.name || '',
|
||||
},
|
||||
area_id: createdProjectFlock.area_id,
|
||||
category_option: {
|
||||
value: createdProjectFlock.category,
|
||||
label: createdProjectFlock.category,
|
||||
},
|
||||
category: createdProjectFlock.category,
|
||||
production_standard: {
|
||||
value: createdProjectFlock.production_standard_id,
|
||||
label: createdProjectFlock.production_standard?.name || '',
|
||||
},
|
||||
production_standard_id: createdProjectFlock.production_standard_id,
|
||||
location: {
|
||||
value: createdProjectFlock.location_id,
|
||||
label: createdProjectFlock.location?.name || '',
|
||||
},
|
||||
location_id: createdProjectFlock.location_id,
|
||||
kandang_ids: createdProjectFlock.kandangs?.map((k) => k.id) || [],
|
||||
project_budgets:
|
||||
createdProjectFlock.project_budgets?.map((budget) => ({
|
||||
nonstock: budget.nonstock
|
||||
? {
|
||||
value: budget.nonstock_id,
|
||||
label: budget.nonstock.name || '',
|
||||
}
|
||||
: null,
|
||||
nonstock_id: budget.nonstock_id,
|
||||
qty: budget.qty,
|
||||
price: budget.price,
|
||||
total_price: budget.qty * budget.price,
|
||||
})) || [],
|
||||
} as ProjectFlockFormValues;
|
||||
}, [createdProjectFlock]);
|
||||
|
||||
// ====== MEMO ======
|
||||
const selectedSingleRow: ProjectFlock | null | undefined = useMemo(() => {
|
||||
return selectedRowIds.length === 1
|
||||
@@ -363,10 +434,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
accessorKey: 'location.name',
|
||||
header: 'Lokasi',
|
||||
},
|
||||
{
|
||||
accessorKey: 'fcr.name',
|
||||
header: 'FCR',
|
||||
},
|
||||
{
|
||||
accessorKey: 'category',
|
||||
header: 'Kategori',
|
||||
@@ -877,6 +944,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
isLoading: isApproveLoading,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProjectFlockConfirmationModal
|
||||
ref={successModal.ref}
|
||||
type='success'
|
||||
text='Data Berhasil Ditambahkan'
|
||||
subtitleText='Data project flock telah berhasil disimpan.'
|
||||
projectFlockForm={projectFlockFormValues}
|
||||
onClose={handleSuccessModalClose}
|
||||
secondaryButton={undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,11 +16,6 @@ type ProjectFlockFormSchemaType = {
|
||||
label: string;
|
||||
} | null;
|
||||
category: string;
|
||||
fcr: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
} | null;
|
||||
fcr_id: number;
|
||||
production_standard: {
|
||||
value: number | string;
|
||||
label: string;
|
||||
@@ -96,15 +91,6 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema<ProjectFlockFormSchemaType
|
||||
.oneOf(['GROWING', 'LAYING'], 'Kategori wajib diisi!')
|
||||
.required('Kategori wajib diisi!'),
|
||||
|
||||
// FCR
|
||||
fcr: Yup.object({
|
||||
value: Yup.number().required('ID FCR wajib diisi!'),
|
||||
label: Yup.string().required('Nama FCR wajib diisi!'),
|
||||
}).nullable(),
|
||||
fcr_id: Yup.number()
|
||||
.min(1, 'FCR wajib diisi!')
|
||||
.required('FCR wajib diisi!'),
|
||||
|
||||
// Production Standard
|
||||
production_standard: Yup.object({
|
||||
value: Yup.number().required('ID Standar Produksi wajib diisi!'),
|
||||
|
||||
@@ -9,7 +9,6 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import {
|
||||
AreaApi,
|
||||
FcrApi,
|
||||
FlockApi,
|
||||
KandangApi,
|
||||
LocationApi,
|
||||
@@ -53,7 +52,7 @@ import Table from '@/components/Table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
||||
import ProjectFlockConfirmationModal from '../ProjectFlockConfirmationModal';
|
||||
import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -214,6 +213,10 @@ const ProjectFlockForm = ({
|
||||
}: ProjectFlockFormProps) => {
|
||||
// State
|
||||
const router = useRouter();
|
||||
const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess);
|
||||
const setCreatedProjectFlock = useProjectFlockStore(
|
||||
(s) => s.setCreatedProjectFlock
|
||||
);
|
||||
|
||||
const [formStep, setFormStep] = useState<'form' | 'confirmation'>('form');
|
||||
|
||||
@@ -240,7 +243,6 @@ const ProjectFlockForm = ({
|
||||
const subscribeValidate = useUiStore((s) => s.subscribeValidate);
|
||||
const setIsValid = useUiStore((s) => s.setIsValid);
|
||||
|
||||
const successModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
@@ -263,6 +265,8 @@ const ProjectFlockForm = ({
|
||||
loadMore: loadMoreFlock,
|
||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||
project_category: selectedCategory,
|
||||
location_id: selectedLocation,
|
||||
area_id: selectedArea,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -284,13 +288,6 @@ const ProjectFlockForm = ({
|
||||
: ((initialValues?.area?.id ?? '') as string),
|
||||
});
|
||||
|
||||
const {
|
||||
options: optionsFcr,
|
||||
isLoadingOptions: isLoadingFcrs,
|
||||
setInputValue: setInputValueFcr,
|
||||
loadMore: loadMoreFcr,
|
||||
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: optionsProductionStandards,
|
||||
isLoadingOptions: isLoadingProductionStandards,
|
||||
@@ -439,7 +436,9 @@ const ProjectFlockForm = ({
|
||||
if (isResponseSuccess(createProjectFlockRes)) {
|
||||
toast.success(createProjectFlockRes?.message as string);
|
||||
handleReset();
|
||||
successModal.openModal();
|
||||
setCreatedProjectFlock(createProjectFlockRes?.data ?? null);
|
||||
setIsSuccess(true);
|
||||
router.push('/production/project-flock');
|
||||
}
|
||||
if (isResponseError(createProjectFlockRes)) {
|
||||
setProjectFlockFormErrorMessage(createProjectFlockRes?.message as string);
|
||||
@@ -457,7 +456,9 @@ const ProjectFlockForm = ({
|
||||
if (isResponseSuccess(updateProjectFlockRes)) {
|
||||
toast.success(updateProjectFlockRes?.message as string);
|
||||
handleReset();
|
||||
successModal.openModal();
|
||||
setCreatedProjectFlock(updateProjectFlockRes?.data ?? null);
|
||||
setIsSuccess(true);
|
||||
router.push('/production/project-flock');
|
||||
}
|
||||
if (isResponseError(updateProjectFlockRes)) {
|
||||
setProjectFlockFormErrorMessage(updateProjectFlockRes?.message as string);
|
||||
@@ -505,12 +506,6 @@ const ProjectFlockForm = ({
|
||||
label: initialValues.category,
|
||||
}
|
||||
: null,
|
||||
fcr: initialValues?.fcr
|
||||
? {
|
||||
value: initialValues.fcr?.id,
|
||||
label: initialValues.fcr.name,
|
||||
}
|
||||
: null,
|
||||
production_standard: initialValues?.production_standard
|
||||
? {
|
||||
value: initialValues.production_standard?.id,
|
||||
@@ -531,7 +526,6 @@ const ProjectFlockForm = ({
|
||||
category: initialValues?.category as NonNullable<
|
||||
'GROWING' | 'LAYING' | undefined
|
||||
>,
|
||||
fcr_id: initialValues?.fcr?.id ?? 0,
|
||||
production_standard_id: initialValues?.production_standard?.id ?? 0,
|
||||
location_id: initialValues?.location?.id ?? 0,
|
||||
kandang_ids: initialValues?.kandangs?.map(
|
||||
@@ -574,7 +568,6 @@ const ProjectFlockForm = ({
|
||||
flock_name: values.flock_name as string,
|
||||
area_id: values.area_id as number,
|
||||
category: values.category as string,
|
||||
fcr_id: values.fcr_id as number,
|
||||
production_standard_id: values.production_standard_id as number,
|
||||
location_id: values.location_id as number,
|
||||
kandang_ids: values.kandang_ids as number[],
|
||||
@@ -996,25 +989,6 @@ const ProjectFlockForm = ({
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
label='FCR'
|
||||
placeholder='Pilih FCR'
|
||||
value={formik.values.fcr as OptionType}
|
||||
onChange={(val) => {
|
||||
optionChangeHandler(val, 'fcr');
|
||||
}}
|
||||
onInputChange={setInputValueFcr}
|
||||
onMenuScrollToBottom={loadMoreFcr}
|
||||
options={optionsFcr}
|
||||
isLoading={isLoadingFcrs}
|
||||
isError={
|
||||
formik.touched.fcr_id && Boolean(formik.errors.fcr_id)
|
||||
}
|
||||
errorMessage={formik.errors.fcr_id as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
/>
|
||||
<SelectInput
|
||||
required
|
||||
label='Kategori'
|
||||
@@ -1423,19 +1397,6 @@ const ProjectFlockForm = ({
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ProjectFlockConfirmationModal
|
||||
ref={successModal.ref}
|
||||
type='success'
|
||||
text='Data Berhasil Ditambahkan'
|
||||
subtitleText='Data project flock telah berhasil disimpan.'
|
||||
projectFlockForm={formikLastValues}
|
||||
onClose={() => {
|
||||
router.push('/production/project-flock');
|
||||
setFormikLastValues(undefined);
|
||||
}}
|
||||
secondaryButton={undefined}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
RecordingApi,
|
||||
ProjectFlockApi,
|
||||
} from '@/services/api/production';
|
||||
import { FcrApi, ProductionStandardApi } from '@/services/api/master-data';
|
||||
import { FcrWithStandards, FcrStandard } from '@/types/api/master-data/fcr';
|
||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||
import {
|
||||
ProductionStandard,
|
||||
StandardDetails,
|
||||
@@ -87,24 +86,6 @@ interface RecordingFormProps {
|
||||
initialValues?: Recording;
|
||||
}
|
||||
|
||||
const fcrStandardColumns: ColumnDef<FcrStandard>[] = [
|
||||
{
|
||||
accessorKey: 'weight',
|
||||
header: 'Weight',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
},
|
||||
{
|
||||
accessorKey: 'fcr_number',
|
||||
header: 'FCR Number',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
},
|
||||
{
|
||||
accessorKey: 'mortality',
|
||||
header: 'Mortality',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
},
|
||||
];
|
||||
|
||||
const productionStandardColumns: ColumnDef<StandardDetails>[] = [
|
||||
{
|
||||
accessorKey: 'week',
|
||||
@@ -253,36 +234,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
const approveModal = useModal();
|
||||
const rejectModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
const fcrStandardModal = useModal();
|
||||
const productionStandardModal = useModal();
|
||||
|
||||
const [fcrStandards, setFcrStandards] = useState<FcrStandard[]>([]);
|
||||
const [productionStandards, setProductionStandards] =
|
||||
useState<ProductionStandard | null>(null);
|
||||
|
||||
const [isFcrModalOpen, setIsFcrModalOpen] = useState(false);
|
||||
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
|
||||
useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkFcrModalOpen = () => {
|
||||
const isOpen = fcrStandardModal.ref.current?.open || false;
|
||||
setIsFcrModalOpen(isOpen);
|
||||
};
|
||||
|
||||
checkFcrModalOpen();
|
||||
|
||||
const observer = new MutationObserver(checkFcrModalOpen);
|
||||
if (fcrStandardModal.ref.current) {
|
||||
observer.observe(fcrStandardModal.ref.current, {
|
||||
attributes: true,
|
||||
attributeFilter: ['open'],
|
||||
});
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [fcrStandardModal.ref]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkProductionStandardModalOpen = () => {
|
||||
const isOpen = productionStandardModal.ref.current?.open || false;
|
||||
@@ -460,24 +419,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
? projectFlockKandangLookupData.data
|
||||
: undefined;
|
||||
|
||||
const fcrId = useMemo(() => {
|
||||
if (type === 'add') {
|
||||
return projectFlockKandangLookup?.project_flock?.fcr?.id;
|
||||
}
|
||||
return initialValues?.project_flock?.fcr?.id;
|
||||
}, [type, projectFlockKandangLookup, initialValues]);
|
||||
|
||||
const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR(
|
||||
isFcrModalOpen && fcrId ? `fcr-detail-${fcrId}` : null,
|
||||
() => FcrApi.getSingle(fcrId!)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (fcr?.status === 'success') {
|
||||
setFcrStandards((fcr.data as FcrWithStandards).fcr_standards || []);
|
||||
}
|
||||
}, [fcr]);
|
||||
|
||||
const productionStandardId = useMemo(() => {
|
||||
if (type === 'add') {
|
||||
return projectFlockKandangLookup?.project_flock?.production_standard_id;
|
||||
@@ -606,7 +547,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
isLoadingOptions: isLoadingEggProducts,
|
||||
loadMore: loadMoreEggProducts,
|
||||
} = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', {
|
||||
search: 'telur',
|
||||
type: 'TELUR',
|
||||
location_id: eggProductsLocationId,
|
||||
kandang_id: eggProductsKandangId,
|
||||
});
|
||||
@@ -886,20 +827,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
if (isResponseSuccess(eggProductsData) && selectedKandang) {
|
||||
const data = eggProductsData.data as unknown as ProductWarehouse[];
|
||||
data.forEach((product) => {
|
||||
const productName = product.product.name;
|
||||
|
||||
if (
|
||||
productName.toLowerCase().includes('telur') ||
|
||||
productName.toLowerCase().includes('egg') ||
|
||||
productName.toLowerCase().includes('pecah') ||
|
||||
productName.toLowerCase().includes('konsumsi') ||
|
||||
productName.toLowerCase().includes('baik')
|
||||
) {
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: product.product.name,
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: product.product.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1952,24 +1883,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>Standard FCR</span>
|
||||
<div className='mt-1'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color='primary'
|
||||
className={{
|
||||
badge:
|
||||
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
|
||||
}}
|
||||
onClick={() => fcrStandardModal.openModal()}
|
||||
>
|
||||
{projectFlockKandangLookup?.project_flock?.fcr?.name ||
|
||||
initialValues?.project_flock?.fcr?.name ||
|
||||
'-'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Standard Produksi
|
||||
@@ -2160,22 +2073,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>Standard FCR</span>
|
||||
<div className='mt-1'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color='primary'
|
||||
className={{
|
||||
badge:
|
||||
'cursor-pointer hover:opacity-80 transition-opacity whitespace-nowrap',
|
||||
}}
|
||||
onClick={() => fcrStandardModal.openModal()}
|
||||
>
|
||||
{initialValues.project_flock?.fcr?.name || '-'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Standard Produksi
|
||||
@@ -2227,21 +2124,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>FCR (g)</td>
|
||||
<td className='text-center py-3'>
|
||||
<span className='font-semibold'>
|
||||
{initialValues.fcr_value != null
|
||||
? `${formatNumber(initialValues.fcr_value)} g`
|
||||
: '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className='text-center py-3 text-gray-600'>
|
||||
{initialValues.project_flock?.fcr?.fcr_std != null
|
||||
? `${formatNumber(initialValues.project_flock?.fcr?.fcr_std)} g`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='py-3 font-medium'>Feed Intake (g)</td>
|
||||
<td className='text-center py-3'>
|
||||
@@ -2587,6 +2469,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
/>
|
||||
{getStockUsageAdornment(idx)}
|
||||
</div>
|
||||
@@ -2793,6 +2676,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
/>
|
||||
</td>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
@@ -3009,6 +2893,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}}
|
||||
placeholder='Masukkan jumlah telur'
|
||||
inputSuffix={'Butir'}
|
||||
disabled={type === 'detail'}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -3035,6 +2920,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}}
|
||||
placeholder='Masukkan total berat telur (Kilogram)...'
|
||||
inputSuffix='Kilogram'
|
||||
disabled={type === 'detail'}
|
||||
/>
|
||||
</td>
|
||||
{(type as 'add' | 'edit' | 'detail') !==
|
||||
@@ -3283,62 +3169,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* FCR Standard Modal */}
|
||||
<Modal
|
||||
ref={fcrStandardModal.ref}
|
||||
closeOnBackdrop={true}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='mdi:chart-line' width={20} height={20} />
|
||||
<h3 className='font-semibold'>Detail Standard FCR</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={fcrStandardModal.closeModal}
|
||||
className='text-gray-500 hover:text-gray-700 transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='px-4'>
|
||||
{isLoadingFcrStandards ? (
|
||||
<div className='flex justify-center py-8'>
|
||||
<span className='loading loading-spinner loading-lg'></span>
|
||||
</div>
|
||||
) : fcrStandards.length > 0 ? (
|
||||
<Table<FcrStandard>
|
||||
data={fcrStandards}
|
||||
columns={fcrStandardColumns}
|
||||
pageSize={100}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
paginationClassName: 'hidden',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p className='text-sm text-gray-500'>
|
||||
Tidak ada data FCR standards
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Production Standard Modal */}
|
||||
<Modal
|
||||
closeOnBackdrop={true}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -17,16 +18,19 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
||||
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import { ExpenseApi } from '@/services/api/expense';
|
||||
import { Expense } from '@/types/api/expense';
|
||||
import { Color } from '@/types/theme';
|
||||
import Link from 'next/link';
|
||||
|
||||
// ===== STATUS BADGE UTILITIES =====
|
||||
const statusTextMap: Record<string, string> = {
|
||||
@@ -159,27 +163,33 @@ const PurchaseTable = () => {
|
||||
PurchaseApi.getAllFetcher
|
||||
);
|
||||
|
||||
const [isDownloadingInvoice, setIsDownloadingInvoice] = useState(false);
|
||||
const [invoicePurchaseData, setInvoicePurchaseData] =
|
||||
useState<Purchase | null>(null);
|
||||
|
||||
const handleDownloadInvoice = async (purchaseId: number) => {
|
||||
setIsDownloadingInvoice(true);
|
||||
try {
|
||||
const response = await PurchaseApi.getSingle(purchaseId);
|
||||
if (isResponseSuccess(response) && response.data) {
|
||||
setInvoicePurchaseData(response.data);
|
||||
setTimeout(() => {
|
||||
setInvoicePurchaseData(null);
|
||||
}, 1000);
|
||||
}
|
||||
} catch {
|
||||
toast.error('Gagal mengambil data purchase order.');
|
||||
} finally {
|
||||
setIsDownloadingInvoice(false);
|
||||
}
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
previousPageData: BaseApiResponse<Expense>[] | null
|
||||
) => {
|
||||
if (pageIndex > 0 && !previousPageData) return null;
|
||||
return `${ExpenseApi.basePath}?page=${pageIndex + 1}&limit=100`;
|
||||
};
|
||||
|
||||
const { data: expensesPages } = useSWRInfinite(
|
||||
getKey,
|
||||
ExpenseApi.getAllFetcher
|
||||
);
|
||||
|
||||
const expenseMap = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
if (!expensesPages) return map;
|
||||
|
||||
expensesPages.forEach((page) => {
|
||||
if (isResponseSuccess(page)) {
|
||||
page.data.forEach((expense: Expense) => {
|
||||
map.set(expense.reference_number, expense.id);
|
||||
});
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [expensesPages]);
|
||||
|
||||
// ===== TABLE COLUMNS DEFINITION =====
|
||||
const purchaseColumns: ColumnDef<Purchase>[] = [
|
||||
{
|
||||
@@ -191,37 +201,34 @@ const PurchaseTable = () => {
|
||||
},
|
||||
{
|
||||
accessorKey: 'po_expedition',
|
||||
header: 'PO Ekspedisi',
|
||||
header: 'Ekspedisi PO',
|
||||
cell: (props) => {
|
||||
const purchase = props.row.original;
|
||||
|
||||
if (!purchase.po_number || purchase.po_number === 'Belum dibuat') {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
const poExpedition = props.row.original.po_expedition;
|
||||
if (!poExpedition || poExpedition.length === 0) return '-';
|
||||
return (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={() => handleDownloadInvoice(purchase.id)}
|
||||
disabled={isDownloadingInvoice}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
isDownloadingInvoice
|
||||
? 'eos-icons:loading'
|
||||
: 'material-symbols:file-open-outline'
|
||||
<ul className='list-disc pl-4'>
|
||||
{poExpedition.map((exp, index) => {
|
||||
const expenseId = expenseMap.get(exp.refrence);
|
||||
if (expenseId) {
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link
|
||||
href={`/expense/detail/?expenseId=${expenseId}`}
|
||||
className='p-0 h-auto text-primary underline'
|
||||
>
|
||||
{exp.refrence}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{purchase.po_number}
|
||||
</Button>
|
||||
return <li key={index}>{exp.refrence}</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'supplier.name',
|
||||
accessorKey: 'supplier',
|
||||
header: 'Vendor',
|
||||
cell: (props) => props.row.original.supplier.name,
|
||||
},
|
||||
@@ -505,15 +512,6 @@ const PurchaseTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{invoicePurchaseData && (
|
||||
<div className='hidden'>
|
||||
<PurchaseOrderInvoice
|
||||
data={invoicePurchaseData}
|
||||
triggerDownloadOnMount={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -402,6 +402,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
<tbody>
|
||||
{purchaseItems?.map((purchaseItem, idx) => {
|
||||
const formItem = formik.values.items?.[idx];
|
||||
|
||||
const originalPurchaseItem = initialValues?.items?.find(
|
||||
(item) => item.id === purchaseItem.id
|
||||
);
|
||||
const isReceivedQtyDisabled =
|
||||
originalPurchaseItem?.has_chickin === true;
|
||||
|
||||
return (
|
||||
<tr key={`purchase-item-${idx}`}>
|
||||
<td>
|
||||
@@ -580,7 +587,12 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
decimalScale={0}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
bottomLabel={`Total: ${purchaseItems[idx]?.quantity ? formatNumber(purchaseItems[idx].quantity) : 0}`}
|
||||
disabled={isReceivedQtyDisabled}
|
||||
bottomLabel={
|
||||
isReceivedQtyDisabled
|
||||
? 'Sudah chickin, tidak bisa diubah'
|
||||
: `Total: ${purchaseItems[idx]?.quantity ? formatNumber(purchaseItems[idx].quantity) : 0}`
|
||||
}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'received_qty').isError ||
|
||||
(formItem?.received_qty
|
||||
|
||||
@@ -740,6 +740,13 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
)
|
||||
return null;
|
||||
|
||||
const originalPurchaseItem =
|
||||
initialValues?.items?.find(
|
||||
(item) => item.id === purchaseItem.id
|
||||
);
|
||||
const isQtyDisabled =
|
||||
originalPurchaseItem?.has_chickin === true;
|
||||
|
||||
return (
|
||||
<tr key={`purchase-item-${purchaseItem.id}`}>
|
||||
<td>
|
||||
@@ -807,7 +814,12 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
placeholder='Masukkan jumlah'
|
||||
allowNegative={false}
|
||||
decimalScale={0}
|
||||
bottomLabel={`Previous: ${formatNumber(purchaseItem.quantity)}`}
|
||||
disabled={isQtyDisabled}
|
||||
bottomLabel={
|
||||
isQtyDisabled
|
||||
? 'Sudah chickin, tidak bisa diubah'
|
||||
: `Previous: ${formatNumber(purchaseItem.quantity)}`
|
||||
}
|
||||
className={{
|
||||
wrapper: 'min-w-32',
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
@@ -235,16 +235,11 @@ const pdfStyles = StyleSheet.create({
|
||||
interface PurchaseOrderInvoiceProps {
|
||||
data?: Purchase;
|
||||
className?: string;
|
||||
triggerDownloadOnMount?: boolean;
|
||||
}
|
||||
|
||||
const PurchaseOrderInvoice = ({
|
||||
data,
|
||||
triggerDownloadOnMount,
|
||||
}: PurchaseOrderInvoiceProps) => {
|
||||
const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
const [, setIsGeneratingPDF] = useState(false);
|
||||
const purchaseData = data;
|
||||
const hasDownloadedRef = useRef(false);
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
@@ -255,7 +250,7 @@ const PurchaseOrderInvoice = ({
|
||||
);
|
||||
}, [purchaseData?.items]);
|
||||
|
||||
const handleDownloadPDF = useCallback(async () => {
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!purchaseData) {
|
||||
toast.error('No purchase order data available');
|
||||
return;
|
||||
@@ -515,20 +510,7 @@ const PurchaseOrderInvoice = ({
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
}, [purchaseData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerDownloadOnMount && purchaseData && !hasDownloadedRef.current) {
|
||||
hasDownloadedRef.current = true;
|
||||
handleDownloadPDF();
|
||||
}
|
||||
}, [triggerDownloadOnMount, purchaseData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerDownloadOnMount) {
|
||||
hasDownloadedRef.current = false;
|
||||
}
|
||||
}, [triggerDownloadOnMount]);
|
||||
};
|
||||
|
||||
if (!purchaseData) {
|
||||
return (
|
||||
@@ -538,10 +520,6 @@ const PurchaseOrderInvoice = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (triggerDownloadOnMount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return purchaseData?.po_number &&
|
||||
purchaseData.po_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
|
||||
@@ -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;
|
||||
@@ -2,22 +2,26 @@
|
||||
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
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',
|
||||
@@ -34,53 +38,6 @@ const pdfStyles = StyleSheet.create({
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierInfo: {
|
||||
fontSize: 9,
|
||||
marginBottom: 5,
|
||||
color: '#333333',
|
||||
},
|
||||
badge: {
|
||||
backgroundColor: '#1f74bf',
|
||||
color: '#FFFFFF',
|
||||
padding: 2,
|
||||
borderRadius: 2,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
alignSelf: 'center',
|
||||
marginRight: 4,
|
||||
},
|
||||
badgeLunas: {
|
||||
backgroundColor: '#1f74bf',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
badgeBelumLunas: {
|
||||
backgroundColor: '#F97316',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
textError: {
|
||||
color: '#DC2626',
|
||||
},
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -100,194 +57,183 @@ interface CustomerPaymentExportPDFParams {
|
||||
};
|
||||
}
|
||||
|
||||
const getParameterText = (
|
||||
params?: CustomerPaymentExportPDFParams['params']
|
||||
) => {
|
||||
const paramsText = [];
|
||||
|
||||
if (params?.customer_name) {
|
||||
paramsText.push(`Customer: ${params.customer_name}`);
|
||||
} else {
|
||||
paramsText.push('Semua Customer');
|
||||
}
|
||||
|
||||
// TODO: Uncomment when BE is ready
|
||||
// if (params?.sales) {
|
||||
// paramsText.push(`Sales: ${params.sales}`);
|
||||
// }
|
||||
|
||||
if (params?.start_date && params?.end_date) {
|
||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||
const endDate = formatDate(params.end_date, 'DD MMM YYYY');
|
||||
paramsText.push(`Periode: ${startDate} - ${endDate}`);
|
||||
} else if (params?.start_date) {
|
||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||
paramsText.push(`Tanggal: ${startDate}`);
|
||||
}
|
||||
|
||||
const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm');
|
||||
paramsText.push(`Dicetak: ${currentDate}`);
|
||||
|
||||
return paramsText;
|
||||
};
|
||||
|
||||
// Helper functions for PdfTable
|
||||
const getTableColumns = (): PdfColumn[] => [
|
||||
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
|
||||
{ key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' },
|
||||
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 ? (
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.badge,
|
||||
item.status === 'LUNAS'
|
||||
? pdfStyles.badgeLunas
|
||||
: pdfStyles.badgeBelumLunas,
|
||||
]}
|
||||
>
|
||||
<Text>{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}</Text>
|
||||
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
|
||||
style={{
|
||||
backgroundColor: getPDFBadgeStyle(row.status, 'payment').bg,
|
||||
color: getPDFBadgeStyle(row.status, 'payment').text,
|
||||
borderColor: getPDFBadgeStyle(row.status, 'payment').border,
|
||||
}}
|
||||
>
|
||||
{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) => {
|
||||
@@ -302,54 +248,44 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
>
|
||||
{/* Title and Parameters */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > Kontrol Pembayaran Customer
|
||||
</Text>
|
||||
</PdfTypography>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Periode:{' '}
|
||||
{params.params?.start_date
|
||||
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||
: '-'}{' '}
|
||||
s.d{' '}
|
||||
{params.params?.end_date
|
||||
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<PdfParamBadge>
|
||||
Periode:{' '}
|
||||
{params.params?.start_date
|
||||
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||
: '-'}{' '}
|
||||
s.d{' '}
|
||||
{params.params?.end_date
|
||||
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</PdfParamBadge>
|
||||
{/* TODO: Uncomment when BE is ready */}
|
||||
{/* <View style={pdfStyles.parameterBadge}>
|
||||
<Text>Filter Tanggal: Tanggal DO</Text>
|
||||
</View> */}
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Customer: {params.params?.customer_name || 'Semua Customer'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
{/* <PdfParamBadge>
|
||||
Filter Tanggal: Tanggal DO
|
||||
</PdfParamBadge> */}
|
||||
<PdfParamBadge>
|
||||
Customer: {params.params?.customer_name || 'Semua Customer'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</PdfParamBadge>
|
||||
</View>
|
||||
<Text style={pdfStyles.supplierTitle}>
|
||||
<PdfTypography size='h2' variant='primary'>
|
||||
{customerReport.customer.name}
|
||||
</Text>
|
||||
<Text style={pdfStyles.supplierInfo}>
|
||||
</PdfTypography>
|
||||
<PdfTypography size='label'>
|
||||
Alamat: {customerReport.customer.address || '-'}
|
||||
</Text>
|
||||
</PdfTypography>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
<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
|
||||
@@ -357,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 },
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
@@ -12,53 +11,18 @@ import {
|
||||
|
||||
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
|
||||
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
|
||||
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
|
||||
import { PdfTable, PdfColumn } 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,
|
||||
@@ -69,133 +33,6 @@ const pdfStyles = StyleSheet.create({
|
||||
titleSection: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
mainTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
supplierInfo: {
|
||||
fontSize: 9,
|
||||
marginBottom: 5,
|
||||
color: '#333333',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'left',
|
||||
},
|
||||
tableCellNo: {
|
||||
flex: 0.5,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableCellHeaderRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
textAlign: 'right',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
paddingVertical: 12,
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellCenter: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 4,
|
||||
fontSize: 7,
|
||||
textAlign: 'center',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
summaryRow: {
|
||||
backgroundColor: '#F0F0F0',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
badge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 12,
|
||||
fontSize: 5,
|
||||
fontWeight: 'bold',
|
||||
borderWidth: 1,
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
parameterBadge: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
color: '#333333',
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
fontSize: 8,
|
||||
marginRight: 8,
|
||||
marginBottom: 4,
|
||||
},
|
||||
parameterContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -203,6 +40,225 @@ const pdfStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => {
|
||||
type DebtRow = DebtSupplier['rows'][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',
|
||||
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',
|
||||
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',
|
||||
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: 'due_date',
|
||||
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',
|
||||
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
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{(row as unknown as DebtRow).due_status}
|
||||
</PdfStatusBadge>
|
||||
</View>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
footer: '',
|
||||
},
|
||||
{
|
||||
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: '',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
interface DebtSupplierExportPDFParams {
|
||||
data: DebtSupplier[];
|
||||
params?: {
|
||||
@@ -219,418 +275,70 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
{params.data.map((supplierReport, supplierIndex) => (
|
||||
<Page
|
||||
key={supplierIndex}
|
||||
size='A4'
|
||||
size='A3'
|
||||
orientation='landscape'
|
||||
style={pdfStyles.page}
|
||||
>
|
||||
{/* Title and Supplier Info */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
<PdfTypography size='h1' variant='primary'>
|
||||
Laporan > Rekapitulasi Hutang ke Supplier
|
||||
</Text>
|
||||
</PdfTypography>
|
||||
<View style={pdfStyles.parameterContainer}>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Periode:{' '}
|
||||
{params.params?.start_date
|
||||
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||
: '-'}{' '}
|
||||
s.d{' '}
|
||||
{params.params?.end_date
|
||||
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<PdfParamBadge>
|
||||
Periode:{' '}
|
||||
{params.params?.start_date
|
||||
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||
: '-'}{' '}
|
||||
s.d{' '}
|
||||
{params.params?.end_date
|
||||
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</PdfParamBadge>
|
||||
{params.params?.filter_by && (
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Filter Tanggal:{' '}
|
||||
{params.params.filter_by === 'po_date'
|
||||
? 'Tanggal PO'
|
||||
: params.params.filter_by === 'received_date'
|
||||
? 'Tanggal Terima'
|
||||
: params.params.filter_by === 'due_date'
|
||||
? 'Tanggal Jatuh Tempo'
|
||||
: params.params.filter_by}
|
||||
</Text>
|
||||
</View>
|
||||
<PdfParamBadge>
|
||||
Filter Tanggal:{' '}
|
||||
{params.params.filter_by === 'po_date'
|
||||
? 'Tanggal PO'
|
||||
: params.params.filter_by === 'received_date'
|
||||
? 'Tanggal Terima'
|
||||
: params.params.filter_by === 'due_date'
|
||||
? 'Tanggal Jatuh Tempo'
|
||||
: params.params.filter_by}
|
||||
</PdfParamBadge>
|
||||
)}
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</Text>
|
||||
</View>
|
||||
<PdfParamBadge>
|
||||
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
|
||||
</PdfParamBadge>
|
||||
<PdfParamBadge>
|
||||
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||
</PdfParamBadge>
|
||||
</View>
|
||||
<Text style={pdfStyles.supplierTitle}>
|
||||
<PdfTypography size='h2' variant='primary'>
|
||||
{supplierReport.supplier.name}
|
||||
</Text>
|
||||
<Text style={pdfStyles.supplierInfo}>
|
||||
</PdfTypography>
|
||||
<PdfTypography size='label'>
|
||||
{supplierReport.supplier.category}
|
||||
</Text>
|
||||
</PdfTypography>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||
<Text>No</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>No. PR</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>No. PO</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
|
||||
<Text>Tgl Terima/Bayar</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
|
||||
<Text>Tgl PO</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.6 }]}>
|
||||
<Text>Aging</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Area</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Gudang</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<Text>Jatuh Tempo</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
|
||||
<Text>Status Jatuh Tempo</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Nominal Pembelian (Rp)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Pembayaran (Rp)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||
<Text>Sisa Saldo Hutang (Rp)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Status</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{ flex: 1, borderRight: 'none' },
|
||||
]}
|
||||
>
|
||||
<Text>No. Perjalanan</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Initial Balance Row */}
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text></Text> {/* NO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text> {/* No. PR */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text> {/* No. PO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
|
||||
<Text></Text> {/* Tgl Terima/Bayar */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
|
||||
<Text></Text> {/* Tgl PO */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||
<Text></Text> {/* Aging */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text> {/* Area */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text> {/* Gudang */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text></Text> {/* Jatuh Tempo */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
<Text></Text> {/* Status Jatuh Tempo */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text></Text> {/* Nominal Pembelian (Rp) */}
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||
<Text></Text> {/* Pembayaran (Rp) */}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
<PdfTable
|
||||
columns={getTableColumns(supplierReport.total)}
|
||||
data={supplierReport.rows as unknown as Record<string, unknown>[]}
|
||||
showFooter={!!supplierReport.total}
|
||||
firstRow={
|
||||
typeof supplierReport.initial_balance === 'number' &&
|
||||
supplierReport.initial_balance !== 0
|
||||
? {
|
||||
valueKey: 'balance',
|
||||
value: supplierReport.initial_balance,
|
||||
align: 'right',
|
||||
color: supplierReport.initial_balance < 0 ? 'red' : 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{' '}
|
||||
{/* Sisa Saldo Hutang (Rp) */}
|
||||
{formatCurrency(supplierReport.initial_balance || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text></Text> {/* Status */}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCell, // No. Perjalanan
|
||||
{ flex: 1, borderRight: 'none' },
|
||||
]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
{supplierReport.rows.map((item, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < supplierReport.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.pr_number || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.po_number || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
|
||||
<Text>
|
||||
{item.received_date
|
||||
? item.received_date != '-'
|
||||
? formatDate(item.received_date, 'DD MMM YY')
|
||||
: '-'
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
|
||||
<Text>
|
||||
{item.po_date
|
||||
? item.po_date != '-'
|
||||
? formatDate(item.po_date, 'DD MMM YY')
|
||||
: '-'
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||
<Text>{formatNumber(item.aging)} Hari</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.area?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text>{item.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||
<Text>
|
||||
{item.due_date
|
||||
? item.due_date != '-'
|
||||
? formatDate(item.due_date, 'DD MMM YY')
|
||||
: '-'
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
{item.due_status && item.due_status !== '-' ? (
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.badge,
|
||||
{
|
||||
backgroundColor: getPDFBadgeStyle(
|
||||
item.due_status,
|
||||
'due'
|
||||
).bg,
|
||||
borderColor: getPDFBadgeStyle(item.due_status, 'due')
|
||||
.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: getPDFBadgeStyle(item.due_status, 'due').text,
|
||||
}}
|
||||
>
|
||||
{item.due_status}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
color: item.total_price < 0 ? 'red' : 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(item.total_price)}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
color: item.payment_price < 0 ? 'red' : 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(item.payment_price)}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{ flex: 1.5, color: item.balance < 0 ? 'red' : 'black' },
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(item.balance)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
{item.status && item.status !== '-' ? (
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.badge,
|
||||
{
|
||||
backgroundColor: getPDFBadgeStyle(
|
||||
item.status,
|
||||
'payment'
|
||||
).bg,
|
||||
borderColor: getPDFBadgeStyle(item.status, 'payment')
|
||||
.border,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: getPDFBadgeStyle(item.status, 'payment').text,
|
||||
}}
|
||||
>
|
||||
{item.status}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCell, // No. Perjalanan
|
||||
{ flex: 1, borderRight: 'none' },
|
||||
]}
|
||||
>
|
||||
<Text>{item.travel_number || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Summary Row */}
|
||||
{supplierReport.total && (
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.summaryRow]}>
|
||||
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||
<Text>Total</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||
<Text>{formatNumber(supplierReport.total.aging)} Hari</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
color:
|
||||
supplierReport.total.total_price < 0 ? 'red' : 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatCurrency(supplierReport.total.total_price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
color:
|
||||
supplierReport.total.payment_price < 0
|
||||
? 'red'
|
||||
: 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatCurrency(supplierReport.total.payment_price)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{
|
||||
flex: 1.5,
|
||||
color:
|
||||
supplierReport.total.debt_price < 0 ? 'red' : 'black',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Page>
|
||||
))}
|
||||
</Document>
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
};
|
||||
+1
-11
@@ -220,7 +220,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
'lti.master.area.list',
|
||||
'lti.master.banks.list',
|
||||
'lti.master.customer.list',
|
||||
'lti.master.fcr.list',
|
||||
'lti.master.flocks.list',
|
||||
'lti.master.kandangs.list',
|
||||
'lti.master.locations.list',
|
||||
@@ -283,11 +282,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
||||
link: '/master-data/nonstock',
|
||||
permission: ['lti.master.nonstocks.list'],
|
||||
},
|
||||
{
|
||||
text: 'FCR',
|
||||
link: '/master-data/fcr',
|
||||
permission: ['lti.master.fcr.list'],
|
||||
},
|
||||
{
|
||||
text: 'Supplier',
|
||||
link: '/master-data/supplier',
|
||||
@@ -512,13 +506,9 @@ export const FILTER_TYPE_OPTIONS = [
|
||||
];
|
||||
|
||||
export const MARKETING_TYPE_OPTIONS = [
|
||||
{
|
||||
label: 'Ayam Pullet',
|
||||
value: 'AYAM_PULLET',
|
||||
},
|
||||
{
|
||||
label: 'Ayam',
|
||||
value: 'AYAM',
|
||||
value: 'AYAM,AYAM_PULLET',
|
||||
},
|
||||
{
|
||||
label: 'Trading',
|
||||
|
||||
@@ -195,11 +195,6 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/master-data/nonstock/detail/': ['lti.master.nonstocks.detail'],
|
||||
'/master-data/nonstock/detail/edit/': ['lti.master.nonstocks.update'],
|
||||
|
||||
'/master-data/fcr/': ['lti.master.fcr.list'],
|
||||
'/master-data/fcr/add/': ['lti.master.fcr.create'],
|
||||
'/master-data/fcr/detail/': ['lti.master.fcr.detail'],
|
||||
'/master-data/fcr/detail/edit/': ['lti.master.fcr.update'],
|
||||
|
||||
'/master-data/supplier/': ['lti.master.suppliers.list'],
|
||||
'/master-data/supplier/add/': ['lti.master.suppliers.create'],
|
||||
'/master-data/supplier/detail/': ['lti.master.suppliers.detail'],
|
||||
|
||||
@@ -40,6 +40,7 @@ export const safeRound = (num: number, decimals: number) => {
|
||||
export const formatTitleCase = (value: string) => {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
@@ -15,7 +15,7 @@ export type MarketingFormValues = {
|
||||
total_price?: string | number;
|
||||
marketing_type?: { value: string; label: string } | null;
|
||||
convertion_unit?: { value: string; label: string } | null;
|
||||
week?: { value?: number; label?: string } | null;
|
||||
week?: number | null;
|
||||
weight_per_convertion?: number | null;
|
||||
price_per_convertion?: number | null;
|
||||
total_peti?: number | null;
|
||||
@@ -100,7 +100,7 @@ export const calculateAyamPullet = (
|
||||
): void => {
|
||||
const { values, setFieldValue } = ctx;
|
||||
const unitPrice = Number(values.unit_price || 0);
|
||||
const week = Number(values.week?.value || 0);
|
||||
const week = Number(values.week || 0);
|
||||
const qty = Number(values.qty || 0);
|
||||
const avgWeight = Number(values.avg_weight || 0);
|
||||
const totalWeight = Number(values.total_weight || 0);
|
||||
|
||||
@@ -54,11 +54,7 @@ import {
|
||||
CreateBankPayload,
|
||||
UpdateBankPayload,
|
||||
} from '@/types/api/master-data/bank';
|
||||
import {
|
||||
CreateFcrPayload,
|
||||
Fcr,
|
||||
UpdateFcrPayload,
|
||||
} from '@/types/api/master-data/fcr';
|
||||
|
||||
import {
|
||||
CreateFlockPayload,
|
||||
Flock,
|
||||
@@ -131,12 +127,6 @@ export const BankApi = new BaseApiService<
|
||||
UpdateBankPayload
|
||||
>('/master-data/banks');
|
||||
|
||||
export const FcrApi = new BaseApiService<
|
||||
Fcr,
|
||||
CreateFcrPayload,
|
||||
UpdateFcrPayload
|
||||
>('/master-data/fcrs');
|
||||
|
||||
export const FlockApi = new BaseApiService<
|
||||
Flock,
|
||||
CreateFlockPayload,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { createProjectFlockSlice } from '@/stores/project-flock/slices/project-flock.slice';
|
||||
import { ProjectFlockSlice } from '@/types/stores';
|
||||
|
||||
export type ProjectFlockStore = ProjectFlockSlice;
|
||||
|
||||
export const useProjectFlockStore = create<ProjectFlockStore>()(
|
||||
devtools(
|
||||
(...args) => ({
|
||||
...createProjectFlockSlice(...args),
|
||||
}),
|
||||
{
|
||||
name: 'ProjectFlockStore',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ProjectFlockSlice } from '@/types/stores';
|
||||
import { StateCreator } from 'zustand';
|
||||
|
||||
export const createProjectFlockSlice: StateCreator<
|
||||
ProjectFlockSlice,
|
||||
[],
|
||||
[],
|
||||
ProjectFlockSlice
|
||||
> = (set) => ({
|
||||
// Initial state
|
||||
isSuccess: false,
|
||||
createdProjectFlock: null,
|
||||
|
||||
// Actions
|
||||
setIsSuccess: (success) => set({ isSuccess: success }),
|
||||
|
||||
setCreatedProjectFlock: (data) => set({ createdProjectFlock: data }),
|
||||
|
||||
resetProjectFlock: () =>
|
||||
set({
|
||||
isSuccess: false,
|
||||
createdProjectFlock: null,
|
||||
}),
|
||||
});
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { Flock } from '@/types/api/master-data/flock';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
|
||||
@@ -11,6 +11,7 @@ export type BaseProductWarehouse = {
|
||||
quantity: number;
|
||||
product: Product;
|
||||
warehouse: Warehouse;
|
||||
week?: number | null;
|
||||
};
|
||||
|
||||
export type ProductWarehouse = BaseMetadata & BaseProductWarehouse;
|
||||
|
||||
+2
@@ -17,6 +17,8 @@ export type BaseMarketing = {
|
||||
status?: string;
|
||||
so_number: string;
|
||||
so_date: string;
|
||||
do_number?: string;
|
||||
do_date?: string;
|
||||
customer: Customer;
|
||||
sales_person: CreatedUser;
|
||||
notes: string;
|
||||
|
||||
Vendored
-30
@@ -1,30 +0,0 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
|
||||
export type BaseFcr = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type FcrStandard = {
|
||||
id: number;
|
||||
weight: number;
|
||||
fcr_number: number;
|
||||
mortality: number;
|
||||
};
|
||||
|
||||
export type Fcr = BaseMetadata & BaseFcr;
|
||||
|
||||
export type FcrWithStandards = Fcr & {
|
||||
fcr_standards: FcrStandard[];
|
||||
};
|
||||
|
||||
export type CreateFcrPayload = {
|
||||
name: string;
|
||||
fcr_standards: {
|
||||
weight: number;
|
||||
fcr_number: number;
|
||||
mortality: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateFcrPayload = CreateFcrPayload;
|
||||
-4
@@ -1,5 +1,4 @@
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { Flock } from '@/types/api/master-data/flock';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
@@ -16,8 +15,6 @@ export type BaseProjectFlock = {
|
||||
area: Area;
|
||||
area_id: number;
|
||||
category: string;
|
||||
fcr: Fcr;
|
||||
fcr_id: number;
|
||||
production_standard: ProductionStandard;
|
||||
production_standard_id: number;
|
||||
location: Location;
|
||||
@@ -51,7 +48,6 @@ export type CreateProjectFlockPayload = {
|
||||
flock_name: string;
|
||||
area_id: number;
|
||||
category: string;
|
||||
fcr_id: number;
|
||||
production_standard_id: number;
|
||||
location_id: number;
|
||||
kandang_ids: number[];
|
||||
|
||||
Vendored
+2
-1
@@ -50,6 +50,7 @@ export type PurchaseItem = {
|
||||
expedition_vendor_name?: string | null;
|
||||
received_qty?: number | null;
|
||||
transport_per_item?: number | null;
|
||||
has_chickin?: boolean;
|
||||
expedition_vendor?: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
@@ -76,7 +77,7 @@ export type BasePurchase = {
|
||||
items?: PurchaseItem[];
|
||||
latest_approval?: BaseApproval;
|
||||
requester_name?: string;
|
||||
po_expedition?: string[];
|
||||
po_expedition?: { id: number; refrence: string }[];
|
||||
created_user?: CreatedUser;
|
||||
products?: PurchaseItemProduct[];
|
||||
};
|
||||
|
||||
Vendored
+12
@@ -5,6 +5,7 @@ import type {
|
||||
UniformityDetail,
|
||||
VerifyUniformityResponse,
|
||||
} from '@/types/api/production/uniformity';
|
||||
import type { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
|
||||
type MainUiSlice = {
|
||||
mainDrawerOpen: boolean;
|
||||
@@ -97,3 +98,14 @@ export type DashboardFilterSlice = {
|
||||
setFilterValues: (values: DashboardFilterType) => void;
|
||||
resetFilterValues: () => void;
|
||||
};
|
||||
|
||||
export type ProjectFlockSlice = {
|
||||
// State
|
||||
isSuccess: boolean;
|
||||
createdProjectFlock: ProjectFlock | null;
|
||||
|
||||
// Actions
|
||||
setIsSuccess: (success: boolean) => void;
|
||||
setCreatedProjectFlock: (data: ProjectFlock | null) => void;
|
||||
resetProjectFlock: () => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user