diff --git a/src/app/master-data/fcr/add/page.tsx b/src/app/master-data/fcr/add/page.tsx deleted file mode 100644 index 9a74034d..00000000 --- a/src/app/master-data/fcr/add/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; - -const AddFcr = () => { - return ( -
- -
- ); -}; - -export default AddFcr; diff --git a/src/app/master-data/fcr/detail/edit/page.tsx b/src/app/master-data/fcr/detail/edit/page.tsx deleted file mode 100644 index 54277e8a..00000000 --- a/src/app/master-data/fcr/detail/edit/page.tsx +++ /dev/null @@ -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 | undefined - > - ); - - if (!fcrId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingFcr && } - {!isLoadingFcr && isResponseSuccess(fcr) && ( - - )} -
- ); -}; - -export default FcrEdit; diff --git a/src/app/master-data/fcr/detail/layout.tsx b/src/app/master-data/fcr/detail/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/master-data/fcr/detail/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/master-data/fcr/detail/page.tsx b/src/app/master-data/fcr/detail/page.tsx deleted file mode 100644 index 5db1ab32..00000000 --- a/src/app/master-data/fcr/detail/page.tsx +++ /dev/null @@ -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 | undefined - > - ); - - if (!fcrId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoadingFcr && } - {!isLoadingFcr && isResponseSuccess(fcr) && ( - - )} -
- ); -}; - -export default FcrDetail; diff --git a/src/app/master-data/fcr/page.tsx b/src/app/master-data/fcr/page.tsx deleted file mode 100644 index 9ca9c55d..00000000 --- a/src/app/master-data/fcr/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable'; - -const Fcr = () => { - return ( -
- -
- ); -}; - -export default Fcr; diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 52a3d4dd..87ed7a1a 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,4 +1,4 @@ -import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; const MarketingReportPage = () => { return ( diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index 691ea734..cdac598c 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -2,7 +2,7 @@ import ProductionResultContent from '@/components/pages/report/production-result const ProductionResultReportPage = () => { return ( -
+
); diff --git a/src/components/helper/pdf/badge/PdfParamBadge.tsx b/src/components/helper/pdf/badge/PdfParamBadge.tsx new file mode 100644 index 00000000..fbc2cf2a --- /dev/null +++ b/src/components/helper/pdf/badge/PdfParamBadge.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/src/components/helper/pdf/badge/PdfStatusBadge.tsx b/src/components/helper/pdf/badge/PdfStatusBadge.tsx new file mode 100644 index 00000000..23c9c5e9 --- /dev/null +++ b/src/components/helper/pdf/badge/PdfStatusBadge.tsx @@ -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; + 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 + ); + + return ( + 0 ? [viewStyle as Style] : []), + ]} + > + + {children} + + + ); +}; diff --git a/src/components/helper/pdf/layout/PdfPageNumber.tsx b/src/components/helper/pdf/layout/PdfPageNumber.tsx new file mode 100644 index 00000000..977cac89 --- /dev/null +++ b/src/components/helper/pdf/layout/PdfPageNumber.tsx @@ -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 ( + + + format + .replace('{pageNumber}', String(pageNumber)) + .replace('{totalPages}', String(totalPages)) + } + fixed + /> + + ); +}; diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx index 27369db5..86f4ed77 100644 --- a/src/components/helper/pdf/table/PdfTable.tsx +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -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> { + columns: PdfColumn[]; + data: TData[]; + showFooter?: boolean; footerLabel?: string; firstRow?: { valueKey: string; @@ -26,20 +27,26 @@ interface PdfTableProps { }; } -export const PdfTable = ({ +export const PdfTable = ,>({ columns, data, - footer, + showFooter = false, footerLabel = 'Total', firstRow, -}: PdfTableProps) => { +}: PdfTableProps) => { + // Check if any column has footer defined + const hasFooter = + showFooter || columns.some((col) => col.footer !== undefined); + return ( - - - {footer && footer.length > 0 && ( - + + + {hasFooter && data.length > 0 && ( + )} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx index fee79726..cc9fe41d 100644 --- a/src/components/helper/pdf/table/PdfTbody.tsx +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -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> { + columns: PdfColumn[]; + 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 = ,>({ + columns, + data, + firstRow, +}: PdfTbodyProps) => { return ( <> {/* First Row */} @@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { {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 ( - {isfirstRowColumn ? firstRow.value : ''} + {isFirstRowColumn ? firstRow.value : ''} ); })} @@ -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 ( { ]} > {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)[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 ( - {cell?.value !== undefined && - cell?.value !== null && - cell?.value !== '' ? ( - typeof cell.value === 'object' ? ( - cell.value - ) : ( - {String(cell.value)} - ) + {typeof cellContent === 'string' || + typeof cellContent === 'number' ? ( + {String(cellContent)} ) : ( - - + cellContent )} ); @@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTfoot.tsx b/src/components/helper/pdf/table/PdfTfoot.tsx index a9f209b1..9d974f38 100644 --- a/src/components/helper/pdf/table/PdfTfoot.tsx +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -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> { + columns: PdfColumn[]; + data: TData[]; label?: string; } -export const PdfTfoot = ({ +export const PdfTfoot = ,>({ columns, - cells, + data, label = 'Total', -}: PdfTfootProps) => { +}: PdfTfootProps) => { return ( {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 ( - {column.key === 'no' ? label : cellData?.value || ''} + {displayContent !== undefined && displayContent !== null ? ( + typeof displayContent === 'string' || + typeof displayContent === 'number' ? ( + {String(displayContent)} + ) : ( + displayContent + ) + ) : ( + - + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfThead.tsx b/src/components/helper/pdf/table/PdfThead.tsx index 89037216..889f9f34 100644 --- a/src/components/helper/pdf/table/PdfThead.tsx +++ b/src/components/helper/pdf/table/PdfThead.tsx @@ -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> { + columns: PdfColumn[]; + data?: TData[]; } -export const PdfThead = ({ columns }: PdfTheadProps) => { +export const PdfThead = ,>({ + columns, + data, +}: PdfTheadProps) => { return ( {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 ( - {column.header} + {typeof headerContent === 'string' ? ( + {headerContent} + ) : ( + headerContent + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/index.ts b/src/components/helper/pdf/table/index.ts index 35839f17..3c780688 100644 --- a/src/components/helper/pdf/table/index.ts +++ b/src/components/helper/pdf/table/index.ts @@ -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'; diff --git a/src/components/helper/pdf/table/types.ts b/src/components/helper/pdf/table/types.ts new file mode 100644 index 00000000..c2437f13 --- /dev/null +++ b/src/components/helper/pdf/table/types.ts @@ -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> { + 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 }; diff --git a/src/components/helper/pdf/typography/PdfTypography.tsx b/src/components/helper/pdf/typography/PdfTypography.tsx new file mode 100644 index 00000000..43aac19a --- /dev/null +++ b/src/components/helper/pdf/typography/PdfTypography.tsx @@ -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 = { + 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 ( + + {children} + + ); +}; diff --git a/src/components/helper/pdf/utils/pdf-badge.ts b/src/components/helper/pdf/utils/pdf-badge.ts new file mode 100644 index 00000000..4b26b4eb --- /dev/null +++ b/src/components/helper/pdf/utils/pdf-badge.ts @@ -0,0 +1,65 @@ +export type StatusColor = { + bg: string; + text: string; + border: string; +}; + +// Due status colors (for debt supplier reports) +export const dueStatusColors: Record = { + '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 = { + '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; +}; diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index a79054dd..38be09e4 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -246,8 +246,8 @@ const SelectInput = (props: SelectInputProps) => { 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 = (props: SelectInputProps) => { 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 = (props: SelectInputProps) => { 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 = (props: SelectInputProps) => { 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'), diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 89d4f059..b7d0984f 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -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 = ({
diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 6d992e30..e141ad67 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -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', diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 7c953fe8..f1d5e3cc 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -59,6 +59,10 @@ const DeliveryOrderFormModal = ({ const modalAction = searchParams.get('action'); const marketingId = searchParams.get('id'); + const [currentModalAction, setCurrentModalAction] = useState( + 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(null); const textareaRef = useRef(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 = ({ - No. Sales Order + No. Order - {marketing.data.so_number} + {marketing.data.do_number + ? marketing.data.do_number + : marketing.data.so_number} @@ -667,13 +781,7 @@ const DeliveryOrderFormModal = ({
)}
@@ -715,31 +828,40 @@ const DeliveryOrderFormModal = ({ />
)} - {step === 1 && ( -
- - -
- )} + {step === 1 && + marketing?.data?.latest_approval?.step_number !== 3 && ( +
+ + +
+ )} )} @@ -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 = ({ }, }} > - +
+ +
+ + ); }; diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 3c59e07e..3f56854e 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -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; @@ -31,25 +33,59 @@ const MarketingFilterModal = ({ // ===== OPTIONS ===== const { - options: productsOptions, + rawData: productsRawData, isLoadingOptions: isLoadingProductsOptions, setInputValue: setProductsInputValue, loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', '', { + } = useSelect(MarketingApi.basePath, 'id', 'so_number', '', { limit: 'limit', }); + + const productsOptions = useMemo(() => { + if (!productsRawData || !isResponseSuccess(productsRawData)) return []; + + const productsMap = new Map(); + + 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} diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 0a35a8bc..6368df11 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -109,7 +109,9 @@ const RowsOptionsMenu = ({ className='p-3 justify-start text-sm font-semibold w-full' > - Deliver Item + {props.row.original.latest_approval.step_number == 2 + ? 'Deliver Item' + : 'Edit Delivery'} @@ -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' } diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index 66acc440..5c4d6bb2 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -63,6 +63,10 @@ const SalesOrderFormModal = ({ const modalAction = searchParams.get('action'); const marketingId = searchParams.get('id'); + const [currentModalAction, setCurrentModalAction] = useState( + 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 = ({ }, }} > - +
+ +
= @@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema 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(), }), }); diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 850d88d2..f233521c 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -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; + isLoading?: boolean; }) => { const [formikErrorMessage, setFormErrorMessage] = useState(''); const [selectedProduct, setSelectedProduct] = useState( @@ -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' && ( - { - 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 = ({
+
+ )} + + + + <> + + Gudang + + {doItem?.warehouse?.name || + item.marketing_product?.product_warehouse_data?.warehouse?.name} + + + + Produk + + {item.marketing_product?.product_warehouse?.label} + + + + Qty + + {item.qty + ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` + : '-'} + + + {Number(item.avg_weight ?? 0) > 0 && ( + + Avg Bobot + + {formatNumber(Number(item.avg_weight))} Kg + + + )} + {Number(item.total_weight ?? 0) > 0 && ( + + Total Bobot + + {formatNumber(Number(item.total_weight))} + + + )} + + Total Harga Satuan + + {formatCurrency(parseFloat(item.unit_price as string))} + + + + Total Penjualan + + {formatCurrency(parseFloat(item.total_price as string))} + + + + + + Label + + +
+
Value
+
+ + + <> + {approvalStepNumber !== 1 && ( + + Tanggal Pengiriman + + {item.delivery_date ? ( + formatDate(item.delivery_date, 'DD MMM YYYY') + ) : formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail' ? ( + { + onEditRef.current(item.id as number, item); + }} + > + Belum diisi + + ) : ( + Belum diisi + )} + + + )} + {item.do_number && ( + + No. Pengiriman + {item.do_number} + + )} + + No. Polisi + {item.vehicle_number} + + {doItem && ( + + Dokumen Pengiriman + + + + + )} + + + ); + }; + return ( <>
- {data.map((item) => { - const doItem = marketing?.delivery_order?.find( - (doItem) => doItem.do_number === item.do_number - ); - return ( -
- ( +
+ {formType === 'success' ? ( +
+
+ {renderTableContent(item)} +
+
+ ) : ( + - - - - Label - - -
-
Value
- {(formType === 'add_delivery' || - formType === 'edit_delivery' || - formType === 'detail') && ( -
- - -
- )} -
- - - <> - - Tanggal Pengiriman - - {item.delivery_date ? ( - formatDate(item.delivery_date, 'DD MMM YYYY') - ) : ( - Belum diisi - )} - - - {item.do_number && ( - - No. Pengiriman - {item.do_number} - - )} - - No. Polisi - - {item.vehicle_number} - - - - Gudang - - {item.marketing_product?.product_warehouse?.label} - - - - Produk - - {item.marketing_product?.product_warehouse?.label} - - - - Qty - - {item.qty - ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` - : '-'} - - - - Avg Bobot - - {item.avg_weight - ? formatNumber( - parseFloat(item.avg_weight as string) - ) + ' Kg' - : '-'} - - - - Total Bobot - - {formatNumber(parseFloat(item.total_weight as string))} - - - - Total Harga Satuan - - {formatCurrency(parseFloat(item.unit_price as string))} - - - - Total Penjualan - - {formatCurrency(parseFloat(item.total_price as string))} - - - {doItem && ( - - - Dokumen Pengiriman - - - - - - )} - - - -
- ); - })} + + {renderTableContent(item)} +
+ + )} + + ))} ); diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 6f667f76..18f6145b 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -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; - }) => ( -
- + const renderTableContent = (item: SalesOrderProductFormValues) => ( + <> + + + Label + + +
+
Value
+ {formType !== 'success' && ( +
+ + +
+ )}
- ), - cell: ({ row }: { row: TanStack.Row }) => ( -
- -
- ), - }, - { - 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 }) => - 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 - ) => ( -
- - -
- ), - }, - ], - [] + + + <> + + No. Polisi + {item.vehicle_number} + + + Gudang + {item.kandang?.label} + + + Kategori + {item.marketing_type?.label} + + + Produk + {item.product_warehouse?.label} + + {item.marketing_type?.value.toLowerCase() === 'telur' && ( + + Tipe Konversi + {item.convertion_unit?.label} + + )} + {item.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( + + Tipe Konversi + Week {item.week} + + )} + {item.convertion_unit?.value.toLowerCase() === 'peti' && ( + + Total Peti + + {item.total_peti} {item.convertion_unit?.label} + + + )} + {item.marketing_type?.value.toLowerCase() !== 'trading' && ( + <> + + Total Bobot + + {item.total_weight + ? formatNumber(parseFloat(item.total_weight as string)) + + ' Kg' + : '0 Kg'} + + + + Avg Bobot + + {item.avg_weight + ? formatNumber(parseFloat(item.avg_weight as string)) + ' Kg' + : '0 Kg'} + + + + )} + + + {item.marketing_type?.value === 'telur' + ? 'Total Butir Telur' + : 'Qty'} + + + {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} + + + + Harga Satuan + + {formatCurrency(parseFloat(item.unit_price as string))} + + + + Total Penjualan + + {formatCurrency(parseFloat(item.total_price as string))} + + + + ); return ( <>
{data.map((item) => ( -
- - - - - - - <> - - - - - - - - - - - - - - - - - {item.marketing_type?.value.toLowerCase() === 'telur' && ( - - - - - )} - {item.marketing_type?.value.toLowerCase() === - 'ayam_pullet' && ( - - - - - )} - {item.convertion_unit?.value.toLowerCase() === 'peti' && ( - - - - - )} - {item.marketing_type?.value.toLowerCase() !== 'trading' && ( - <> - - - - - - - - - - )} - - - - - - - - - - - - - - -
- Label - -
-
Value
- {formType !== 'success' && ( -
- - -
- )} -
-
No. Polisi{item.vehicle_number}
Gudang{item.kandang?.label}
Kategori - {item.marketing_type?.label} -
Produk - {item.product_warehouse?.label} -
Tipe Konversi - {item.convertion_unit?.label} -
Tipe Konversi{item.week?.label}
Total Peti - {item.total_peti} {item.convertion_unit?.label} -
Total Bobot - {item.total_weight - ? formatNumber( - parseFloat(item.total_weight as string) - ) + ' Kg' - : '0 Kg'} -
Avg Bobot - {item.avg_weight - ? formatNumber( - parseFloat(item.avg_weight as string) - ) + ' Kg' - : '0 Kg'} -
- {item.marketing_type?.value === 'telur' - ? 'Total Butir Telur' - : 'Qty'} - - {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} -
Harga Satuan - {formatCurrency(parseFloat(item.unit_price as string))} -
Total Penjualan - {formatCurrency(parseFloat(item.total_price as string))} -
+
+ {formType === 'success' ? ( +
+ + {renderTableContent(item)} +
+
+ ) : ( + + + {renderTableContent(item)} +
+
+ )}
))} {formType != 'add_deliver' && diff --git a/src/components/pages/master-data/fcr/FcrsTable.tsx b/src/components/pages/master-data/fcr/FcrsTable.tsx deleted file mode 100644 index 2eb8d8da..00000000 --- a/src/components/pages/master-data/fcr/FcrsTable.tsx +++ /dev/null @@ -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; - deleteClickHandler: () => void; -}) => { - return ( - - - - - - - - - - - - - - ); -}; - -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(undefined); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const [sorting, setSorting] = useState([]); - - const fcrsColumns: ColumnDef[] = [ - { - 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 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; - - 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 = (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 ( - <> -
-
-
-
- - - -
- - -
- -
- -
-
- - - 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', - }} - /> -
- - - - ); -}; - -export default FcrsTable; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts deleted file mode 100644 index 21b0b9ee..00000000 --- a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts +++ /dev/null @@ -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; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.tsx b/src/components/pages/master-data/fcr/form/FcrForm.tsx deleted file mode 100644 index 807e7e45..00000000 --- a/src/components/pages/master-data/fcr/form/FcrForm.tsx +++ /dev/null @@ -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(() => { - return { - name: initialValues?.name ?? '', - fcrStandards: initialValues?.fcr_standards - ? initialValues?.fcr_standards - : [ - { - weight: '', - fcr_number: '', - mortality: '', - }, - ], - }; - }, [initialValues]); - - const formik = useFormik({ - 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 ( - <> -
-
- - -

- {type === 'add' && 'Tambah FCR'} - {type === 'edit' && 'Edit FCR'} - {type === 'detail' && 'Detail FCR'} -

-
- -
-
- - -
-
- - - - - - - {type !== 'detail' && } - - - - - {formik.values.fcrStandards.map((fcrStandard, idx) => ( - - - - - {type !== 'detail' && ( - - )} - - ))} - -
BobotFCRMortalitasAksi
- - - - - - - -
-
-
- - {type !== 'detail' && ( - - )} -
- - - -
- {type !== 'add' && ( -
- - - - - {type !== 'edit' && ( - - - - )} -
- )} - - {type !== 'detail' && ( -
- - - -
- )} -
- - {fcrFormErrorMessage && ( -
- - {fcrFormErrorMessage} -
- )} - -
- - {type !== 'add' && ( - - )} - - ); -}; - -export default FcrForm; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index cad76310..4085bc56 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -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([]); 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, }} /> + + ); }; diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts index dc972b14..eb6f236c 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts @@ -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 { // 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'} /> - { - 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'} - /> - { - router.push('/production/project-flock'); - setFormikLastValues(undefined); - }} - secondaryButton={undefined} - /> - [] = [ - { - 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[] = [ { 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([]); const [productionStandards, setProductionStandards] = useState(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) => { : '-'}

-
- Standard FCR -
- fcrStandardModal.openModal()} - > - {projectFlockKandangLookup?.project_flock?.fcr?.name || - initialValues?.project_flock?.fcr?.name || - '-'} - -
-
Standard Produksi @@ -2160,22 +2073,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} -
- Standard FCR -
- fcrStandardModal.openModal()} - > - {initialValues.project_flock?.fcr?.name || '-'} - -
-
Standard Produksi @@ -2227,21 +2124,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - - FCR (g) - - - {initialValues.fcr_value != null - ? `${formatNumber(initialValues.fcr_value)} g` - : '-'} - - - - {initialValues.project_flock?.fcr?.fcr_std != null - ? `${formatNumber(initialValues.project_flock?.fcr?.fcr_std)} g` - : '-'} - - Feed Intake (g) @@ -2587,6 +2469,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } + disabled={type === 'detail'} /> {getStockUsageAdornment(idx)}
@@ -2793,6 +2676,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } + disabled={type === 'detail'} /> {(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'} /> @@ -3035,6 +2920,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} placeholder='Masukkan total berat telur (Kilogram)...' inputSuffix='Kilogram' + disabled={type === 'detail'} /> {(type as 'add' | 'edit' | 'detail') !== @@ -3283,62 +3169,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - {/* FCR Standard Modal */} - -
- {/* Modal Header */} -
-
- -

Detail Standard FCR

-
- -
-
- {isLoadingFcrStandards ? ( -
- -
- ) : fcrStandards.length > 0 ? ( - - 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', - }} - /> - ) : ( -

- Tidak ada data FCR standards -

- )} -
-
-
- {/* Production Standard Modal */} = { @@ -159,27 +163,33 @@ const PurchaseTable = () => { PurchaseApi.getAllFetcher ); - const [isDownloadingInvoice, setIsDownloadingInvoice] = useState(false); - const [invoicePurchaseData, setInvoicePurchaseData] = - useState(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[] | 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(); + 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[] = [ { @@ -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 -; - } - + const poExpedition = props.row.original.po_expedition; + if (!poExpedition || poExpedition.length === 0) return '-'; return ( - + return
  • {exp.refrence}
  • ; + })} + ); }, }, { - accessorKey: 'supplier.name', + accessorKey: 'supplier', header: 'Vendor', cell: (props) => props.row.original.supplier.name, }, @@ -505,15 +512,6 @@ const PurchaseTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> - - {invoicePurchaseData && ( -
    - -
    - )} ); }; diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index c7b196a2..f872e7e0 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -402,6 +402,13 @@ const PurchaseOrderAcceptApprovalForm = ({ {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 ( @@ -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 diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 000c212b..1e674f4f 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -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 ( @@ -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', }} diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index 4ad093e1..aed154d0 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -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' ? (