diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 309addbd..d83b7608 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -3,11 +3,10 @@ import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; -import ClosingDetail from '@/components/pages/closing/ClosingDetail'; +import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { FlockApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { ProjectFlockKandangApi } from '@/services/api/production'; @@ -34,33 +33,6 @@ const ClosingDetailPage = () => { () => ProjectFlockKandangApi.getSingle(Number(kandangId)) ); - const { data: salesData, isLoading: isLoadingSales } = useSWR( - kandangId - ? `sales-${closingId}-${kandangId}` - : closingId - ? `sales-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId)) - : ClosingApi.getPenjualan(Number(closingId)) - ); - - const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( - kandangId - ? `hpp-ekspedisi-${closingId}-${kandangId}` - : closingId - ? `hpp-ekspedisi-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getHppEkspedisiByKandang( - Number(closingId), - Number(kandangId) - ) - : ClosingApi.getHppEkspedisi(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -76,12 +48,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = - isLoadingClosing || - isLoadingSales || - isLoadingHppEkspedisi || - isLoadingProject || - isLoadingKandang; + const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang; return (
@@ -91,12 +58,6 @@ const ClosingDetailPage = () => { { return ( -
+
); diff --git a/src/app/finance/detail/edit/page.tsx b/src/app/finance/detail/edit/page.tsx index 93a0daea..331f4101 100644 --- a/src/app/finance/detail/edit/page.tsx +++ b/src/app/finance/detail/edit/page.tsx @@ -5,7 +5,6 @@ import useSWR from 'swr'; import { FinanceApi } from '@/services/api/finance'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd'; -import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance'; const EditFinanceTransactionPage = () => { const router = useRouter(); diff --git a/src/app/finance/detail/page.tsx b/src/app/finance/detail/page.tsx index b80e8acb..f23d7770 100644 --- a/src/app/finance/detail/page.tsx +++ b/src/app/finance/detail/page.tsx @@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail'; import useSWR from 'swr'; import { useRouter, useSearchParams } from 'next/navigation'; import { FinanceApi } from '@/services/api/finance'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseSuccess } from '@/lib/api-helper'; const FinanceDetailPage = () => { const router = useRouter(); 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/page.tsx b/src/app/page.tsx index 33d01de7..8c10b702 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,10 +3,9 @@ import { useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useAuth } from '@/services/hooks/useAuth'; -import { redirectToSSO } from '@/lib/auth-helper'; export default function Home() { - const { user, isLoadingUser } = useAuth(); + const { isLoadingUser } = useAuth(); const router = useRouter(); const pathname = usePathname(); diff --git a/src/app/production/project-flock/add/page.tsx b/src/app/production/project-flock/add/page.tsx index 2eb2c090..eb2b6dd1 100644 --- a/src/app/production/project-flock/add/page.tsx +++ b/src/app/production/project-flock/add/page.tsx @@ -1,8 +1,8 @@ 'use client'; import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; -import React, { useImperativeHandle } from 'react'; -import toast from 'react-hot-toast'; +import React from 'react'; +// import React, { useImperativeHandle } from 'react'; const AddProjectFlock = () => { // useImperativeHandle(ref, () => ({ diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx index e5f88f19..4551dd85 100644 --- a/src/app/production/project-flock/detail/edit/page.tsx +++ b/src/app/production/project-flock/detail/edit/page.tsx @@ -12,11 +12,10 @@ const ProjectFlockEdit = () => { const projectFlockId = searchParams.get('projectFlockId'); - const { - data: projectFlock, - isLoading: isLoadingProjectFlock, - mutate: refreshProjectFlocks, - } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); if (!projectFlockId) { router.back(); diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx index 6187898e..f4d58f9a 100644 --- a/src/app/production/project-flock/detail/page.tsx +++ b/src/app/production/project-flock/detail/page.tsx @@ -1,7 +1,6 @@ 'use client'; import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail'; -import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useRouter, useSearchParams } from 'next/navigation'; @@ -13,11 +12,10 @@ const ProjectFlockDetailPage = () => { const projectFlockId = searchParams.get('projectFlockId'); - const { - data: projectFlock, - isLoading: isLoadingProjectFlock, - mutate: refreshProjectFlock, - } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); if (!projectFlockId) { router.back(); diff --git a/src/app/report/expense/page.tsx b/src/app/report/expense/page.tsx index 99d2862e..bb497283 100644 --- a/src/app/report/expense/page.tsx +++ b/src/app/report/expense/page.tsx @@ -1,13 +1,9 @@ 'use client'; -import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable'; +import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs'; const ReportExpense = () => { - return ( -
- -
- ); + return ; }; export default ReportExpense; diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 52a3d4dd..cb79f109 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,11 +1,7 @@ -import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs'; const MarketingReportPage = () => { - return ( -
- -
- ); + return ; }; export default MarketingReportPage; diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index 691ea734..4c9ea02b 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -1,9 +1,9 @@ -import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent'; +import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs'; const ProductionResultReportPage = () => { return ( -
- +
+
); }; diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 71da0789..724e4b0a 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useCallback } from 'react'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; @@ -13,7 +12,6 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound'; import { useUiStore } from '@/stores/ui/ui.store'; import { MAIN_DRAWER_LINKS } from '@/config/constant'; -import { isPathActive } from '@/lib/helper'; import { ROUTE_PERMISSIONS } from '@/config/route-permission'; import { useAuth } from '@/services/hooks/useAuth'; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b40d9db5..d9d81543 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ })); const emptyContentDefaultValue = ( -
- +
+ Tidak ada data yang dapat ditampilkan...
@@ -452,6 +452,20 @@ const Table = ({ ); })} + + {(data.length === 0 || table.getRowModel().rows.length === 0) && + !isLoading && ( + + + {emptyContent} + + + )} ({
- {(data.length === 0 || table.getRowModel().rows.length === 0) && - !isLoading && - emptyContent} - {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 8a06f9ed..52047d8b 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'; +import { HTMLAttributes, ReactNode, useState } from 'react'; import { cn } from '@/lib/helper'; export interface TabItem { 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/helper/skeleton/DataStateSkeleton.tsx b/src/components/helper/skeleton/DataStateSkeleton.tsx index cd5474e0..f3e9fdef 100644 --- a/src/components/helper/skeleton/DataStateSkeleton.tsx +++ b/src/components/helper/skeleton/DataStateSkeleton.tsx @@ -1,5 +1,4 @@ import IconSkeleton from '@/components/helper/skeleton/IconSkeleton'; -import { Icon } from '@iconify/react'; const DataStateSkeleton = ({ icon, diff --git a/src/components/input/PatternInput.tsx b/src/components/input/PatternInput.tsx index 9af1b68e..290614c7 100644 --- a/src/components/input/PatternInput.tsx +++ b/src/components/input/PatternInput.tsx @@ -4,7 +4,6 @@ import { ChangeEvent } from 'react'; import { PatternFormat, NumberFormatBase, - NumberFormatBaseProps, OnValueChange, } from 'react-number-format'; import TextInput, { TextInputProps } from '@/components/input/TextInput'; diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index a79054dd..ef959ea7 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,28 +278,28 @@ 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!', { - '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': + 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-error!': isError, + 'ring-2 ring-error/20': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment && !isError, + 'border-base-content/10!': !isError && !isFocused, 'rounded-l-none!': inputPrefix && !startAdornment, 'rounded-r-none!': inputSuffix && !startAdornment, }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), @@ -370,8 +370,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 @@ -403,31 +403,26 @@ const SelectInput = (props: SelectInputProps) => { className={cn('w-full', className?.select)} classNames={{ control: ({ isFocused, isDisabled }) => - cn( - 'w-full border bg-white transition-shadow', - // Gunakan rounded-lg untuk semua kasus - 'rounded-lg!', - { - '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, - } - ), + 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-error!': isError, + 'ring-2 ring-error/20': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment && !isError, + 'border-base-content/10!': !isError && !isFocused, + }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), 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/modal/ConfirmationModalWithNotes.tsx b/src/components/modal/ConfirmationModalWithNotes.tsx index e862dffc..20f63019 100644 --- a/src/components/modal/ConfirmationModalWithNotes.tsx +++ b/src/components/modal/ConfirmationModalWithNotes.tsx @@ -56,7 +56,7 @@ const ConfirmationModalWithNotes: React.FC = ({ closeOnBackdrop={closeOnBackdrop} primaryButton={{ ...primaryButton, - onClick: (e) => { + onClick: () => { if (primaryButton && primaryButton?.onClick) { primaryButton?.onClick?.(notes); } else { diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx similarity index 57% rename from src/components/pages/closing/ClosingDetail.tsx rename to src/components/pages/closing/ClosingDetailTabs.tsx index c3c91a5a..dc8bd6f8 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -5,28 +5,23 @@ import { useMemo, useState } from 'react'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; -import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; -import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; -import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; +import ClosingGeneralInformationTable from '@/components/pages/closing/table/ClosingGeneralInformationTable'; +import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab'; +import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab'; -import { - ClosingGeneralInformation, - BaseClosingSales, - ClosingHppExpedition, -} from '@/types/api/closing'; -import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; -import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; -import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; -import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; +import { ClosingGeneralInformation } from '@/types/api/closing'; +import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; +import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; +import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; +import SalesClosingTab from '@/components/pages/closing/tab/SalesClosingTab'; +import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { useClosingTabStore } from '@/stores/closing/closing-tab.store'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; - salesData?: BaseClosingSales; - hppExpeditionData?: ClosingHppExpedition; projectData?: ProjectFlock; kandangData?: ProjectFlockKandang; } @@ -34,25 +29,24 @@ interface ClosingDetailProps { const ClosingDetail: React.FC = ({ id, initialValue, - salesData, - hppExpeditionData, projectData, kandangData, }) => { - const [activeTab, setActiveTab] = useState('sapronak'); + const [activeTabId, setActiveTabId] = useState('sapronak'); + const tabActions = useClosingTabStore((state) => state.tabActions); const closingDetailTabs = useMemo(() => { const validTabs = [ { id: 'sapronak', label: 'Sapronak', - content: , + content: , }, { id: 'perhitunganSapronak', label: 'Perhitungan Sapronak', content: ( - @@ -61,13 +55,13 @@ const ClosingDetail: React.FC = ({ { id: 'penjualan', label: 'Penjualan', - content: , + content: , }, { id: 'overhead', label: 'Overhead', content: ( - = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: , + content: , }, { id: 'dataProduksi', label: 'Data Produksi', - content: , + content: , }, { id: 'keuangan', label: 'Keuangan', - content: , + content: , }, ]; return validTabs; - }, [initialValue]); + }, [initialValue, kandangData, id]); return ( <> -
+
diff --git a/src/components/pages/closing/ClosingFinanceTabContent.tsx b/src/components/pages/closing/ClosingFinanceTabContent.tsx deleted file mode 100644 index 92386178..00000000 --- a/src/components/pages/closing/ClosingFinanceTabContent.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import ClosingFinanceTable from '@/components/pages/closing/ClosingFinanceTable'; - -const ClosingFinanceTabContent = ({ - projectFlockId, -}: { - projectFlockId: number; -}) => { - return ( -
- {projectFlockId && ( - - )} -
- ); -}; - -export default ClosingFinanceTabContent; diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx deleted file mode 100644 index 6225f5e7..00000000 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import Card from '@/components/Card'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { formatCurrency, formatTitleCase } from '@/lib/helper'; -import { ClosingApi } from '@/services/api/closing'; -import { HppItem, ProfitLossItem } from '@/types/api/closing'; -import { useSearchParams } from 'next/navigation'; -import { useMemo } from 'react'; -import useSWR from 'swr'; - -const ClosingFinanceTable = ({ - projectFlockId, -}: { - projectFlockId: number; -}) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: finance, isLoading } = useSWR( - `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, - () => - ClosingApi.getFinance( - projectFlockId, - kandangId ? Number(kandangId) : undefined - ) - ); - - const hppTableData: HppItem[] = useMemo(() => { - if (isResponseSuccess(finance)) { - const customItems = { - label: 'HPP dan Pengeluaran', - code: 'custom_row', - } as HppItem; - const purchases = finance.data.hpp.items.filter( - (item) => item.category === 'purchase' - ); - const totalBudgeting = { - label: 'HPP dan Bahan Baku', - code: 'custom_row', - } as HppItem; - const overheads = finance.data.hpp.items.filter( - (item) => item.category === 'overhead' - ); - return [customItems, ...purchases, totalBudgeting, ...overheads]; - } - return []; - }, [finance]); - - const profitLossTableData: ProfitLossItem[] = useMemo(() => { - if (isResponseSuccess(finance)) { - const incomes = finance.data.profit_loss.items.filter( - (item) => item.type === 'income' - ); - const purchases = finance.data.profit_loss.items.filter( - (item) => item.type === 'purchase' - ); - const overheads = finance.data.profit_loss.items.filter( - (item) => item.type === 'overhead' - ); - const grossProfit = { - label: 'LABA RUGI BRUTO', - code: 'custom_row', - type: 'gross_profit', - rp_per_bird: - finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0, - rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0, - amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0, - } as ProfitLossItem; - const subtotal = { - label: 'Subtotal', - code: 'custom_row', - type: 'subtotal', - rp_per_bird: - finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0, - rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0, - amount: finance.data.profit_loss.summary.sub_total.amount ?? 0, - } as ProfitLossItem; - return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; - } - return []; - }, [finance]); - - return ( -
- <> - -
-
-
Laba Rugi Brutto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.gross_profit.amount - ) - : '-'} -
-
-
-
Laba Rugi Netto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit.amount - ) - : '-'} -
-
-
-
- -
- - data={hppTableData} - isLoading={isLoading} - columns={[ - { - header: 'No.', - enableSorting: false, - accessorFn: (item, index) => { - if (item.code === 'custom_row') return '-'; - const dataRowsBefore = hppTableData - .slice(0, index) - .filter((row) => row.code !== 'custom_row').length; - return dataRowsBefore + 1; - }, - footer: (props) => { - return 'HPP'; - }, - }, - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => formatTitleCase(item.label || '-'), - }, - { - header: 'Budgeting', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'budgeting_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting - ?.rp_per_bird || 0 - ) - : '-'; - }, - }, - { - header: 'Rp/Kg', - id: 'budgeting_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.rp_per_kg || - 0 - ) - : '-'; - }, - }, - { - header: 'Jumlah (Rp)', - id: 'budgeting_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.amount || 0), - footer: (props) => { - return props.column.id === 'budgeting_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.amount || 0 - ) - : '-'; - }, - }, - ], - }, - { - header: 'Realization', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'realization_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_bird || 0 - ) - : '-'; - }, - }, - { - header: 'Rp/Kg', - id: 'realization_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_kg || 0 - ) - : '-'; - }, - }, - { - header: 'Jumlah (Rp)', - id: 'realization_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.amount || 0), - footer: (props) => { - return props.column.id === 'realization_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization?.amount || 0 - ) - : '-'; - }, - }, - ], - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( - - - -
- {formatTitleCase(rowData.label ?? '-')} -
- - - ); - } - return null; - }} - renderFooter={isResponseSuccess(finance)} - /> -
-
- -
- - data={profitLossTableData} - isLoading={isLoading} - columns={[ - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => item.label, - cell: (item) => ( -
- {formatTitleCase(item.row.original.label || '-')} -
- ), - footer: () => ( -
LABA RUGI NETTO
- ), - }, - { - header: 'Rp/Ekor', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_bird || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Rp/Kg', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_kg || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Jumlah (Rp)', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.amount || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .amount || 0 - ) - : formatCurrency(0)} -
- ), - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( - - -
- {formatTitleCase(rowData.label ?? '-')} -
- - -
- {formatCurrency(rowData.rp_per_bird ?? 0)} -
- - -
- {formatCurrency(rowData.rp_per_kg ?? 0)} -
- - -
- {formatCurrency(rowData.amount ?? 0)} -
- - - ); - } - return null; - }} - className={{ - paginationClassName: 'hidden', - }} - renderFooter={isResponseSuccess(finance)} - /> -
-
- -
- ); -}; - -export default ClosingFinanceTable; diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index dd3083a7..4ecf607f 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,18 +10,18 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
-

Kandang

-
+

Kandang

+
{projectData?.kandangs?.map((kandang) => ( diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx deleted file mode 100644 index 9295d283..00000000 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ /dev/null @@ -1,308 +0,0 @@ -'use client'; - -import { useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { formatNumber } from '@/lib/helper'; - -interface ClosingProductionDataTabContentProps { - projectFlockId: number; -} - -const ClosingProductionDataTabContent = ({ - projectFlockId, -}: ClosingProductionDataTabContentProps) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: productionData, isLoading } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, - () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) - ); - - if (isLoading) { - return ( -
- -
- ); - } - - if (!productionData || !isResponseSuccess(productionData)) { - return ( -
- Gagal memuat data produksi. -
- ); - } - - const { purchase, sales, performance } = productionData.data; - - // Helper for consistent row styling - const DataRow = ({ - label, - value, - unit = '', - valueClassName = 'font-bold text-gray-800', - unitClassName = 'text-gray-500 w-12 text-right', - }: { - label: string; - value: string | number; - unit?: string; - valueClassName?: string; - unitClassName?: string; - }) => ( -
- {label} -
- {value} - {unit && {unit}} -
-
- ); - - return ( -
-

Data Produksi

- -
- {/* Left Column */} -
- {/* Purchase Section */} -
-

- Pembelian -

-
- - - - - -
-
- - {/* Sales Section */} -
-

- Penjualan -

-
- {/* Chicken Sales */} -
- - - - -
- - {/* Egg Sales (if available) */} - {sales.egg && ( - <> -
-
- - - - -
- - )} -
-
-
- - {/* Divider Line (Absolute centered) */} -
- - {/* Right Column */} -
- {/* Performance Section */} -
-

- Performance -

-
- - - - - - {/* - */} - - - - - - - {/* Laying Specific Fields */} - {performance.hen_day_act !== undefined && ( - <> - - - - )} - - {performance.egg_mass !== undefined && ( - <> - - - - )} - - {performance.egg_weight !== undefined && ( - <> - - - - )} - - {performance.hen_housed_act !== undefined && ( - <> - - - - )} -
-
-
-
-
- ); -}; - -export default ClosingProductionDataTabContent; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx deleted file mode 100644 index 77cef803..00000000 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ /dev/null @@ -1,268 +0,0 @@ -'use client'; - -import Card from '@/components/Card'; - -import Table from '@/components/Table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; -import { - RowSapronakCalculation, - TotalSapronakCalculation, -} from '@/types/api/closing'; -import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import useSWR from 'swr'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { ClosingGeneralInformation } from '@/types/api/closing'; -import { useSearchParams } from 'next/navigation'; - -interface ClosingSapronakCalculationTableProps { - projectFlockId: number; - closingGeneralInformation?: ClosingGeneralInformation; -} - -const ClosingSapronakCalculationTable = ({ - projectFlockId, - closingGeneralInformation, -}: ClosingSapronakCalculationTableProps) => { - const searchParams = useSearchParams(); - const kandangId = searchParams.get('kandangId'); - - const { data: sapronakCalculation, isLoading } = useSWR( - `/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, - () => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)), - { - keepPreviousData: true, - } - ); - - // Helper function to create columns with footer support - const createColumns = ( - total?: TotalSapronakCalculation - ): ColumnDef[] => [ - { - header: 'Tanggal', - accessorKey: 'date', - cell: (props) => - props.row.original.date - ? formatDate(props.row.original.date, 'DD MMM YYYY') - : '-', - footer: 'Total', - }, - { - header: 'No. Referensi', - accessorKey: 'reference_number', - cell: (props) => (props.row.original.reference_number as string) || '-', - footer: '', - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_in', - cell: (props) => - props.row.original.qty_in - ? formatNumber(props.row.original.qty_in as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_in ? formatNumber(total?.qty_in) : '0'} -
- ) - : '', - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_out', - cell: (props) => - props.row.original.qty_out - ? formatNumber(props.row.original.qty_out as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_out ? formatNumber(total?.qty_out) : '0'} -
- ) - : '', - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_used', - cell: (props) => - props.row.original.qty_used - ? formatNumber(props.row.original.qty_used as number) - : '0', - footer: total - ? () => ( -
- {total?.qty_used ? formatNumber(total?.qty_used) : '0'} -
- ) - : '', - }, - { - header: 'Uraian', - accessorKey: 'description', - cell: (props) => (props.row.original.description as string) || '-', - footer: '', - }, - { - header: 'Kategori Produk', - accessorKey: 'product_category', - cell: (props) => (props.row.original.product_category as string) || '-', - footer: '', - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'unit_price', - cell: (props) => - props.row.original.unit_price - ? formatCurrency(props.row.original.unit_price as number) - : '-', - footer: total - ? () => ( -
- {total?.avg_unit_price - ? formatCurrency(total?.avg_unit_price) - : '-'} -
- ) - : '', - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_amount', - cell: (props) => - props.row.original.total_amount - ? formatCurrency(props.row.original.total_amount as number) - : '-', - footer: total - ? () => ( -
- {total?.total_amount ? formatCurrency(total?.total_amount) : '-'} -
- ) - : '', - }, - { - header: 'Keterangan', - accessorKey: 'notes', - cell: (props) => (props.row.original.notes as string) || '-', - footer: '', - }, - ]; - - // Memoize columns untuk setiap kategori - const docColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.doc?.total) - : createColumns(), - [sapronakCalculation] - ); - - const ovkColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.ovk?.total) - : createColumns(), - [sapronakCalculation] - ); - - const pakanColumns = useMemo( - () => - isResponseSuccess(sapronakCalculation) - ? createColumns(sapronakCalculation.data?.pakan?.total) - : createColumns(), - [sapronakCalculation] - ); - - return ( -
- {/* Table DOC jika kategori Project Flock Growing */} - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) - : [] - } - columns={docColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.doc?.rows.length > 0 - } - /> - - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.ovk?.rows ?? []) - : [] - } - columns={ovkColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.ovk?.rows.length > 0 - } - /> - - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pakan?.rows ?? []) - : [] - } - columns={pakanColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.pakan?.rows.length > 0 - } - /> - -
- ); -}; - -export default ClosingSapronakCalculationTable; diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx deleted file mode 100644 index 03c3c984..00000000 --- a/src/components/pages/closing/ClosingSapronakTabContent.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; -import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; -import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable'; - -interface ClosingSapronakTableProps { - projectFlockId?: number; -} - -const ClosingSapronakTabContent = ({ - projectFlockId, -}: ClosingSapronakTableProps) => { - return ( -
- {projectFlockId && ( - <> - - - - - - - - - )} -
- ); -}; - -export default ClosingSapronakTabContent; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index dc9609ac..12114110 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -1,68 +1,116 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; +import StatusBadge from '@/components/helper/StatusBadge'; +import Modal, { useModal } from '@/components/Modal'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { useFormik } from 'formik'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { LocationApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { ClosingApi } from '@/services/api/closing'; import { Closing } from '@/types/api/closing'; - -const PROJECT_STATUS_OPTIONS = [ - { - value: 1, - label: 'Pengajuan', - }, - { - value: 2, - label: 'Aktif', - }, -]; +import { Color } from '@/types/theme'; +import { + ClosingFilterSchema, + ClosingFilterType, +} from '@/components/pages/closing/filter/ClosingFilter'; +import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton'; const RowOptionsMenu = ({ - type = 'dropdown', props, + popoverPosition = 'bottom', + detailClickHandler, }: { - type: 'dropdown' | 'collapse'; props: CellContext; + popoverPosition: 'bottom' | 'top'; + detailClickHandler: (id: number) => void; }) => { + const popoverId = `closing#${props.row.original.id}`; + const popoverAnchorName = `--anchor-closing#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + const detailClickHandlerWrapper = () => { + detailClickHandler(props.row.original.id); + closePopover(); + }; + return ( - -
- - - -
-
+
+ + + + + +
+ + + +
+
+
); }; const ClosingsTable = () => { + // ===== ROUTER ===== + const router = useRouter(); + + // ===== STATUS BADGE COLOR HELPER ===== + const getProjectStatusBadgeColor = (status: string): Color => { + const normalizedValue = status.toLowerCase(); + + if (normalizedValue === 'aktif') { + return 'success'; + } + + if (normalizedValue === 'pengajuan') { + return 'neutral'; + } + + return 'neutral'; + }; + + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + const { state: tableFilterState, updateFilter, @@ -72,36 +120,67 @@ const ClosingsTable = () => { } = useTableFilter({ initial: { search: '', - nameSort: '', - transactionDate: '', - realizationDate: '', - locationId: '', - projectStatus: '', - userId: '', + // nameSort: '', + // transactionDate: '', + // realizationDate: '', + location_id: '', + project_status: '', + // userId: '', }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - transactionDate: 'transaction_date', - realizationDate: 'realization_date', - locationId: 'location_id', - projectStatus: 'project_status', - userId: 'user_id', + // nameSort: 'sort_name', + // transactionDate: 'transaction_date', + // realizationDate: 'realization_date', + // locationId: 'location_id', + // projectStatus: 'project_status', + // userId: 'user_id', + search: 'search', + location_id: 'location_id', + project_status: 'project_status', }, }); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + location_id: null, + project_status: null, + }, + validationSchema: ClosingFilterSchema, + onSubmit: (values, { setSubmitting }) => { + updateFilter('location_id', values.location_id || ''); + updateFilter('project_status', values.project_status || ''); + filterModal.closeModal(); + setSubmitting(false); + }, + onReset: () => { + updateFilter('location_id', ''); + updateFilter('project_status', ''); + }, + }); + + // ===== DATA FETCHING ===== const { data: closings, isLoading: isLoadingClosings } = useSWR( `${ClosingApi.basePath}${getTableFilterQueryString()}`, ClosingApi.getAllFetcher ); + const data = useMemo( + () => + isResponseSuccess(closings) ? (closings?.data as Closing[]) || [] : [], + [closings] + ); + + // ===== PAGINATION & STATE ===== const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); + // ===== TABLE COLUMNS ===== const closingsColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -133,6 +212,19 @@ const ClosingsTable = () => { { accessorKey: 'project_status', header: 'Status', + cell: (props) => { + const status = props.row.original.project_status; + const badgeColor = getProjectStatusBadgeColor(status); + return ( + + ); + }, }, { header: 'Aksi', @@ -142,27 +234,24 @@ const ClosingsTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const detailClickHandler = (id: number) => { + router.push(`/closing/detail/?closingId=${id}`); + }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, ]; + // ===== LOCATION OPTIONS ===== const { setInputValue: setLocationInputValue, options: locationOptions, @@ -170,115 +259,246 @@ const ClosingsTable = () => { loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); - const [selectedLocation, setSelectedLocation] = useState( - null + // ===== PROJECT STATUS OPTIONS ===== + const projectStatusOptions = useMemo( + () => [ + { value: '1', label: 'Pengajuan' }, + { value: '2', label: 'Aktif' }, + ], + [] ); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'locationId', - val ? ((val as OptionType).value as string) : '' + // ===== FILTER HELPERS ===== + const locationIdValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null ); - }; + }, [formik.values.location_id, locationOptions]); - const [selectedProjectStatus, setSelectedProjectStatus] = - useState(null); - - const projectStatusChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedProjectStatus(val as OptionType); - updateFilter( - 'projectStatus', - val ? ((val as OptionType).value as string) : '' + const projectStatusValue = useMemo(() => { + if (!formik.values.project_status) return null; + return ( + projectStatusOptions.find( + (opt) => opt.value === formik.values.project_status + ) || null ); - }; + }, [formik.values.project_status, projectStatusOptions]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (tableFilterState.location_id) { + count += 1; + } + + if (tableFilterState.project_status) { + count += 1; + } + + return count; + }, [tableFilterState.location_id, tableFilterState.project_status]); + + const hasFilters = activeFiltersCount > 0; + + // ===== SEARCH CHANGE HANDLER ===== const searchChangeHandler: ChangeEventHandler = (e) => { updateFilter('search', e.target.value); }; + // ===== HANDLE FILTER MODAL OPEN ===== + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); if (!isNameSorted) { - updateFilter('nameSort', ''); + // updateFilter('nameSort', ''); } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + // updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); } - }, [sorting, updateFilter]); + }, [sorting]); return ( <> -
-
-
-
+
+
+
+
-
- -
- + } className={{ - wrapper: 'col-span-12 sm:col-span-6', + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} /> - +
-
- - data={isResponseSuccess(closings) ? closings?.data : []} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={isResponseSuccess(closings) ? closings?.meta?.page : 0} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), - }} - /> + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('mt-3', { + 'w-full mb-0': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'location_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('project_status', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+
+
); }; diff --git a/src/components/pages/closing/filter/ClosingFilter.ts b/src/components/pages/closing/filter/ClosingFilter.ts new file mode 100644 index 00000000..77f0c9d2 --- /dev/null +++ b/src/components/pages/closing/filter/ClosingFilter.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export type ClosingFilterType = { + location_id: string | null; + project_status: string | null; +}; + +export const ClosingFilterSchema = yup.object({ + location_id: yup.string().nullable(), + project_status: yup.string().nullable(), +}); + +export type ClosingFilterValues = yup.InferType; diff --git a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx deleted file mode 100644 index da89d963..00000000 --- a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client'; - -import React, { useMemo } from 'react'; -import { ColumnDef } from '@tanstack/react-table'; -import Table from '@/components/Table'; -import Card from '@/components/Card'; -import { formatCurrency } from '@/lib/helper'; -import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; - -interface HppExpeditionReportTableProps { - type?: 'detail'; - initialValues?: BaseHppExpedition; -} - -const HppExpeditionReportTable = ({ - initialValues, -}: HppExpeditionReportTableProps) => { - const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { - return initialValues?.expedition_costs || []; - }, [initialValues]); - - const totals = useMemo(() => { - const totalHpp = initialValues?.total_hpp_amount || 0; - - return { - totalHpp, - }; - }, [initialValues]); - - const costOfRevenueExpeditionColumns: ColumnDef[] = - useMemo( - () => [ - { - id: 'id', - accessorKey: 'id', - header: 'No', - cell: (props) => { - return
{props.row.index + 1}
; - }, - footer: () => ( -
- Total HPP Ekspedisi -
- ), - }, - { - id: 'expedition_vendor_name', - accessorKey: 'expedition_vendor_name', - header: 'Nama Ekspedisi', - cell: (props) => props.getValue() || '-', - }, - { - id: 'hpp_amount', - accessorKey: 'hpp_amount', - header: 'HPP Ekspedisi', - cell: (props) => { - const value = props.getValue() as number; - return
{formatCurrency(value)}
; - }, - footer: () => ( -
- {formatCurrency(totals.totalHpp)} -
- ), - }, - ], - [totals] - ); - - return ( - <> -
-
-

HPP Ekspedisi

- - 0} - 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 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - - - ); -}; - -export default HppExpeditionReportTable; diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx new file mode 100644 index 00000000..44defca8 --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -0,0 +1,36 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTabSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTabSkeleton; diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx new file mode 100644 index 00000000..4b59510a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Closing } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTableSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTableSkeleton; diff --git a/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx new file mode 100644 index 00000000..1168710c --- /dev/null +++ b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const FinanceClosingSkeleton = ({ + title = 'Data Keuangan Belum Tersedia', + subtitle = 'Tidak ada data keuangan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + +
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default FinanceClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx new file mode 100644 index 00000000..d9be9971 --- /dev/null +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppExpeditionClosingSkeleton = ({ + columns, + title = 'Data HPP Ekspedisi Belum Tersedia', + subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default HppExpeditionClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx new file mode 100644 index 00000000..7404f5d2 --- /dev/null +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { Overhead } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const OverheadClosingSkeleton = ({ + columns, + title = 'Data Overhead Belum Tersedia', + subtitle = 'Tidak ada data overhead untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default OverheadClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx new file mode 100644 index 00000000..e0031394 --- /dev/null +++ b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx @@ -0,0 +1,33 @@ +import { Icon } from '@iconify/react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const ProductionDataClosingSkeleton = ({ + title = 'Data Produksi Belum Tersedia', + subtitle = 'Tidak ada data produksi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( +
+
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default ProductionDataClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx new file mode 100644 index 00000000..a9ec35aa --- /dev/null +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseSales } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SalesClosingSkeleton = ({ + columns, + title = 'Data Penjualan Belum Tersedia', + subtitle = 'Tidak ada data penjualan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SalesClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx new file mode 100644 index 00000000..97d4a56c --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -0,0 +1,29 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { RowSapronakCalculation } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakCalculationClosingSkeleton = ({ + columns, + title = 'Data Perhitungan Sapronak Belum Tersedia', + subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + + columns={columns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SapronakCalculationClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx new file mode 100644 index 00000000..130cd846 --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakClosingSkeleton = ({ + columns, + type = 'incoming', + title, + subtitle, + iconName = 'heroicons:chart-bar', +}: { + columns: ColumnDef[]; + type?: 'incoming' | 'outgoing'; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultTitle = + type === 'incoming' + ? 'Data Sapronak Masuk Belum Tersedia' + : 'Data Sapronak Keluar Belum Tersedia'; + + const defaultSubtitle = + type === 'incoming' + ? 'Tidak ada data sapronak masuk untuk periode ini.' + : 'Tidak ada data sapronak keluar untuk periode ini.'; + + return ( + + columns={columns} + icon={ + + } + title={title || defaultTitle} + subtitle={subtitle || defaultSubtitle} + /> + ); +}; + +export default SapronakClosingSkeleton; diff --git a/src/components/pages/closing/tab/FinanceClosingTab.tsx b/src/components/pages/closing/tab/FinanceClosingTab.tsx new file mode 100644 index 00000000..53a5068b --- /dev/null +++ b/src/components/pages/closing/tab/FinanceClosingTab.tsx @@ -0,0 +1,13 @@ +import FinanceClosingTable from '@/components/pages/closing/table/FinanceClosingTable'; + +const FinanceClosingTab = ({ projectFlockId }: { projectFlockId: number }) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default FinanceClosingTab; diff --git a/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx new file mode 100644 index 00000000..ad7f0ec1 --- /dev/null +++ b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx @@ -0,0 +1,19 @@ +import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable'; + +interface HppExpeditionClosingTabProps { + projectFlockId: number; +} + +const HppExpeditionClosingTab = ({ + projectFlockId, +}: HppExpeditionClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default HppExpeditionClosingTab; diff --git a/src/components/pages/closing/ClosingOverheadTabContent.tsx b/src/components/pages/closing/tab/OverheadClosingTab.tsx similarity index 67% rename from src/components/pages/closing/ClosingOverheadTabContent.tsx rename to src/components/pages/closing/tab/OverheadClosingTab.tsx index e6b0cb5a..85942a62 100644 --- a/src/components/pages/closing/ClosingOverheadTabContent.tsx +++ b/src/components/pages/closing/tab/OverheadClosingTab.tsx @@ -1,22 +1,22 @@ -import ClosingOverheadTable from '@/components/pages/closing/ClosingOverheadTable'; +import OverheadClosingTable from '@/components/pages/closing/table/OverheadClosingTable'; import { ClosingGeneralInformation } from '@/types/api/closing'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -interface ClosingOverheadTabContentProps { +interface OverheadClosingTabProps { projectFlockId: number; generalInformation?: ClosingGeneralInformation; kandangData?: ProjectFlockKandang; } -const ClosingOverheadTabContent = ({ +const OverheadClosingTab = ({ projectFlockId, generalInformation, kandangData, -}: ClosingOverheadTabContentProps) => { +}: OverheadClosingTabProps) => { return (
{projectFlockId && ( - { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: productionData, isLoading } = useSWR( + `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, + () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) + ); + + if (isLoading) { + return ; + } + + if (!productionData || !isResponseSuccess(productionData)) { + return ( + + ); + } + + const { purchase, sales, performance } = productionData.data; + + // Helper for consistent row styling + const DataRow = ({ + label, + value, + unit = '', + valueClassName = 'font-bold text-gray-800', + unitClassName = 'text-gray-500 w-12 text-right', + }: { + label: string; + value: string | number; + unit?: string; + valueClassName?: string; + unitClassName?: string; + }) => ( +
+ {label} +
+ {value} + {unit && {unit}} +
+
+ ); + + return ( +
+ +
+
+ {/* Left Column */} +
+ {/* Purchase Section */} +
+

+ Pembelian +

+
+ + + + + +
+
+ + {/* Sales Section */} +
+

+ Penjualan +

+
+ {/* Chicken Sales */} +
+ + + + +
+ + {/* Egg Sales (if available) */} + {sales.egg && ( + <> +
+
+ + + + +
+ + )} +
+
+
+ + {/* Divider Line (Absolute centered) */} +
+ + {/* Right Column */} +
+ {/* Performance Section */} +
+

+ Performance +

+
+ + + + + + {/* + */} + + + + + + + {/* Laying Specific Fields */} + {performance.hen_day_act !== undefined && ( + <> + + + + )} + + {performance.egg_mass !== undefined && ( + <> + + + + )} + + {performance.egg_weight !== undefined && ( + <> + + + + )} + + {performance.hen_housed_act !== undefined && ( + <> + + + + )} +
+
+
+
+
+ +
+ ); +}; + +export default ProductionDataClosingTab; diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx new file mode 100644 index 00000000..ee343da0 --- /dev/null +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -0,0 +1,15 @@ +import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; + +interface SalesClosingTabProps { + projectFlockId: number; +} + +const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => { + return ( +
+ {projectFlockId && } +
+ ); +}; + +export default SalesClosingTab; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx similarity index 56% rename from src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx rename to src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx index b8add15b..77a74d71 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx +++ b/src/components/pages/closing/tab/SapronakCalculationClosingTab.tsx @@ -1,22 +1,22 @@ 'use client'; -import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; +import SapronakCalculationClosingTable from '@/components/pages/closing/table/SapronakCalculationClosingTable'; import { ClosingGeneralInformation } from '@/types/api/closing'; -interface ClosingSapronakCalculationTabContentProps { +interface SapronakCalculationClosingTabProps { projectFlockId?: number; closingGeneralInformation?: ClosingGeneralInformation; } -const ClosingSapronakCalculationTabContent = ({ +const SapronakCalculationClosingTab = ({ projectFlockId, closingGeneralInformation, -}: ClosingSapronakCalculationTabContentProps) => { +}: SapronakCalculationClosingTabProps) => { return (
{projectFlockId && ( <> - @@ -26,4 +26,4 @@ const ClosingSapronakCalculationTabContent = ({ ); }; -export default ClosingSapronakCalculationTabContent; +export default SapronakCalculationClosingTab; diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx new file mode 100644 index 00000000..21bb3b3f --- /dev/null +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -0,0 +1,30 @@ +'use client'; + +import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; +import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; +import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; +import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable'; + +interface SapronakClosingTabProps { + projectFlockId?: number; +} + +const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => { + return ( +
+ {projectFlockId && ( + <> + + + + + + + + + )} +
+ ); +}; + +export default SapronakClosingTab; diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/table/ClosingGeneralInformationTable.tsx similarity index 100% rename from src/components/pages/closing/ClosingGeneralInformationTable.tsx rename to src/components/pages/closing/table/ClosingGeneralInformationTable.tsx diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx new file mode 100644 index 00000000..760fbd04 --- /dev/null +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -0,0 +1,507 @@ +import Alert from '@/components/Alert'; +import Card from '@/components/Card'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatCurrency, formatTitleCase } from '@/lib/helper'; +import { ClosingApi } from '@/services/api/closing'; +import { HppItem, ProfitLossItem } from '@/types/api/closing'; +import { Icon } from '@iconify/react'; +import { useSearchParams } from 'next/navigation'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton'; + +const FinanceClosingTable = ({ + projectFlockId, +}: { + projectFlockId: number; +}) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: finance, isLoading } = useSWR( + `/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => + ClosingApi.getFinance( + projectFlockId, + kandangId ? Number(kandangId) : undefined + ) + ); + + const hppTableData: HppItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const customItems = { + label: 'HPP dan Pengeluaran', + code: 'custom_row', + } as HppItem; + const purchases = finance.data.hpp.items.filter( + (item) => item.category === 'purchase' + ); + const totalBudgeting = { + label: 'HPP dan Bahan Baku', + code: 'custom_row', + } as HppItem; + const overheads = finance.data.hpp.items.filter( + (item) => item.category === 'overhead' + ); + return [customItems, ...purchases, totalBudgeting, ...overheads]; + } + return []; + }, [finance]); + + const profitLossTableData: ProfitLossItem[] = useMemo(() => { + if (isResponseSuccess(finance)) { + const incomes = finance.data.profit_loss.items.filter( + (item) => item.type === 'income' + ); + const purchases = finance.data.profit_loss.items.filter( + (item) => item.type === 'purchase' + ); + const overheads = finance.data.profit_loss.items.filter( + (item) => item.type === 'overhead' + ); + const grossProfit = { + label: 'LABA RUGI BRUTO', + code: 'custom_row', + type: 'gross_profit', + rp_per_bird: + finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0, + } as ProfitLossItem; + const subtotal = { + label: 'Subtotal', + code: 'custom_row', + type: 'subtotal', + rp_per_bird: + finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0, + rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0, + amount: finance.data.profit_loss.summary.sub_total.amount ?? 0, + } as ProfitLossItem; + return [...incomes, ...purchases, grossProfit, ...overheads, subtotal]; + } + return []; + }, [finance]); + + return ( +
+ {isLoading ? ( + + ) : !isResponseSuccess(finance) ? ( + + ) : ( + <> +
+ +
+ + + +
+

+ Laba Rugi Brutto +

+

+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.gross_profit.amount + ) + : '-'} +

+
+
+
+ + +
+ + + +
+

+ Laba Rugi Netto +

+

+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit.amount + ) + : '-'} +

+
+
+
+
+ +
+ + data={hppTableData} + isLoading={isLoading} + columns={[ + { + header: 'No.', + enableSorting: false, + accessorFn: (item, index) => { + if (item.code === 'custom_row') return '-'; + const dataRowsBefore = hppTableData + .slice(0, index) + .filter((row) => row.code !== 'custom_row').length; + return dataRowsBefore + 1; + }, + footer: () => { + return 'HPP'; + }, + }, + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => formatTitleCase(item.label || '-'), + }, + { + header: 'Budgeting', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'budgeting_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_bird || 0 + ) + : '-'; + }, + }, + { + header: 'Rp/Kg', + id: 'budgeting_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_kg || 0 + ) + : '-'; + }, + }, + { + header: 'Jumlah (Rp)', + id: 'budgeting_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.amount || 0), + footer: (props) => { + return props.column.id === 'budgeting_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting?.amount || 0 + ) + : '-'; + }, + }, + ], + }, + { + header: 'Realization', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'realization_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === + 'realization_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_bird || 0 + ) + : '-'; + }, + }, + { + header: 'Rp/Kg', + id: 'realization_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'realization_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_kg || 0 + ) + : '-'; + }, + }, + { + header: 'Jumlah (Rp)', + id: 'realization_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.amount || 0), + footer: (props) => { + return props.column.id === 'realization_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization?.amount || + 0 + ) + : '-'; + }, + }, + ], + }, + ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + +
+ + data={profitLossTableData} + isLoading={isLoading} + columns={[ + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => item.label, + cell: (item) => ( +
+ {formatTitleCase(item.row.original.label || '-')} +
+ ), + footer: () => ( +
LABA RUGI NETTO
+ ), + }, + { + header: 'Rp/Ekor', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_bird || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Rp/Kg', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_kg || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Jumlah (Rp)', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.amount || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .amount || 0 + ) + : formatCurrency(0)} +
+ ), + }, + ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + + )} + + ); +}; + +export default FinanceClosingTable; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx new file mode 100644 index 00000000..5389e3d5 --- /dev/null +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -0,0 +1,147 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table from '@/components/Table'; +import Card from '@/components/Card'; +import { formatCurrency } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton'; + +interface HppExpeditionClosingTableProps { + projectFlockId: number; +} + +const HppExpeditionClosingTable = ({ + projectFlockId, +}: HppExpeditionClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: hppExpedition, isLoading } = useSWR( + kandangId + ? `/closing/hpp-expedition/${projectFlockId}/${kandangId}` + : `/closing/hpp-expedition/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getHppEkspedisi(projectFlockId) + ); + + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { + if (isResponseSuccess(hppExpedition)) { + return hppExpedition.data.expedition_costs || []; + } + return []; + }, [hppExpedition]); + + const totals = useMemo(() => { + if (isResponseSuccess(hppExpedition)) { + return { + totalHpp: hppExpedition.data.total_hpp_amount || 0, + }; + } + return { + totalHpp: 0, + }; + }, [hppExpedition]); + + const costOfRevenueExpeditionColumns: ColumnDef[] = + useMemo( + () => [ + { + id: 'id', + accessorKey: 'id', + header: 'No', + cell: (props) => { + return
{props.row.index + 1}
; + }, + footer: () => ( +
+ Total HPP Ekspedisi +
+ ), + }, + { + id: 'expedition_vendor_name', + accessorKey: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + cell: (props) => props.getValue() || '-', + }, + { + id: 'hpp_amount', + accessorKey: 'hpp_amount', + header: 'HPP Ekspedisi', + cell: (props) => { + const value = props.getValue() as number; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(totals.totalHpp)} +
+ ), + }, + ], + [totals] + ); + + return ( +
+ + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatCurrency(rowData.rp_per_bird ?? 0)} +
+
+
+ {formatCurrency(rowData.rp_per_kg ?? 0)} +
+
+
+ {formatCurrency(rowData.amount ?? 0)} +
+
0} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + + + ); +}; + +export default HppExpeditionClosingTable; diff --git a/src/components/pages/closing/ClosingOverheadTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx similarity index 72% rename from src/components/pages/closing/ClosingOverheadTable.tsx rename to src/components/pages/closing/table/OverheadClosingTable.tsx index a7a170eb..421817f9 100644 --- a/src/components/pages/closing/ClosingOverheadTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -1,5 +1,5 @@ import Card from '@/components/Card'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { ClosingApi } from '@/services/api/closing'; @@ -14,18 +14,19 @@ import { ColumnDef } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton'; -interface ClosingOverheadTableProps { +interface OverheadClosingTableProps { projectFlockId: number; generalInformation?: ClosingGeneralInformation; kandangData?: ProjectFlockKandang; } -const ClosingOverheadTable = ({ +const OverheadClosingTable = ({ projectFlockId, generalInformation, kandangData, -}: ClosingOverheadTableProps) => { +}: OverheadClosingTableProps) => { const searchParams = useSearchParams(); const kandangId = searchParams.get('kandangId'); @@ -37,7 +38,7 @@ const ClosingOverheadTable = ({ } ); - const { data: overheadKandang, isLoading: isLoadingOverheadKandang } = useSWR( + const { data: overheadKandang } = useSWR( kandangId ? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead` : undefined, @@ -208,42 +209,84 @@ const ClosingOverheadTable = ({ ); return ( - <> +
- - data={ - kandangId - ? isResponseSuccess(overheadKandang) - ? (overheadKandang.data?.overheads ?? []) - : [] - : isResponseSuccess(overhead) - ? (overhead.data?.overheads ?? []) - : [] - } - columns={columns} - className={{ - containerClassName: 'my-4', - headerColumnClassName: cn( - TABLE_DEFAULT_STYLING.headerColumnClassName, - 'whitespace-nowrap' - ), - }} - isLoading={isLoadingOverhead} - renderFooter={ - isResponseSuccess(overhead) - ? overhead.data?.overheads.length > 0 - : false - } - /> - {kandangId && ( + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) ? ( + + ) : kandangId && !isResponseSuccess(overheadKandang) ? ( + + ) : (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && + isResponseSuccess(overheadKandang) && + overheadKandang.data?.overheads.length === 0) ? ( + + ) : ( + + data={ + kandangId + ? isResponseSuccess(overheadKandang) + ? (overheadKandang.data?.overheads ?? []) + : [] + : isResponseSuccess(overhead) + ? (overhead.data?.overheads ?? []) + : [] + } + columns={columns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: cn( + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + 'whitespace-nowrap' + ), + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + isLoading={isLoadingOverhead} + renderFooter={ + isResponseSuccess(overhead) + ? overhead.data?.overheads.length > 0 + : false + } + /> + )} + {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( )} - +
); }; -export default ClosingOverheadTable; +export default OverheadClosingTable; diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx similarity index 73% rename from src/components/pages/closing/sale/SalesReportTable.tsx rename to src/components/pages/closing/table/SalesClosingTable.tsx index 0632676b..5105d965 100644 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -5,28 +5,47 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { - BaseClosingSales, - BaseSales, - ClosingSalesSummary, -} from '@/types/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseSales, ClosingSalesSummary } from '@/types/api/closing'; import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton'; -interface SalesReportTableProps { - type?: 'detail'; - initialValues?: BaseClosingSales; +interface SalesClosingTableProps { + projectFlockId: number; } -const SalesReportTable = ({ initialValues }: SalesReportTableProps) => { +const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sales, isLoading } = useSWR( + kandangId + ? `/closing/sales/${projectFlockId}/${kandangId}` + : `/closing/sales/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getPenjualan(projectFlockId) + ); + const salesData: BaseSales[] = useMemo(() => { - return initialValues?.sales || []; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.sales || []; + } + return []; + }, [sales]); const summary: ClosingSalesSummary | undefined = useMemo(() => { - return initialValues?.summary; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.summary; + } + return undefined; + }, [sales]); const totals = useMemo(() => { if (salesData.length === 0) { @@ -293,41 +312,55 @@ const SalesReportTable = ({ initialValues }: SalesReportTableProps) => { ); return ( - <> -
-
-

Penjualan

- + + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - 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', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - - + /> + )} + + ); }; -export default SalesReportTable; +export default SalesClosingTable; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx new file mode 100644 index 00000000..6f1252fc --- /dev/null +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -0,0 +1,359 @@ +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { ClosingGeneralInformation } from '@/types/api/closing'; +import { useSearchParams } from 'next/navigation'; +import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton'; + +interface SapronakCalculationClosingTableProps { + projectFlockId: number; + closingGeneralInformation?: ClosingGeneralInformation; +} + +const SapronakCalculationClosingTable = ({ + projectFlockId, + closingGeneralInformation, +}: SapronakCalculationClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sapronakCalculation, isLoading } = useSWR( + `/closing/sapronak-calculation/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId, Number(kandangId)), + { + keepPreviousData: true, + } + ); + + // Helper function to create columns with footer support + const createColumns = ( + total?: TotalSapronakCalculation + ): ColumnDef[] => [ + { + header: 'Tanggal', + accessorKey: 'date', + cell: (props) => + props.row.original.date + ? formatDate(props.row.original.date, 'DD MMM YYYY') + : '-', + footer: 'Total', + }, + { + header: 'No. Referensi', + accessorKey: 'reference_number', + cell: (props) => (props.row.original.reference_number as string) || '-', + footer: '', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_in', + cell: (props) => + props.row.original.qty_in + ? formatNumber(props.row.original.qty_in as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_in ? formatNumber(total?.qty_in) : '0'} +
+ ) + : '', + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_out', + cell: (props) => + props.row.original.qty_out + ? formatNumber(props.row.original.qty_out as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_out ? formatNumber(total?.qty_out) : '0'} +
+ ) + : '', + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_used', + cell: (props) => + props.row.original.qty_used + ? formatNumber(props.row.original.qty_used as number) + : '0', + footer: total + ? () => ( +
+ {total?.qty_used ? formatNumber(total?.qty_used) : '0'} +
+ ) + : '', + }, + { + header: 'Uraian', + accessorKey: 'description', + cell: (props) => (props.row.original.description as string) || '-', + footer: '', + }, + { + header: 'Kategori Produk', + accessorKey: 'product_category', + cell: (props) => (props.row.original.product_category as string) || '-', + footer: '', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'unit_price', + cell: (props) => + props.row.original.unit_price + ? formatCurrency(props.row.original.unit_price as number) + : '-', + footer: total + ? () => ( +
+ {total?.avg_unit_price + ? formatCurrency(total?.avg_unit_price) + : '-'} +
+ ) + : '', + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_amount', + cell: (props) => + props.row.original.total_amount + ? formatCurrency(props.row.original.total_amount as number) + : '-', + footer: total + ? () => ( +
+ {total?.total_amount ? formatCurrency(total?.total_amount) : '-'} +
+ ) + : '', + }, + { + header: 'Keterangan', + accessorKey: 'notes', + cell: (props) => (props.row.original.notes as string) || '-', + footer: '', + }, + ]; + + // Memoize columns untuk setiap kategori + const docColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.doc?.total) + : createColumns(), + [sapronakCalculation] + ); + + const ovkColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.ovk?.total) + : createColumns(), + [sapronakCalculation] + ); + + const pakanColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.pakan?.total) + : createColumns(), + [sapronakCalculation] + ); + + return ( +
+ {/* Table DOC jika kategori Project Flock Growing */} + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.doc?.rows ?? []) + : [] + } + columns={docColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length > 0 + } + /> + )} + + + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.ovk?.rows ?? []) + : [] + } + columns={ovkColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length > 0 + } + /> + )} + + + + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.pakan?.rows ?? []) + : [] + } + columns={pakanColumns} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length > 0 + } + /> + )} + +
+ ); +}; + +export default SapronakCalculationClosingTable; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 49e4f108..78773dbb 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -1,20 +1,20 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksSummaryTableProps { projectFlockId: number; @@ -55,20 +55,60 @@ const ClosingIncomingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { accessorKey: 'category', header: 'Kategori', + cell: (props) => { + const categories = props.row.original.category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'total_qty', @@ -78,10 +118,6 @@ const ClosingIncomingSapronaksSummaryTable = ({ }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -93,44 +129,35 @@ const ClosingIncomingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries.data.length > 0 - : false - ); - } - }, [incomingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Masuk
- - - - } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
+ {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(incomingSapronakSummaries) @@ -158,16 +185,21 @@ const ClosingIncomingSapronaksSummaryTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
- -
+ )} + +
); }; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx similarity index 51% rename from src/components/pages/closing/ClosingIncomingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 3d3a9d70..b0bd2744 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -9,13 +9,14 @@ import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksTableProps { projectFlockId: number; @@ -51,14 +52,12 @@ const ClosingIncomingSapronaksTable = ({ ClosingApi.getAllIncomingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -81,6 +80,48 @@ const ClosingIncomingSapronaksTable = ({ { accessorKey: 'product_category', header: 'Kategori Produk', + cell: (props) => { + const categories = props.row.original.product_category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'source_warehouse', @@ -117,56 +158,59 @@ const ClosingIncomingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks.data.length > 0 - : false - ); - } - }, [incomingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Masuk
- - + +
+
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
- } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
+
+ {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(incomingSapronaks) @@ -194,16 +238,21 @@ const ClosingIncomingSapronaksTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
-
-
+ )} +
+ ); }; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 42fcb588..b50b012b 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -1,20 +1,20 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; import { cn, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksSummaryTableProps { projectFlockId: number; @@ -55,20 +55,60 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { accessorKey: 'category', header: 'Kategori', + cell: (props) => { + const categories = props.row.original.category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'total_qty', @@ -78,10 +118,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({ }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -93,44 +129,35 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries.data.length > 0 - : false - ); - } - }, [outgoingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Keluar
- - - - } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
+ {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(outgoingSapronakSummaries) @@ -158,16 +185,21 @@ const ClosingOutgoingSapronaksSummaryTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
- -
+ )} + +
); }; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx similarity index 50% rename from src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index acbbc52d..23e3e8b0 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -9,13 +9,16 @@ import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; +import Badge from '@/components/Badge'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { cn } from '@/lib/helper'; + +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksTableProps { projectFlockId: number; @@ -51,14 +54,12 @@ const ClosingOutgoingSapronaksTable = ({ ClosingApi.getAllOutgoingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { @@ -81,6 +82,48 @@ const ClosingOutgoingSapronaksTable = ({ { accessorKey: 'product_category', header: 'Kategori Produk', + cell: (props) => { + const categories = props.row.original.product_category + .split(' ') + .filter((cat) => cat.trim()); + const maxBadges = 4; + const visibleCategories = categories.slice(0, maxBadges); + const remainingCount = categories.length - maxBadges; + + return ( +
+ {visibleCategories.map((category, index) => ( + + {category.length > 12 + ? `${category.slice(0, 12)}...` + : category} + + ))} + {remainingCount > 0 && ( + + +{remainingCount} + + )} +
+ ); + }, }, { accessorKey: 'source_warehouse', @@ -117,56 +160,59 @@ const ClosingOutgoingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks.data.length > 0 - : false - ); - } - }, [outgoingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Keluar
- - + +
+
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
- } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
+
+ {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( data={ isResponseSuccess(outgoingSapronaks) @@ -194,16 +240,21 @@ const ClosingOutgoingSapronaksTable = ({ rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), + containerClassName: 'w-full mb-5!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border-b border-gray-200', + bodyRowClassName: 'hover:bg-gray-50 transition-colors', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> -
-
-
+ )} +
+ ); }; diff --git a/src/components/pages/dashboard/export/DashboardPDF.ts b/src/components/pages/dashboard/export/DashboardPDF.ts index 8b4c7e6a..340abcbc 100644 --- a/src/components/pages/dashboard/export/DashboardPDF.ts +++ b/src/components/pages/dashboard/export/DashboardPDF.ts @@ -256,7 +256,7 @@ export const generateDashboardPDF = async ({ pdf.save(fileName); toast.success('PDF exported successfully!', { id: 'export-pdf' }); - } catch (error) { + } catch { toast.error('Failed to export PDF. Please try again.', { id: 'export-pdf', }); 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/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 6f422753..f83fa469 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { CellContext } from '@tanstack/react-table'; -import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { useFormik } from 'formik'; diff --git a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx index a63caa94..621557b2 100644 --- a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx +++ b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx @@ -48,9 +48,9 @@ const FormFinanceAddInitialBalance = ({ // ===== Formik ===== const formikInitialValues = useMemo((): InitialBalanceFormValues => { // Type assertion to handle potential initial_balance_type field - const extendedInitialValues = initialValues as Finance & { - initial_balance_type?: string; - }; + // const extendedInitialValues = initialValues as Finance & { + // initial_balance_type?: string; + // }; return { party_type_option: @@ -122,8 +122,6 @@ const FormFinanceAddInitialBalance = ({ options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - setInputValue: setBankInputValue, - loadMore: loadMoreBankOptions, } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== diff --git a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx index a4b77baf..b729ce11 100644 --- a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx +++ b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx @@ -28,10 +28,7 @@ import { useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import Alert from '@/components/Alert'; import { Icon } from '@iconify/react'; -import { - FINANCE_INJECTION_STATUS, - FINANCE_INJECTION_TYPE_OPTIONS, -} from '@/config/constant'; +import { FINANCE_INJECTION_TYPE_OPTIONS } from '@/config/constant'; interface FormFinanceInjectionProps { type?: 'add' | 'edit'; diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 8da2abf8..1bd47caf 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -15,7 +15,6 @@ import { Icon } from '@iconify/react'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; import { useCallback, useEffect, useState } from 'react'; import useSWR from 'swr'; -import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; const InventoryAdjustmentTable = () => { const { diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 3bae393d..612fbb20 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -14,7 +14,6 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; -import useSWR from 'swr'; import { ProductApi, ProductCategoryApi, diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index dbb30314..f723e763 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -33,7 +33,6 @@ import { toast } from 'react-hot-toast'; import { MovementApi } from '@/services/api/inventory'; import FileInput from '@/components/input/FileInput'; import CheckboxInput from '@/components/input/CheckboxInput'; -import Badge from '@/components/Badge'; import Card from '@/components/Card'; import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index fce21b2c..316bd103 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -15,12 +15,7 @@ import { InventoryProductApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { InventoryProduct } from '@/types/api/inventory/product'; import { Icon } from '@iconify/react'; -import { - CellContext, - ColumnDef, - Row, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index 96d3dda6..0a305659 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -35,6 +35,13 @@ const StockLogTable = ({ header: 'Gudang', accessorKey: 'warehouse_name', }, + { + header: 'Stock Akhir', + accessorKey: 'stock', + cell: (props) => { + return formatNumber(props.row.original.stock); + }, + }, { header: 'Peningkatan', accessorKey: 'increase', diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx index 6f48f7cd..aa375bdc 100644 --- a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx +++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx @@ -1,10 +1,7 @@ import Card from '@/components/Card'; import Table from '@/components/Table'; import { formatNumber } from '@/lib/helper'; -import { - InventoryProduct, - ProductWarehouseStock, -} from '@/types/api/inventory/product'; +import { ProductWarehouseStock } from '@/types/api/inventory/product'; const StockProductWarehouseTable = ({ productWarehouseStock, diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 7c953fe8..ae559328 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -48,17 +48,17 @@ import RequirePermission from '@/components/helper/RequirePermission'; const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable); const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm); -const DeliveryOrderFormModal = ({ - initialValues, -}: { - initialValues?: Marketing; -}) => { +const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { const router = useRouter(); const searchParams = useSearchParams(); const modalAction = searchParams.get('action'); const marketingId = searchParams.get('id'); + const [currentModalAction, setCurrentModalAction] = useState( + modalAction + ); + const isModalActionForForm = modalAction === 'add_delivery' || modalAction === 'edit_delivery' || @@ -72,19 +72,14 @@ const DeliveryOrderFormModal = ({ ); }; - const { data: marketing, isLoading: isLoadingMarketing } = useSWR( + const { data: marketing } = useSWR( isModalActionForForm && marketingId ? `detail-marketing-${marketingId}` : undefined, () => MarketingApi.getSingle(Number(marketingId)) ); - const { - approvals, - rawDataApprovals, - isLoading: isLoadingApproval, - refresh: refreshApproval, - } = useApprovalSteps({ + const { rawDataApprovals, refresh: refreshApproval } = useApprovalSteps({ latestApproval: isResponseSuccess(marketing) ? marketing?.data.latest_approval : undefined, @@ -107,6 +102,7 @@ const DeliveryOrderFormModal = ({ const successModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); + const approveModal = useModal(); const formRef = useRef(null); const textareaRef = useRef(null); @@ -279,29 +275,10 @@ const DeliveryOrderFormModal = ({ setIsLoading(false); }; - const memoSalesOrder = formik.values.sales_order; - // ================== HANDLER ================== - const nextButtonHandler = () => { - setStep(step + 1); - }; const prevButtonHandler = () => { setStep(step - 1); }; - const handleChangeCustomer = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('customer_id', (val as OptionType)?.value); - formik.setFieldValue('customer', val as OptionType); - }, - [] - ); - const handleChangeSalesPerson = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('sales_person_id', (val as OptionType)?.value); - formik.setFieldValue('sales_person', val as OptionType); - }, - [] - ); const rejectMarketingHandler = async (notes: string) => { if (!marketingId) { toast.error(`Tidak ada data yang valid untuk di reject.`); @@ -329,6 +306,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 +380,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) => @@ -405,32 +479,22 @@ const DeliveryOrderFormModal = ({ }, []); // ================== MEMOIZED ================== - const isNextButtonDisabled = useMemo(() => { - if (step === 1) { - return Boolean( - !formik.values.customer_id || - !formik.values.sales_person_id || - !formik.values.so_date || - !formik.values.notes - ); - } + // const isNextButtonDisabled = useMemo(() => { + // if (step === 1) { + // return Boolean( + // !formik.values.customer_id || + // !formik.values.sales_person_id || + // !formik.values.so_date || + // !formik.values.notes + // ); + // } - return true; - }, [step, formik.values]); + // return true; + // }, [step, formik.values]); 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 +505,7 @@ const DeliveryOrderFormModal = ({ modalAction === 'edit_delivery' || modalAction === 'detail' ) { + setCurrentModalAction(modalAction); formModal.openModal(); } }, [modalAction]); @@ -468,7 +533,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 +563,7 @@ const DeliveryOrderFormModal = ({ }; getFilledInitialValues(); - }, [marketingId, marketing]); + }, [marketingId, marketing, modalAction]); // Reset error message when step changes useEffect(() => { @@ -562,9 +646,11 @@ const DeliveryOrderFormModal = ({
- + @@ -667,13 +753,7 @@ const DeliveryOrderFormModal = ({
)}
@@ -715,31 +800,40 @@ const DeliveryOrderFormModal = ({ /> )} - {step === 1 && ( -
- - -
- )} + {step === 1 && + marketing?.data?.latest_approval?.step_number !== 3 && ( +
+ + +
+ )} )} @@ -749,25 +843,29 @@ 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', className: 'rounded-lg', - onClick: (e) => { + onClick: () => { closeModalHandler(); }, }} > - +
+ +
+ + ); }; 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..0bf00833 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -25,8 +25,6 @@ import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useAuth } from '@/services/hooks/useAuth'; -import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/Dropdown'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; @@ -109,7 +107,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'} @@ -134,7 +134,7 @@ const RowsOptionsMenu = ({ onClick={deleteClickHandler} variant='ghost' color='none' - className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-0.25 before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5' + className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-px before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5' > Delete Item @@ -147,14 +147,11 @@ const RowsOptionsMenu = ({ }; const MarketingTable = () => { - const [search, setSearch] = useState(''); - const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); const [selectedItem, setSelectedItem] = useState(null); const [rowSelection, setRowSelection] = useState>({}); - const { permissionCheck } = useAuth(); const router = useRouter(); const deleteModal = useModal(); @@ -217,6 +214,32 @@ const MarketingTable = () => { updateFilter('customer_id', ''); }; + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + // Product filter + if (tableFilterState.product_ids) { + count += 1; + } + + // Status filter + if (tableFilterState.status) { + count += 1; + } + + // Customer filter + if (tableFilterState.customer_id) { + count += 1; + } + + return count; + }, [ + tableFilterState.product_ids, + tableFilterState.status, + tableFilterState.customer_id, + ]); + const approveClickHandler = () => { setApproveAction('APPROVED'); confirmationModal.openModal(); @@ -379,8 +402,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 +436,7 @@ const MarketingTable = () => { : approval?.step_number == 2 ? 'info' : approval?.step_number == 3 - ? 'warning' + ? 'success' : 'neutral' : 'neutral' } @@ -523,7 +551,7 @@ const MarketingTable = () => { {idsToProcess.length > 0 && ( <> -
+
( + modalAction + ); + const isModalActionForForm = modalAction === 'add' || modalAction === 'edit' || @@ -77,7 +81,7 @@ const SalesOrderFormModal = ({ ); }; - const { data: marketing, isLoading: isLoadingMarketing } = useSWR( + const { data: marketing } = useSWR( isModalActionForForm && marketingId ? `detail-marketing-${marketingId}` : undefined, @@ -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,24 +744,26 @@ 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', className: 'rounded-lg', - onClick: (e) => { + onClick: () => { closeModalHandler(); }, }} > - +
+ +
= @@ -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..aae37d8e 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -34,8 +34,8 @@ const DeliveryOrderProductForm = ({ salesOrders, initialValues, exisitingValues, - onSubmitForm, onUpdateForm, + isLoading, }: { formState: 'add' | 'edit'; salesOrders: BaseSalesOrder[]; @@ -46,6 +46,7 @@ const DeliveryOrderProductForm = ({ id: number, value: DeliveryOrderProductFormValues ) => Promise; + isLoading?: boolean; }) => { const [formikErrorMessage, setFormErrorMessage] = useState(''); const [selectedProduct, setSelectedProduct] = useState( @@ -94,12 +95,12 @@ const DeliveryOrderProductForm = ({ ); // Options Week dari minggu 1 - 22 - const optionsWeek = useMemo(() => { - return Array.from({ length: 22 }, (_, i) => ({ - value: i + 1, - label: `Week ${i + 1}`, - })); - }, []); + // const optionsWeek = useMemo(() => { + // return Array.from({ length: 22 }, (_, i) => ({ + // value: i + 1, + // label: `Week ${i + 1}`, + // })); + // }, []); const options = exisitingValues ?.map((item) => { @@ -178,6 +179,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 +382,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 +533,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 +821,8 @@ const DeliveryOrderProductForm = ({
+ + + + <> + + + + + + + + + + + + + {Number(item.avg_weight ?? 0) > 0 && ( + + + + + )} + {Number(item.total_weight ?? 0) > 0 && ( + + + + + )} + + + + + + + + + + + + + + <> + {approvalStepNumber !== 1 && ( + + + + + )} + {item.do_number && ( + + + + + )} + + + + + {doItem && ( + + + + + )} + + + ); + }; + return ( <>
- {data.map((item) => { - const doItem = marketing?.delivery_order?.find( - (doItem) => doItem.do_number === item.do_number - ); - return ( -
-
No. Sales OrderNo. Order - {marketing.data.so_number} + {marketing.data.do_number + ? marketing.data.do_number + : marketing.data.so_number}
+ Label + +
+
Value
+ {formType !== 'success' && + (formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail') && ( +
+ +
+ )} +
+
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 ?? ''}` + : '-'} +
Avg Bobot + {formatNumber(Number(item.avg_weight))} Kg +
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
+
+
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 + )} +
No. Pengiriman{item.do_number}
No. Polisi{item.vehicle_number}
Dokumen Pengiriman + +
( +
+ {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..70282648 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,169 @@ 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' && ( +
+ + {data.length > 1 && ( + + )} +
+ )}
- ), - 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/marketing/pdf/DeliveryOrderExport.tsx b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx index 61ce7913..5db89450 100644 --- a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx +++ b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx @@ -49,7 +49,7 @@ const DeliveryOrderExport = ({ document.body.removeChild(link); window.URL.revokeObjectURL(url); }, 150); - } catch (error) { + } catch { toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); diff --git a/src/components/pages/marketing/pdf/SalesOrderExport.tsx b/src/components/pages/marketing/pdf/SalesOrderExport.tsx index 5767c049..55eb3b5b 100644 --- a/src/components/pages/marketing/pdf/SalesOrderExport.tsx +++ b/src/components/pages/marketing/pdf/SalesOrderExport.tsx @@ -1,7 +1,7 @@ import Button from '@/components/Button'; import { Marketing } from '@/types/api/marketing/marketing'; import { Icon } from '@iconify/react'; -import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer'; +import { Document, Page, pdf, Text, View } from '@react-pdf/renderer'; import { useMemo, useState } from 'react'; import { formatDate, formatNumber } from '@/lib/helper'; import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles'; @@ -43,7 +43,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => { document.body.removeChild(link); window.URL.revokeObjectURL(url); }, 150); - } catch (error) { + } catch { toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); @@ -162,7 +162,7 @@ const PDFDocument = ({ data }: { data: Marketing }) => { {data?.sales_order?.map((item, index) => { - const isLastItem = index === (data?.sales_order?.length || 0) - 1; + // const isLastItem = index === (data?.sales_order?.length || 0) - 1; return ( ; - 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/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index 22ad91f8..87ddfd70 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -22,7 +21,7 @@ import { KandangFormValues, UpdateKandangFormSchema, } from '@/components/pages/master-data/kandang/form/KandangForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Kandang, CreateKandangPayload, diff --git a/src/components/pages/master-data/location/form/LocationForm.tsx b/src/components/pages/master-data/location/form/LocationForm.tsx index 224c0f35..9b8b6ec1 100644 --- a/src/components/pages/master-data/location/form/LocationForm.tsx +++ b/src/components/pages/master-data/location/form/LocationForm.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -22,7 +21,7 @@ import { LocationFormValues, UpdateLocationFormSchema, } from '@/components/pages/master-data/location/form/LocationForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Location, CreateLocationPayload, diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index bfe88c0e..883aac03 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -22,7 +21,7 @@ import { NonstockFormValues, UpdateNonstockFormSchema, } from '@/components/pages/master-data/nonstock/form/NonstockForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Nonstock, CreateNonstockPayload, diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index b56e31bd..a8df6ae8 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -1,7 +1,6 @@ 'use client'; import Button from '@/components/Button'; -import { FormHeader } from '@/components/helper/form/FormHeader'; import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { ProductionStandard } from '@/types/api/master-data/production-standard'; import { Icon } from '@iconify/react'; @@ -82,14 +81,11 @@ const ProductionStandardTable = () => { >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { - data: productionStandards, - isLoading: productionStandardsLoading, - mutate: refreshProductionStandards, - } = useSWR( - `${ProductionStandardApi.basePath}`, - ProductionStandardApi.getAllFetcher - ); + const { data: productionStandards, mutate: refreshProductionStandards } = + useSWR( + `${ProductionStandardApi.basePath}`, + ProductionStandardApi.getAllFetcher + ); const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index a6a53e3f..307a8c92 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import { toast } from 'react-hot-toast'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -22,7 +21,7 @@ import { WarehouseFormValues, UpdateWarehouseFormSchema, } from '@/components/pages/master-data/warehouse/form/WarehouseForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Warehouse, CreateWarehousePayload, diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index bd3ff57c..64ff2ffd 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -1,12 +1,8 @@ 'use client'; import Card from '@/components/Card'; -import { FormHeader } from '@/components/helper/form/FormHeader'; -import Table from '@/components/Table'; import { formatNumber } from '@/lib/helper'; -import { Kandang } from '@/types/api/master-data/kandang'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import Tabs from '@/components/Tabs'; import { useState } from 'react'; import ApprovalSteps, { useApprovalSteps, @@ -15,7 +11,7 @@ import ChickinFormView from '@/components/pages/production/chickin/form/tabs/Chi import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/ChickLogsView'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { Icon } from '@iconify/react'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import RequirePermission from '@/components/helper/RequirePermission'; import { BaseApproval } from '@/types/api/api-general'; @@ -53,135 +49,126 @@ const ChickinFormKandang = ({ }; return ( -
+
+ {/* Header */} - {/* Informasi Kandang */} -
-
-

Informasi Kandang

+ {approvals && !approvalsLoading && ( + + )} - {approvals && !approvalsLoading && ( -
- -
- )} + {/* Informasi Kandang */} +
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - +
- - - {` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {initialValues.project_flock.area.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {initialValues.project_flock?.location.name} +
+
+
+
+ {' '} + Kandang +
+
+ {initialValues.kandang.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + initialValues.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
- {initialValues.project_flock.area.name} -
- - {/* Lokasi */} -
- Lokasi -
-
- {initialValues.project_flock?.location.name} -
- - {/* Kandang */} -
- Kandang -
-
{initialValues.kandang.name}
- - {/* Jumlah DOC */} -
- Jumlah DOC -
-
- {formatNumber( - initialValues.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
-
-
-

Informasi Chick In

+ {/* Informasi Chick In */} +
+

+ Informasi Chick In +

{/* Badge Row */}
- - {' '} - Perlu Chick In ({initialValues.available_qtys?.length ?? 0}) - +
- setOpenChickin(!openChickin)} - > - {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} - - + text={ + <> + {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} + + + } + className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + />
{openChickin && ( @@ -198,7 +185,7 @@ const ChickinFormKandang = ({ afterSubmit={afterSubmitFormChickin} /> -
+
); }; diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index e800ee68..bdffda33 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,8 +2,6 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useModal } from '@/components/Modal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; @@ -13,6 +11,7 @@ import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandan import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const ChickinLogsView = ({ initialValues, @@ -23,32 +22,26 @@ const ChickinLogsView = ({ afterSubmit?: () => void; rawDataApprovals: BaseApproval[]; }) => { - const confirmModal = useModal(); - const [isApproveLoading, setIsApproveLoading] = useState(false); const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + const { openChickinApproveModal } = useChickinStore(); const handleClickApprove = () => { - confirmModal.openModal(); - }; - - const confirmationModalApproveClickHandler = async (notes?: string) => { - setChickinErrorMessage(''); - setIsApproveLoading(true); - const approveChickinRes = await ChickinApi.singleApproval( - initialValues?.id as number, - 'APPROVED', - notes - ); - if (isResponseSuccess(approveChickinRes)) { - toast.success(approveChickinRes?.message as string); - } - if (isResponseError(approveChickinRes)) { - toast.error(approveChickinRes?.message as string); - setChickinErrorMessage(approveChickinRes?.message as string); - } - confirmModal.closeModal(); - setIsApproveLoading(false); - afterSubmit && afterSubmit(); + openChickinApproveModal(initialValues, async (notes?: string) => { + setChickinErrorMessage(''); + const approveChickinRes = await ChickinApi.singleApproval( + initialValues?.id as number, + 'APPROVED', + notes + ); + if (isResponseSuccess(approveChickinRes)) { + toast.success(approveChickinRes?.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + setChickinErrorMessage(approveChickinRes?.message as string); + } + afterSubmit && afterSubmit(); + }); }; return ( @@ -83,7 +76,7 @@ const ChickinLogsView = ({ key={chickin.id || index} variant='bordered' className={{ - wrapper: 'w-full', + wrapper: 'w-full mt-3', body: 'p-3', }} > @@ -176,23 +169,6 @@ const ChickinLogsView = ({
)}
- - { - confirmationModalApproveClickHandler(notes); - }, - isLoading: isApproveLoading, - }} - /> ); }; diff --git a/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx b/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx index 2dcc502e..f14b8710 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickinFormView.tsx @@ -1,10 +1,8 @@ 'use client'; import Card from '@/components/Card'; -import Table from '@/components/Table'; import { ChickinFormValues, - ChickinRequestFormValues, ChickinSchema, } from '@/components/pages/production/chickin/form/ChickinForm.schema'; import DateInput from '@/components/input/DateInput'; diff --git a/src/components/pages/production/project-flock/ProjectFlockConfirmationModal.tsx b/src/components/pages/production/project-flock/ProjectFlockConfirmationModal.tsx index 45834ee5..4a03b7ab 100644 --- a/src/components/pages/production/project-flock/ProjectFlockConfirmationModal.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockConfirmationModal.tsx @@ -60,11 +60,7 @@ const ProjectFlockConfirmationModal = ({ : '', limit: '500', }).toString()}`; - const { - data: kandang, - isLoading: isLoadingKandang, - mutate: refreshKandang, - } = useSWR(kandangUrl, KandangApi.getAllFetcher); + const { data: kandang } = useSWR(kandangUrl, KandangApi.getAllFetcher); const notesChangeHandler: ChangeEventHandler = (e) => { setNotes(e.target.value); @@ -85,7 +81,7 @@ const ProjectFlockConfirmationModal = ({ text: primaryButton?.text ?? 'Oke', color: primaryButton?.color ?? 'primary', className: 'rounded-lg', - onClick: (e) => { + onClick: () => { if (withNote) { primaryButton?.onClick?.(notes); } else if (primaryButton && primaryButton?.onClick) { diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index cad76310..7a8ba4e1 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -1,22 +1,16 @@ 'use client'; -import Badge from '@/components/Badge'; import Button from '@/components/Button'; -import FloatingActionsButton from '@/components/FloatingActionsButton'; import CheckboxInput from '@/components/input/CheckboxInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import Table from '@/components/Table'; import Dropdown from '@/components/Dropdown'; -import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn, formatDate, formatTitleCase } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -33,6 +27,11 @@ 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/production/project-flock/project-flock.store'; +import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; +import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; const RowOptionsMenu = ({ props, @@ -137,6 +136,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, @@ -169,17 +177,12 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { .filter((id) => rowSelection[id]) .map((id) => parseInt(id)); - const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState( - null - ); - const [selectedKandang, setSelectedKandang] = useState( - null - ); - const [periodInputValue, setPeriodInputValue] = useState(null); const [sorting, setSorting] = useState([]); const deleteModal = useModal(); const confirmModal = useModal(); + const successModal = useModal(); + const chickinApproveModal = useModal(); + const closingModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -187,6 +190,22 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const { + isChickinApproveModalOpen, + isChickinApproveLoading, + chickinApproveCallback, + closeChickinApproveModal, + setChickinApproveLoading, + } = useChickinStore(); + + const { + isClosingModalOpen, + isKandangClosed, + isClosingLoading, + closingCallback, + closeClosingModal, + setClosingLoading, + } = useProjectFlockClosingStore(); // ===== Fetch Data ===== const { @@ -199,26 +218,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { { revalidateOnMount: true } ); - // ===== Fetch Data Select ===== - const { - options: optionsArea, - isLoadingOptions: isLoadingArea, - setInputValue: setAreaSelectInputValue, - loadMore: loadMoreArea, - } = useSelect(AreaApi.basePath, 'id', 'name'); - const { - options: optionsLocation, - isLoadingOptions: isLoadingLocation, - setInputValue: setLocationSelectInputValue, - loadMore: loadMoreLocation, - } = useSelect(LocationApi.basePath, 'id', 'name'); - const { - options: optionsKandang, - isLoadingOptions: isLoadingKandang, - setInputValue: setKandangSelectInputValue, - loadMore: loadMoreKandang, - } = useSelect(KandangApi.basePath, 'id', 'name'); - // ====== HANDLER ====== const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -258,7 +257,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); if (isResponseSuccess(approveProjectFlockRes)) { - toast.success('Project Flock berhasil di-approve!'); + const successMessage = + approvalAction === 'APPROVED' + ? 'Project Flock berhasil di-approve!' + : 'Project Flock berhasil di-reject!'; + toast.success(successMessage); confirmModal.closeModal(); } if (isResponseError(approveProjectFlockRes)) { @@ -275,6 +278,80 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { refreshProjectFlocks(); }, [refresh]); + useEffect(() => { + if (isChickinApproveModalOpen) { + chickinApproveModal.openModal(); + } else { + chickinApproveModal.closeModal(); + } + }, [isChickinApproveModalOpen, chickinApproveModal]); + + useEffect(() => { + if (isClosingModalOpen) { + closingModal.openModal(); + } else { + closingModal.closeModal(); + } + }, [isClosingModalOpen, closingModal]); + + 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 @@ -313,7 +390,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { id: 'select', header: ({ table }) => { const allRows = table.getRowModel().rows; - const selectableRows = allRows; + const selectableRows = allRows.filter((row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }); const allSelected = selectableRows.every((row) => row.getIsSelected()) && @@ -327,6 +410,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); }; + const hasNoSelectableRows = selectableRows.length === 0; + return (
void }) => { checked={allSelected} indeterminate={someSelected} onChange={toggleSelectableRows} + disabled={hasNoSelectableRows} />
); @@ -363,10 +449,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { accessorKey: 'location.name', header: 'Lokasi', }, - { - accessorKey: 'fcr.name', - header: 'FCR', - }, { accessorKey: 'category', header: 'Kategori', @@ -778,6 +860,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setSorting={setSorting} rowSelection={rowSelection} setRowSelection={setRowSelection} + enableRowSelection={(row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }} withCheckbox className={{ containerClassName: cn('p-3', { @@ -877,6 +966,89 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { isLoading: isApproveLoading, }} /> + + + + {/* Chickin Approval Modal */} + { + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'success', + onClick: async (notes) => { + if (chickinApproveCallback) { + setChickinApproveLoading(true); + try { + await chickinApproveCallback(notes); + } finally { + setChickinApproveLoading(false); + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + } + } + }, + isLoading: isChickinApproveLoading, + }} + /> + + {/* Project Flock Closing Modal */} + { + closeClosingModal(); + closingModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'error', + isLoading: isClosingLoading, + onClick: async () => { + if (closingCallback) { + setClosingLoading(true); + try { + await closingCallback(!isKandangClosed ? 'close' : 'unclose'); + } finally { + setClosingLoading(false); + closeClosingModal(); + closingModal.closeModal(); + refreshProjectFlocks(); + } + } + }, + }} + /> ); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index aab21172..935ee071 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -1,10 +1,11 @@ 'use client'; import Button from '@/components/Button'; +import Card from '@/components/Card'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; -import Badge from '@/components/Badge'; -import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; +import StatusBadge from '@/components/helper/StatusBadge'; +import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ClosingExpense, @@ -15,14 +16,31 @@ import { Icon } from '@iconify/react'; import useSWR from 'swr'; import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import { useMemo, useState } from 'react'; +import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; +import { useMemo } from 'react'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; -import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; -import { ApprovalApi } from '@/services/api/approval'; import RequirePermission from '@/components/helper/RequirePermission'; +import { Color } from '@/types/theme'; + +const getExpenseStatusBadgeColor = (step: number): Color => { + switch (step) { + case 1: + return 'neutral'; + case 2: + return 'info'; + case 3: + return 'warning'; + case 4: + return 'error'; + case 5: + return 'warning'; + case 6: + return 'success'; + default: + return 'neutral'; + } +}; const ProjectFlockClosingForm = ({ projectFlock, @@ -32,206 +50,209 @@ const ProjectFlockClosingForm = ({ projectFlockKandang: ProjectFlockKandang; }) => { const router = useRouter(); - const closeModal = useModal(); - const [isClosingLoading, setIsClosingLoading] = useState(false); + const { openClosingModal } = useProjectFlockClosingStore(); const { data: closingData, isLoading } = useSWR( `${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`, () => ProjectFlockKandangApi.checkClosing(projectFlockKandang.id) ); - const { data: projectFlockKandangApprovals } = useSWR( - `${ApprovalApi.basePath}?module_name=PROJECT_FLOCK_KANDANGS&module_id=${projectFlockKandang.id}`, - () => - ApprovalApi.getAllFetcher( - `${ApprovalApi.basePath}?module_name=PROJECT_FLOCK_KANDANGS&module_id=${projectFlockKandang.id}` - ) - ); + const isKandangClosed = useMemo(() => { + return projectFlockKandang.kandang?.status === 'NON_ACTIVE'; + }, [projectFlockKandang]); - const isCanClose = useMemo(() => { - return isResponseSuccess(projectFlockKandangApprovals) - ? projectFlockKandangApprovals?.data?.[0]?.step_number <= 2 - : true; - }, [projectFlockKandangApprovals]); - - const confirmationModalCloseClickHandler = async () => { - setIsClosingLoading(true); - const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( - projectFlockKandang?.id as number, - { - closed_date: isCanClose ? formatDate(new Date(), 'YYYY-MM-DD') : '', - action: isCanClose ? 'close' : 'unclose', - } - ); - - if (isResponseSuccess(deleteProjectFlockRes)) { - toast.success(deleteProjectFlockRes?.message as string); - router.push( - `/production/project-flock/detail?projectFlockId=${projectFlock.id}` + const handleCloseClick = () => { + const closingCallback = async (action: 'close' | 'unclose') => { + const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( + projectFlockKandang?.id as number, + { + closed_date: + action === 'close' ? formatDate(new Date(), 'YYYY-MM-DD') : '', + action, + } ); - } - if (isResponseError(deleteProjectFlockRes)) { - toast.error(deleteProjectFlockRes?.message as string); - } - setIsClosingLoading(false); - closeModal.closeModal(); + + if (isResponseSuccess(deleteProjectFlockRes)) { + toast.success(deleteProjectFlockRes?.message as string); + router.push( + `/production/project-flock/detail?projectFlockId=${projectFlock.id}` + ); + } + if (isResponseError(deleteProjectFlockRes)) { + toast.error(deleteProjectFlockRes?.message as string); + } + }; + + openClosingModal( + projectFlockKandang, + projectFlock.id, + isKandangClosed, + closingCallback + ); }; - const errorStock = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) - : true; - }, [closingData]); + // const errorStock = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) + // : true; + // }, [closingData]); - const errorExpense = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.expenses.every((expense) => expense.step < 5) - : true; - }, [closingData]); + // const errorExpense = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.expenses.every((expense) => expense.step < 5) + // : true; + // }, [closingData]); const isCanCloseValid = true; return ( <> -
+
+ {/* Header */} + leftIconClassName='hover:text-gray-400' + subtitle={isKandangClosed ? 'Unclose Flock' : 'Close Flock'} + className='sticky top-0 z-10 bg-base-100' + /> {/* Informasi Kandang */} -
-
-

Informasi Kandang

+
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - +
- - - {` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {projectFlock.area?.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {projectFlock.location?.name} +
+
+
+
+ {' '} + Kandang +
+
+ {projectFlockKandang.kandang?.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + projectFlockKandang.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
{projectFlock.area?.name}
- - {/* Lokasi */} -
- Lokasi -
-
{projectFlock.location?.name}
- - {/* Kandang */} -
- Kandang -
-
- {projectFlockKandang.kandang?.name} -
- - {/* Jumlah DOC */} -
- Jumlah - DOC -
-
- {formatNumber( - projectFlockKandang.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
{/* Table Biaya */} -
-
-

Biaya

+
+

+ Biaya +

data={ isResponseSuccess(closingData) ? closingData.data?.expenses : [] } columns={[ + { + header: 'Ref Number', + accessorKey: 'reference_number', + cell(props) { + return props.row.original.reference_number || '-'; + }, + }, { header: 'PO Number', accessorKey: 'po_number', + cell(props) { + return props.row.original.po_number || '-'; + }, }, { header: 'Total', accessorKey: 'total', + cell(props) { + return formatNumber(props.row.original.total); + }, }, { header: 'Status', accessorKey: 'status', cell(props) { return ( - - {formatTitleCase(props.row.original.step_name)} - + /> ); }, }, ]} className={{ - containerClassName: cn('my-4'), - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + containerClassName: 'mb-0', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorExpense && ( @@ -242,9 +263,10 @@ const ProjectFlockClosingForm = ({
{/* Table Persediaan Gudang */} -
-
-

Persediaan Gudang

+
+

+ Persediaan Gudang +

data={ isResponseSuccess(closingData) @@ -263,6 +285,9 @@ const ProjectFlockClosingForm = ({ { header: 'Quantity', accessorKey: 'quantity', + cell(props) { + return formatNumber(props.row.original.quantity); + }, }, { header: 'UOM', @@ -270,16 +295,15 @@ const ProjectFlockClosingForm = ({ }, ]} className={{ - containerClassName: cn('my-4'), - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + containerClassName: 'mb-0', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorStock && ( @@ -289,40 +313,28 @@ const ProjectFlockClosingForm = ({ )} */}
-
+
- - -
+
); }; diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 47491dfa..d3812e47 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -1,4 +1,3 @@ -import Badge from '@/components/Badge'; import Button from '@/components/Button'; import Card from '@/components/Card'; import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput'; @@ -42,19 +41,6 @@ const ProjectFlockDetail = ({ (kandang) => kandang.id === Number(selectedKandangId) ); - const { data: projectFlockKandang, isLoading: projectFlockKandangLoading } = - useSWR( - selectedKandangId - ? `${ProjectFlockKandangApi.basePath}/get-detail/${selectedKandangId}` - : null, - selectedKandangId - ? () => - ProjectFlockKandangApi.getSingle( - Number(selectedKandang?.project_flock_kandang_id) - ) - : null - ); - const { data: projectFlockApprovalResponse } = useSWR( projectFlock.id ? ['approval-project-flock', projectFlock.id] : undefined, ([, id]) => ProjectFlockApi.getApprovalLineHistory(Number(id)) @@ -226,15 +212,37 @@ const ProjectFlockDetail = ({

- Kandang Aktif + Kandang

-
- +
+ {projectFlock.kandangs?.filter( + (kandang) => kandang.status !== 'NON_ACTIVE' + ).length > 0 && ( + kandang.status !== 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} + + {projectFlock.kandangs?.filter( + (kandang) => kandang.status === 'NON_ACTIVE' + ).length > 0 && ( + kandang.status === 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} } - className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + className={{ badge: 'w-fit cursor-pointer' }} />
@@ -355,43 +363,53 @@ const ProjectFlockDetail = ({ disabled={projectFlock?.approval?.step_number == 1} />
- - Kapasitas {kandang?.capacity} Ekor - + Kapasitas {kandang?.capacity} Ekor} + className={{ badge: 'w-fit text-nowrap' }} + />
))} - +
+ +
-
- - - - - + + + + )} - Close + {selectedKandang?.status === 'NON_ACTIVE' ? ( + <> + Unclose + + ) : ( + <> + Close + + )} 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'); @@ -225,7 +226,7 @@ const ProjectFlockForm = ({ const [disabledLocation, setDisabledLocation] = useState( initialValues?.location?.id ? false : true ); - const [openSelectKandangs, setOpenSelectKandangs] = useState( + const [, setOpenSelectKandangs] = useState( initialValues?.kandangs && initialValues?.kandangs?.length > 0 ); const [optionsKandang, setOptionsKandang] = useState( @@ -240,7 +241,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 +263,8 @@ const ProjectFlockForm = ({ loadMore: loadMoreFlock, } = useSelect(FlockApi.basePath, 'id', 'name', '', { project_category: selectedCategory, + location_id: selectedLocation, + area_id: selectedArea, }); const { @@ -284,13 +286,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 +434,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 +454,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); @@ -474,9 +473,9 @@ const ProjectFlockForm = ({ formikSetValues(formikInitialValues); }; - const [formikLastValues, setFormikLastValues] = useState< - ProjectFlockFormValues | undefined - >(undefined); + const [, setFormikLastValues] = useState( + undefined + ); // Formik InitialValue const formikInitialValues = useMemo(() => { @@ -485,9 +484,9 @@ const ProjectFlockForm = ({ 0, initialValues?.flock_name?.lastIndexOf(' ') ) ?? ''; - const optionFind = optionsFlock.find((flock) => { - return flock.label == trimFlock; - }) as OptionType; + // const optionFind = optionsFlock.find((flock) => { + // return flock.label == trimFlock; + // }) as OptionType; return { flock: optionsFlock.find((flock) => { @@ -505,12 +504,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 +524,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 +566,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 +987,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} - /> - = { APPROVED: 'Disetujui', - Disetujui: 'Disetujui', REJECTED: 'Ditolak', - Ditolak: 'Ditolak', - CREATED: 'Dibuat', + CREATED: 'Pengajuan', UPDATED: 'Diperbarui', }; const getStatusText = (status: string): string => { - return statusTextMap[status] || status; + const normalizedStatus = status.toUpperCase(); + return statusTextMap[normalizedStatus] || status; }; const statusBadgeColorMap: Record = { APPROVED: 'success', - Disetujui: 'success', - approved: 'success', - disetujui: 'success', REJECTED: 'error', - Ditolak: 'error', - rejected: 'error', - ditolak: 'error', CREATED: 'neutral', - Dibuat: 'neutral', - created: 'neutral', - dibuat: 'neutral', UPDATED: 'warning', - Diperbarui: 'warning', - updated: 'warning', - diperbarui: 'warning', }; const getStatusBadgeColor = (status: string): Color => { - return statusBadgeColorMap[status] || 'neutral'; + const normalizedStatus = status.toUpperCase(); + return statusBadgeColorMap[normalizedStatus] || 'neutral'; }; const RowOptionsMenu = ({ @@ -852,8 +840,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - - const statusText = approval.step_name || getStatusText(status); + const statusText = getStatusText(status); return ( [] = [ - { - 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; @@ -543,6 +484,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { location_id: depletionProductsLocationId, kandang_id: depletionProductsKandangId, + type: 'AYAM', }); const today = new Date().toISOString().split('T')[0]; @@ -606,7 +548,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, }); @@ -843,18 +785,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(depletionProductsData) && selectedKandang) { const data = depletionProductsData.data as unknown as ProductWarehouse[]; data.forEach((product) => { - const productName = product.product.name; - - if ( - productName.toLowerCase().includes('culling') || - productName.toLowerCase().includes('mati') || - productName.toLowerCase().includes('afkir') - ) { - options.push({ - value: product.id, - label: product.product.name, - }); - } + options.push({ + value: product.id, + label: product.product.name, + }); }); } @@ -886,20 +820,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 +1876,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 +2066,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} -
- Standard FCR -
- fcrStandardModal.openModal()} - > - {initialValues.project_flock?.fcr?.name || '-'} - -
-
Standard Produksi @@ -2227,21 +2117,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 +2462,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } + disabled={type === 'detail'} /> {getStockUsageAdornment(idx)}
@@ -2793,6 +2669,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } + disabled={type === 'detail'} /> {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( @@ -3009,6 +2886,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} placeholder='Masukkan jumlah telur' inputSuffix={'Butir'} + disabled={type === 'detail'} /> @@ -3035,6 +2913,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 +3162,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 */} TransferToLayingApi.getSingle(Number(id)) + ([id]) => TransferToLayingApi.getSingle(Number(id)) ); const confirmationTableColumns: ColumnDef[] = @@ -230,7 +230,7 @@ const TransferToLayingConfirmationModal = ({ text: primaryButton?.text ?? 'Oke', color: primaryButton?.color ?? 'primary', className: 'rounded-lg', - onClick: (e) => { + onClick: () => { if (withNote) { primaryButton?.onClick?.(notes); } else if (primaryButton && primaryButton?.onClick) { diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingDetailModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingDetailModal.tsx index 7be756e2..f73ab265 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingDetailModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingDetailModal.tsx @@ -40,10 +40,7 @@ const TransferToLayingDetailModal = () => { ? transferToLayingResponse.data : undefined; - const { - data: transferToLayingApprovalResponse, - isLoading: isLoadingTransferToLayingApproval, - } = useSWR( + const { data: transferToLayingApprovalResponse } = useSWR( transferToLayingId ? ['approval-transfer-to-laying', transferToLayingId] : undefined, diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index 78de70e1..2487f10b 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -60,13 +60,12 @@ const TransferToLayingFormModal = () => { ); }; - const { data: transferToLaying, isLoading: isLoadingTransferToLaying } = - useSWR( - isModalActionForForm && transferToLayingId - ? ['detail-transfer-to-laying', transferToLayingId] - : undefined, - ([, id]) => TransferToLayingApi.getSingle(Number(id)) - ); + const { data: transferToLaying } = useSWR( + isModalActionForForm && transferToLayingId + ? ['detail-transfer-to-laying', transferToLayingId] + : undefined, + ([, id]) => TransferToLayingApi.getSingle(Number(id)) + ); /** * Step 1: General Information @@ -83,6 +82,9 @@ const TransferToLayingFormModal = () => { TransferToLayingFormValues | undefined >(undefined); const [formErrorMessage, setFormErrorMessage] = useState(null); + const [submittedActionType, setSubmittedActionType] = useState< + 'add' | 'edit' | null + >(null); // Flock Source const { @@ -175,7 +177,7 @@ const TransferToLayingFormModal = () => { [router] ); - const [formikInitialValues, setFormikInitialValues] = useState( + const [formikInitialValues] = useState( getTransferToLayingFormInitialValues() ); @@ -203,6 +205,7 @@ const TransferToLayingFormModal = () => { }; setFormikLastValues(values); + setSubmittedActionType(modalAction as 'add' | 'edit'); switch (modalAction) { case 'add': @@ -234,10 +237,7 @@ const TransferToLayingFormModal = () => { ) : undefined; - const { - data: flockSourceKandangsAvailability, - isLoading: isLoadingFlockSourceKandangsAvailability, - } = useSWR( + const { data: flockSourceKandangsAvailability } = useSWR( formik.values.flockSource ? [ 'transfer-to-laying', @@ -293,10 +293,7 @@ const TransferToLayingFormModal = () => { return { available: countAvailable, unavailable: countUnavailable }; }, [mappedFlockSourceKandangsAvailability]); - const { - data: flockDestinationKandangsMaxTargetQty, - isLoading: isLoadingFlockDestinationKandangsMaxTargetQty, - } = useSWR( + const { data: flockDestinationKandangsMaxTargetQty } = useSWR( formik.values.flockDestination ? [ 'transfer-to-laying', @@ -1059,10 +1056,21 @@ const TransferToLayingFormModal = () => { setFormikLastValues(undefined)} + onClose={() => { + setFormikLastValues(undefined); + setSubmittedActionType(null); + }} secondaryButton={undefined} /> diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 438c529c..bf4c31e3 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -680,6 +680,7 @@ const TransferToLayingsTable = () => { subtitleText='Are you sure you want to delete this data? ' transferToLayingIds={selectedRowIds} primaryButton={{ + text: 'Delete', isLoading: isDeleteLoading, color: 'error', onClick: confirmationModalDeleteClickHandler, @@ -704,6 +705,7 @@ const TransferToLayingsTable = () => { withNote noteLabel='Notes Approval' primaryButton={{ + text: 'Approve', isLoading: isApproveLoading, onClick: confirmationModalApproveClickHandler, }} @@ -735,6 +737,7 @@ const TransferToLayingsTable = () => { }, }} primaryButton={{ + text: 'Reject', isLoading: isRejectLoading, color: 'error', onClick: confirmationModalRejectClickHandler, diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts index b34bef24..9211c2bc 100644 --- a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts +++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts @@ -2,9 +2,6 @@ import * as Yup from 'yup'; import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; import { formatDate, formatNumber } from '@/lib/helper'; -import { ProjectFlock } from '@/types/api/production/project-flock'; -import { ProjectFlockApi } from '@/services/api/production/project-flock'; -import { isResponseSuccess } from '@/lib/api-helper'; type TransferToLayingFormSchemaType = { transfer_date?: string; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 39112b47..3473967e 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -26,7 +26,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import UniformityTableSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; import Modal from '@/components/Modal'; import SelectInput, { diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 8ab62d85..724f7b81 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; @@ -203,16 +203,19 @@ const UniformityForm = ({ // ===== RECORDINGS DATA (FOR WEEK CALCULATION) ===== const recordingsUrl = useMemo(() => { + if (!projectFlockKandangLookup?.project_flock_kandang_id) return null; const params = new URLSearchParams({ page: '1', limit: '100', + project_flock_kandang_id: + projectFlockKandangLookup.project_flock_kandang_id.toString(), }); return `${RecordingApi.basePath}?${params.toString()}`; - }, []); + }, [projectFlockKandangLookup?.project_flock_kandang_id]); const { data: recordingsData } = useSWR( recordingsUrl, - RecordingApi.getAllFetcher + recordingsUrl ? RecordingApi.getAllFetcher : null ); // ===== FORM CONFIGURATION ===== @@ -400,50 +403,46 @@ const UniformityForm = ({ useEffect(() => { if ( projectFlockKandangLookup?.chick_in_date && - projectFlockKandangLookup?.project_flock_kandang_id && - isResponseSuccess(recordingsData) && - recordingsData.data + projectFlockKandangLookup?.project_flock_kandang_id ) { - const matchingRecordings = recordingsData.data.filter( - (recording: Recording) => - recording.project_flock?.project_flock_kandang_id === - projectFlockKandangLookup.project_flock_kandang_id - ); + const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); + chickInDate.setHours(0, 0, 0, 0); - matchingRecordings.sort( - (a: Recording, b: Recording) => - new Date(a.record_datetime).getTime() - - new Date(b.record_datetime).getTime() - ); + let initialWeek = 18; - const earliestRecording = matchingRecordings[0]; + if ( + isResponseSuccess(recordingsData) && + recordingsData.data && + recordingsData.data.length > 0 + ) { + const sortedRecordings = [...recordingsData.data].sort( + (a: Recording, b: Recording) => + new Date(a.record_datetime).getTime() - + new Date(b.record_datetime).getTime() + ); - if (earliestRecording) { - const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); - chickInDate.setHours(0, 0, 0, 0); - - const earliestRecordDate = new Date(earliestRecording.record_datetime); - earliestRecordDate.setHours(0, 0, 0, 0); - - const initialWeek = - earliestRecording.project_flock?.production_standart?.week || 18; - - if (formik.values.date) { - const selectedDate = new Date(formik.values.date); - selectedDate.setHours(0, 0, 0, 0); - - const daysDiff = Math.floor( - (selectedDate.getTime() - chickInDate.getTime()) / - (1000 * 60 * 60 * 24) - ); - - const weeksDiff = Math.floor(daysDiff / 7); - - formik.setFieldValue('week', initialWeek + weeksDiff); - } else { - formik.setFieldValue('week', initialWeek); + const earliestRecording = sortedRecordings[0]; + if (earliestRecording?.project_flock?.production_standart?.week) { + initialWeek = + earliestRecording.project_flock.production_standart.week; } } + + if (formik.values.date) { + const selectedDate = new Date(formik.values.date); + selectedDate.setHours(0, 0, 0, 0); + + const daysDiff = Math.floor( + (selectedDate.getTime() - chickInDate.getTime()) / + (1000 * 60 * 60 * 24) + ); + + const weeksDiff = Math.floor(daysDiff / 7); + + formik.setFieldValue('week', initialWeek + weeksDiff); + } else { + formik.setFieldValue('week', initialWeek); + } } }, [ projectFlockKandangLookup?.chick_in_date, diff --git a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx index 3cc120fd..3ca24952 100644 --- a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index eaf51103..108cb4f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { useRouter } from 'next/navigation'; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 2c08f726..733165f8 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -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 = { @@ -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..35882869 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -153,11 +153,9 @@ const PurchaseOrderAcceptApprovalForm = ({ // ===== SELECT INPUT DATA ===== const { - setInputValue: setExpeditionsSelectInputValue, options: expeditionVendors, isLoadingOptions: isLoadingExpeditions, loadMore: loadMoreExpeditions, - hasMore: hasMoreExpeditions, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'BOP', flag: 'EKSPEDISI', @@ -343,19 +341,6 @@ const PurchaseOrderAcceptApprovalForm = ({ ) => { const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value; formik.setFieldValue(`items.${idx}.${field}`, numValue); - - if (field === 'received_qty' || field === 'transport_per_item') { - const receivedQty = - field === 'received_qty' - ? numValue - : parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0; - const transportPerItem = - field === 'transport_per_item' - ? numValue - : parseFloat( - formik.values.items?.[idx]?.transport_per_item as string - ) || 0; - } }; return ( @@ -402,6 +387,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 +572,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/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 90f79e6d..53ff5027 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -55,7 +55,7 @@ const PurchaseRequestForm = ({ const deleteModal = useModal(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); + const [, setLocationSelectInputValue] = useState(''); const [selectedPurchaseItems, setSelectedPurchaseItems] = useState( [] ); @@ -149,7 +149,6 @@ const PurchaseRequestForm = ({ isLoadingOptions: isLoadingSuppliers, rawData: supplierRawData, loadMore: loadMoreSuppliers, - hasMore: hasMoreSuppliers, } = useSelect(SupplierApi.basePath, 'id', 'name', 'search', { category: 'SAPRONAK', }); @@ -164,7 +163,6 @@ const PurchaseRequestForm = ({ options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, - hasMore: hasMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', '', { area_id: selectedArea != '' @@ -173,12 +171,10 @@ const PurchaseRequestForm = ({ }); const { - inputValue: warehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue, options: warehouseOptions, isLoadingOptions: isLoadingWarehouses, loadMore: loadMoreWarehouses, - hasMore: hasMoreWarehouses, } = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', { area_id: selectedArea != '' @@ -651,7 +647,7 @@ const PurchaseRequestForm = ({ {formik.values.items?.map((item, idx) => ( {type !== 'detail' && ( - + { @@ -1042,7 +1041,7 @@ const PurchaseOrderDetail = ({ ref={staffApprovalModal.ref} closeOnBackdrop className={{ - modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto', + modalBox: 'w-full max-w-2xl max-h-[90vh] overflow-y-auto', }} > { +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' ? ( - - - - - Export{' '} - - - } - > - - - - - -
    -
    -
    - - -
    - ); -}; - -export default DailyMarketingReportContent; diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx deleted file mode 100644 index 86ee29bc..00000000 --- a/src/components/pages/report/DailyMarketingReportPDF.tsx +++ /dev/null @@ -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 ( - - - - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - - - - - - PT LUMBUNG TELUR INDONESIA - - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - - - - - - Laporan Penjualan Harian - - - {/* Data Table */} - - {/* Header */} - - - No - - - - Tgl SO - - - - - Tgl DO - - - - Aging - - - - Gudang - - - - - Pelanggan - - - - Sales - - - - Produk - - - - No DO - - - - Plat No - - - - Tipe - - - Qty - - - - Rerata - - - - Berat - - - - Hrg Jual - - - - - HPP/kg - - - - - Total Jual - - - - - Total HPP - - - - - {/* Rows */} - {rows.map((row, index) => ( - - - - {index + 1} - - - - - {formatDate(row.so_date, 'DD/MM/YYYY')} - - - - - {formatDate(row.realization_date, 'DD/MM/YYYY')} - - - - - {row.aging_days} - - - - - {row.warehouse?.name} - - - - - {row.customer?.name} - - - - - {row.sales.name} - - - - - {row.product?.name} - - - - - {row.do_number} - - - - - {row.vehicle_number} - - - - - {row.marketing_type} - - - - - {formatNumber(row.qty)} - - - - - {formatNumber(row.average_weight_kg)} - - - - - {formatNumber(row.total_weight_kg)} - - - - - {formatCurrency(row.sales_price_per_kg)} - - - - - {formatCurrency(row.hpp_price_per_kg)} - - - - - {formatCurrency(row.sales_amount)} - - - - - {formatCurrency(row.hpp_amount)} - - - - ))} - - - {/* Summary */} - - - - - Total Qty: - - - {formatNumber(summary?.total_qty ?? 0)} - - - - - Total Berat (kg): - - - {formatNumber(summary?.total_weight_kg ?? 0)} - - - - - Total Penjualan: - - - {formatCurrency(summary?.total_sales_amount ?? 0)} - - - - - Total HPP Per KG: - - - {formatCurrency(summary?.total_hpp_price_per_kg ?? 0)} - - - - - Total HPP: - - - {formatCurrency(summary?.total_hpp_amount ?? 0)} - - - - - - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - - - - ); -}; - -export default DailyMarketingReportPDF; diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx deleted file mode 100644 index 4904ef16..00000000 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ /dev/null @@ -1,289 +0,0 @@ -'use client'; - -import { ChangeEventHandler, useEffect, useState } from 'react'; -import useSWR from 'swr'; -import { ColumnDef, SortingState } from '@tanstack/react-table'; - -import { Icon } from '@iconify/react'; -import Table from '@/components/Table'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; - -import { - cn, - formatCurrency, - formatDate, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { DailyMarketingRow } from '@/types/api/report/marketing'; -import { MarketingReportApi } from '@/services/api/report/marketing-report'; - -interface DailyMarketingsTableProps { - dailyMarketingsReportUrl: string; - onSetPage: (page: number) => void; - pageSize: number; - onSetPageSize: (pageSize: number) => void; - searchValue: string; - onSearchChange: ChangeEventHandler; - onFilterByChange: (filterBy: string) => void; - onSortByChange: (sort: 'asc' | 'desc' | '') => void; -} - -const DailyMarketingsTable = ({ - dailyMarketingsReportUrl, - onSetPage, - pageSize, - onSetPageSize, - searchValue, - onSearchChange, - onFilterByChange, - onSortByChange, -}: DailyMarketingsTableProps) => { - const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR( - dailyMarketingsReportUrl, - MarketingReportApi.getAllDailyMarketingFetcher, - { - keepPreviousData: true, - } - ); - - const [open, setOpen] = useState(true); - - const [sorting, setSorting] = useState([]); - - const dailyMarketingColumns: ColumnDef[] = [ - { - header: 'No', - cell: (props) => props.row.index + 1, - }, - { - accessorKey: 'so_date', - header: 'Tanggal Jual', - cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), - footer: 'Total', - }, - { - accessorKey: 'realization_date', - header: 'Tanggal Realisasi', - cell: (props) => - formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), - }, - { - accessorKey: 'aging_days', - header: 'Aging', - cell: (props) => `${props.row.original.aging_days} hari`, - }, - { - accessorKey: 'warehouse', - header: 'Gudang', - cell: ({ row }) => row.original.warehouse.name, - }, - { - accessorKey: 'customer', - header: 'Pelanggan', - cell: ({ row }) => row.original.customer.name, - }, - { - accessorKey: 'do_number', - header: 'No. DO', - enableSorting: false, - }, - { - accessorKey: 'sales_person', - header: 'Sales/Marketing', - cell: (props) => props.row.original.sales.name, - }, - { - accessorKey: 'vehicle_number', - header: 'No. Polisi', - cell: (props) => ( - - {formatVechicleNumber(props.row.original.vehicle_number)} - - ), - }, - { - accessorKey: 'marketing_type', - header: 'Marketing Type', - enableSorting: false, - }, - { - accessorKey: 'product', - header: 'Produk', - cell: ({ row }) => row.original.product.name, - }, - { - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => formatNumber(props.row.original.qty), - footer: () => { - const totalQty = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_qty - : 0; - - return totalQty ? formatNumber(totalQty) : '-'; - }, - }, - { - accessorKey: 'average_weight', - header: 'Bobot Rata-Rata (Kg)', - cell: (props) => formatNumber(props.row.original.average_weight_kg), - footer: () => { - const totalAverageWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_weight_kg - : 0; - - return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-'; - }, - }, - { - accessorKey: 'total_weight', - header: 'Bobot Total (Kg)', - cell: (props) => formatNumber(props.row.original.total_weight_kg), - footer: () => { - const totalWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_weight_kg - : 0; - - return totalWeightKg ? formatNumber(totalWeightKg) : '-'; - }, - }, - { - accessorKey: 'sales_price', - header: 'Harga Jual (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), - footer: () => { - const totalSalesPrice = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_sales_price - : 0; - - return totalSalesPrice ? formatNumber(totalSalesPrice) : '-'; - }, - }, - { - accessorKey: 'hpp_price', - header: 'HPP (Rp)', - cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), - footer: () => { - const totalHppPricePerKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_hpp_price_per_kg - : 0; - - return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-'; - }, - }, - { - accessorKey: 'sales_amount', - header: 'Total (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_amount), - footer: () => { - const totalSalesAmount = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_sales_amount - : 0; - - return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-'; - }, - }, - ]; - - useEffect(() => { - if (sorting.length === 1) { - onFilterByChange(sorting[0].id); - onSortByChange(sorting[0].desc ? 'desc' : 'asc'); - } else { - onFilterByChange(''); - onSortByChange(''); - } - }, [sorting]); - - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.length > 0 - : false - ); - } - }, [dailyMarketings, isResponseSuccess]); - - return ( - - -
    Penjualan Harian
    - - -
    - } - className='w-full!' - titleClassName='w-full p-0!' - > -
    -
    -
    - -
    -
    - - - data={ - isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : [] - } - columns={dailyMarketingColumns} - pageSize={pageSize} - onPageSizeChange={onSetPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.total_results - : 0 - } - onPageChange={onSetPage} - isLoading={isLoadingDailyMarketings} - sorting={sorting} - setSorting={setSorting} - renderFooter={true} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(dailyMarketings) && - dailyMarketings?.data?.length === 0, - }), - }} - /> -
    - - - ); -}; - -export default DailyMarketingsTable; diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx deleted file mode 100644 index 3ebacecb..00000000 --- a/src/components/pages/report/MarketingReportContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import { JSX, useState } from 'react'; - -import Tabs from '@/components/Tabs'; -import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent'; -import HppPerKandangTab from './sale/tab/HppPerKandangTab'; - -type MarketingReportTabType = - | 'daily' - | 'transaction' - | 'hpp-comparison' - | 'daily-hpp'; - -const marketingReportTabs: { - id: MarketingReportTabType; - label: string; - content: JSX.Element; -}[] = [ - { - id: 'daily', - label: 'Penjualan Harian', - content: , - }, - { - id: 'daily-hpp', - label: 'HPP Harian Kandang', - content: , - }, -]; - -const MarketingReportContent = () => { - const [activeTab, setActiveTab] = useState('daily'); - - return ( -
    - -
    - ); -}; - -export default MarketingReportContent; diff --git a/src/components/pages/report/expense/ReportExpenseTable.tsx b/src/components/pages/report/expense/ReportExpenseTable.tsx deleted file mode 100644 index c809c153..00000000 --- a/src/components/pages/report/expense/ReportExpenseTable.tsx +++ /dev/null @@ -1,901 +0,0 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; -import useSWR from 'swr'; -import Button from '@/components/Button'; -import Card from '@/components/Card'; -import DateInput from '@/components/input/DateInput'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; -import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; -import { ReportExpense } from '@/types/api/report/report-expense'; -import { Icon } from '@iconify/react'; -import { ColumnDef } from '@tanstack/react-table'; -import { ReportExpenseApi } from '@/services/api/report'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; -import Dropdown from '@/components/dropdown/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import * as XLSX from 'xlsx'; -import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; -import toast from 'react-hot-toast'; -import { - KandangApi, - LocationApi, - NonstockApi, - SupplierApi, -} from '@/services/api/master-data'; -import { Supplier } from '@/types/api/master-data/supplier'; -import { Kandang } from '@/types/api/master-data/kandang'; -import { Nonstock } from '@/types/api/master-data/nonstock'; - -const ReportExpenseTable = () => { - // ===== STATE MANAGEMENT ===== - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); - const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [pdfProgress, setPdfProgress] = useState(0); - const [excelProgress, setExcelProgress] = useState(0); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; - - // ===== SUBMISSION STATE ===== - const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== TABLE FILTER STATE ===== - const { - state: filterState, - updateFilter, - setPage, - setPageSize, - reset: resetFilterState, - toQueryString, - } = useTableFilter({ - initial: { - location_id: '', - supplier_id: '', - kandang_id: '', - nonstock_id: '', - realization_date: '', - category: '', - search: '', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); - - // ===== SELECT OPTIONS ===== - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const { - setInputValue: setSupplierInputValue, - options: supplierOptions, - isLoadingOptions: isLoadingSupplierOptions, - loadMore: loadMoreSuppliers, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangOptions, - loadMore: loadMoreKandangs, - } = useSelect(KandangApi.basePath, 'id', 'name'); - - const { - setInputValue: setNonstockInputValue, - options: nonstockOptions, - isLoadingOptions: isLoadingNonstockOptions, - loadMore: loadMoreNonstocks, - } = useSelect(NonstockApi.basePath, 'id', 'name'); - - const categoryOptions = useMemo( - () => [ - { value: 'BOP', label: 'BOP' }, - { value: 'NON-BOP', label: 'Non BOP' }, - ], - [] - ); - - // Mendapatkan value option select dari filter state - const selectedLocation = useMemo( - () => - locationOptions.find( - (opt) => String(opt.value) === filterState.location_id - ) || null, - [locationOptions, filterState.location_id] - ); - const selectedSupplier = useMemo( - () => - supplierOptions.find( - (opt) => String(opt.value) === filterState.supplier_id - ) || null, - [supplierOptions, filterState.supplier_id] - ); - const selectedKandang = useMemo( - () => - kandangOptions.find( - (opt) => String(opt.value) === filterState.kandang_id - ) || null, - [kandangOptions, filterState.kandang_id] - ); - const selectedNonstock = useMemo( - () => - nonstockOptions.find( - (opt) => String(opt.value) === filterState.nonstock_id - ) || null, - [nonstockOptions, filterState.nonstock_id] - ); - const selectedCategory = useMemo( - () => - categoryOptions.find((opt) => opt.value === filterState.category) || null, - [categoryOptions, filterState.category] - ); - - // ===== FILTER CHANGE HANDLERS ===== - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('location_id', option ? String(option.value) : ''); - updateFilter('kandang_id', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('kandang_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('supplier_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const nonstockChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('nonstock_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const categoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('category', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const realizationDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - updateFilter('realization_date', e.target.value || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const searchChangeHandler = useCallback( - (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - setIsSubmitted(false); - }, - [updateFilter] - ); - - // ===== RESET FILTERS ===== - const resetFilters = useCallback(() => { - resetFilterState(); - setIsSubmitted(false); - }, [resetFilterState]); - - // ===== SUBMIT HANDLER ===== - const handleSubmit = useCallback(() => { - setIsSubmitted(true); - setPage(1); - }, [setPage]); - - // ===== DATA FETCHING FOR TABLE ===== - const { data: reportExpenseResponse, isLoading } = useSWR( - isSubmitted - ? () => { - return ['report-expense', toQueryString()]; - } - : null, - ([, query]) => { - const endpoint = `${ReportExpenseApi.basePath}${query}`; - return ReportExpenseApi.getAllFetcher(endpoint); - } - ); - - const data: ReportExpense[] = useMemo( - () => - isResponseSuccess(reportExpenseResponse) - ? (reportExpenseResponse?.data as ReportExpense[]) || [] - : [], - [reportExpenseResponse] - ); - - const meta = useMemo( - () => - isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta - ? reportExpenseResponse.meta - : null, - [reportExpenseResponse] - ); - - // ===== EXPORT DATA FETCHER ===== - const reportExpenseExport = useCallback(async (): Promise< - ReportExpense[] | null - > => { - const params = new URLSearchParams(toQueryString().replace('?', '')); - params.set('limit', 'limit'); - params.set('page', '1'); - - const endpoint = `${ReportExpenseApi.basePath}?${params.toString()}`; - const response = await ReportExpenseApi.getAllFetcher(endpoint); - - return isResponseSuccess(response) ? response.data : null; - }, [toQueryString]); - - // ===== EXPORT HANDLERS ===== - const handleExportPdf = useCallback(async () => { - if (isPdfExportLoading) return; - setIsPdfExportLoading(true); - setPdfProgress(0); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setPdfProgress(10); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allData = await reportExpenseExport(); - if (!allData || allData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsPdfExportLoading(false); - setPdfProgress(0); - return; - } - - // Stage 2: Data fetched - langsung loncat ke progress tinggi - setPdfProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - const progressInterval = setInterval(() => { - setPdfProgress((prev) => { - // Increment kecil dan random antara 0.5-2% - const increment = Math.random() * 1.5 + 0.5; - const newProgress = Math.min(prev + increment, 50); - return newProgress; - }); - }, 300); // Update setiap 300ms - - const pdfParams = { - location_name: selectedLocation?.label, - supplier_name: selectedSupplier?.label, - kandang_name: selectedKandang?.label, - nonstock_name: selectedNonstock?.label, - category: selectedCategory?.label, - realization_date: filterState.realization_date, - search: filterState.search, - }; - - setDropdownOpen(false); - - // Stage 3: Langsung loncat ke 80-85% untuk menghindari stuck - const baseProgress = 80 + Math.floor(Math.random() * 16); // Random 80-85% - setPdfProgress(baseProgress); - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Stage 4: Berikan jeda untuk UI update - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - // Proses PDF yang sebenarnya - await generateReportExpensePDF(allData, pdfParams); - - clearInterval(progressInterval); - - // Stage 5: Finalizing (98-100%) - setPdfProgress(99); - await new Promise((resolve) => setTimeout(resolve, 100)); - - setPdfProgress(100); - toast.success('PDF berhasil dibuat dan diunduh.'); - - // Reset progress setelah selesai - setTimeout(() => setPdfProgress(0), 500); - } catch (error) { - console.error('PDF Export Error:', error); - toast.error('Gagal membuat PDF. Silakan coba lagi.'); - setPdfProgress(0); - } finally { - setIsPdfExportLoading(false); - } - }, [ - reportExpenseExport, - selectedLocation, - selectedSupplier, - selectedKandang, - selectedNonstock, - selectedCategory, - filterState.realization_date, - filterState.search, - ]); - - const handleExportExcel = useCallback(async () => { - if (isExcelExportLoading) return; - setIsExcelExportLoading(true); - setExcelProgress(0); - setDropdownOpen(false); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setExcelProgress(15); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allDataForExport = await reportExpenseExport(); - - if (!allDataForExport || allDataForExport.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsExcelExportLoading(false); - setExcelProgress(0); - return; - } - - // Stage 2: Data fetched (20-40%) - setExcelProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 3: Grouping data (40-60%) - setExcelProgress(50); - const groupedBySupplier: Record = {}; - allDataForExport.forEach((item) => { - const supplierName = item.supplier?.name || 'Unknown Supplier'; - if (!groupedBySupplier[supplierName]) { - groupedBySupplier[supplierName] = []; - } - groupedBySupplier[supplierName].push(item); - }); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 4: Creating workbook (60-80%) - setExcelProgress(70); - const workbook = XLSX.utils.book_new(); - - const supplierEntries = Object.entries(groupedBySupplier); - const totalSuppliers = supplierEntries.length; - - for (let i = 0; i < supplierEntries.length; i++) { - const [supplierName, supplierData] = supplierEntries[i]; - - // Update progress per supplier - const progressIncrement = (20 / totalSuppliers) * (i + 1); - setExcelProgress(70 + progressIncrement); - - const totals = supplierData.reduce( - (acc, item) => ({ - qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), - total_pengajuan: - acc.total_pengajuan + - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), - total_realisasi: - acc.total_realisasi + - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - }), - { - qty_pengajuan: 0, - total_pengajuan: 0, - qty_realisasi: 0, - total_realisasi: 0, - } - ); - - const excelData = supplierData.map((item, index) => ({ - No: index + 1, - 'No. PO': item.po_number || '', - 'No. Referensi': item.reference_number || '', - 'Tanggal Realisasi': item.realization_date - ? formatDate(item.realization_date, 'DD MMM YYYY') - : '', - 'Tanggal Transaksi': item.transaction_date - ? formatDate(item.transaction_date, 'DD MMM YYYY') - : '', - Kategori: item.category || '', - Produk: item.pengajuan?.nonstock?.name || '', - Lokasi: item.kandang?.location?.name || '', - Kandang: item.kandang?.name || '', - 'Qty Pengajuan': item.pengajuan?.qty || 0, - 'Harga Pengajuan': item.pengajuan?.price || 0, - 'Total Pengajuan': - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - 'Qty Realisasi': item.realisasi?.qty || 0, - 'Harga Realisasi': item.realisasi?.price || 0, - 'Total Realisasi': - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - 'Status Pencairan': item.latest_approval?.step_name || '', - })); - - excelData.push({ - No: 'Total' as unknown as number, - 'No. PO': '', - 'No. Referensi': '', - 'Tanggal Realisasi': '', - 'Tanggal Transaksi': '', - Kategori: '', - Produk: '', - Lokasi: '', - Kandang: '', - 'Qty Pengajuan': totals.qty_pengajuan, - 'Harga Pengajuan': 0, - 'Total Pengajuan': totals.total_pengajuan, - 'Qty Realisasi': totals.qty_realisasi, - 'Harga Realisasi': 0, - 'Total Realisasi': totals.total_realisasi, - 'Status Pencairan': '', - }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - const colWidths = [ - { wch: 5 }, // No - { wch: 20 }, // No. PO - { wch: 20 }, // No. Referensi - { wch: 15 }, // Tanggal Realisasi - { wch: 15 }, // Tanggal Transaksi - { wch: 15 }, // Kategori - { wch: 30 }, // Produk - { wch: 20 }, // Lokasi - { wch: 15 }, // Kandang - { wch: 15 }, // Qty Pengajuan - { wch: 15 }, // Harga Pengajuan - { wch: 20 }, // Total Pengajuan - { wch: 15 }, // Qty Realisasi - { wch: 15 }, // Harga Realisasi - { wch: 20 }, // Total Realisasi - { wch: 20 }, // Status Pencairan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = supplierName.slice(0, 31); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - - // Small delay to allow UI update - if (i < supplierEntries.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - - // Stage 5: Writing file (90-100%) - setExcelProgress(95); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - XLSX.writeFile(workbook, filename); - - setExcelProgress(100); - toast.success('Excel berhasil dibuat dan diunduh.'); - - // Reset progress - setTimeout(() => setExcelProgress(0), 500); - } catch (error) { - console.error('Excel Export Error:', error); - toast.error('Gagal membuat Excel. Silakan coba lagi.'); - setExcelProgress(0); - } finally { - setIsExcelExportLoading(false); - } - }, [isExcelExportLoading, reportExpenseExport]); - - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setPage(page); - }; - - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; - - const handleNextPage = () => { - if (meta && filterState.page < meta.total_pages) { - setPage(filterState.page + 1); - } - }; - - const handlePrevPage = () => { - if (filterState.page > 1) { - setPage(filterState.page - 1); - } - }; - - // ===== TABLE COLUMNS DEFINITION ===== - const columns = useMemo((): ColumnDef[] => { - return [ - { - header: 'No', - accessorFn: (_, index) => - (filterState.page - 1) * filterState.pageSize + index + 1, - }, - { - header: 'No. PO', - accessorKey: 'po_number', - }, - { - header: 'No. Referensi', - accessorKey: 'reference_number', - }, - { - header: 'Tanggal Realisasi', - accessorKey: 'realization_date', - cell: ({ row }) => { - return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Tanggal Transaksi', - accessorKey: 'transaction_date', - cell: ({ row }) => { - return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Kategori', - accessorKey: 'category', - }, - { - header: 'Produk', - accessorFn: (row) => row.pengajuan?.nonstock?.name, - }, - { - header: 'Supplier', - accessorFn: (row) => row.supplier?.name, - }, - { - header: 'Lokasi', - accessorFn: (row) => row.kandang?.location?.name, - }, - { - header: 'Kandang', - accessorFn: (row) => row.kandang?.name, - }, - { - header: 'Pengajuan', - columns: [ - { - header: 'Qty', - id: 'qty_pengajuan', - accessorFn: (row) => row.pengajuan?.qty, - cell: ({ row }) => - row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_pengajuan', - accessorFn: (row) => row.pengajuan?.price, - cell: ({ row }) => - formatCurrency(row.original.pengajuan?.price || 0), - }, - { - header: 'Total', - id: 'total_pengajuan', - accessorFn: (row) => - (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), - cell: ({ row }) => { - const total = - (row.original.pengajuan?.qty || 0) * - (row.original.pengajuan?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Realisasi', - columns: [ - { - header: 'Qty', - id: 'qty_realisasi', - accessorFn: (row) => row.realisasi?.qty, - cell: ({ row }) => - row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_realisasi', - accessorFn: (row) => row.realisasi?.price, - cell: ({ row }) => - formatCurrency(row.original.realisasi?.price || 0), - }, - { - header: 'Total', - id: 'total_realisasi', - accessorFn: (row) => - (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), - cell: ({ row }) => { - const total = - (row.original.realisasi?.qty || 0) * - (row.original.realisasi?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Status Pencairan', - cell: (props) => ( - - ), - }, - { - header: 'Status BOP', - cell: (props) => ( - - ), - }, - ]; - }, [filterState.page, filterState.pageSize]); - - // ===== RENDER ===== - return ( -
    - {isAnyExportLoading && ( -
    - - {((isPdfExportLoading && pdfProgress > 0) || - (isExcelExportLoading && excelProgress > 0)) && ( -
    -
    - {(() => { - const currentProgress = isPdfExportLoading - ? pdfProgress - : excelProgress; - const exportType = isPdfExportLoading ? 'PDF' : 'Excel'; - - if (currentProgress < 20) - return 'Mengambil data dari server...'; - if (currentProgress < 30) return 'Memproses data laporan...'; - if (currentProgress < 40) - return `Menyiapkan struktur dokumen ${exportType}...`; - if (currentProgress < 50) - return 'Mengelompokkan data per supplier...'; - if (currentProgress < 70) - return 'Merender tabel dan kalkulasi...'; - if (currentProgress < 96) - return `Memformat dokumen ${exportType}...`; - if (currentProgress < 100) - return 'Menyelesaikan dan mengunduh...'; - return 'Selesai!'; - })()}{' '} - {Math.round(isPdfExportLoading ? pdfProgress : excelProgress)}% -
    - {((isPdfExportLoading && pdfProgress >= 35 && pdfProgress < 90) || - (isExcelExportLoading && - excelProgress >= 35 && - excelProgress < 90)) && ( -
    - {(isPdfExportLoading ? pdfProgress : excelProgress) < 96 - ? 'Proses ini membutuhkan waktu lebih lama untuk data dalam jumlah besar. Mohon bersabar...' - : 'Sedang memproses baris data. Hampir selesai...'} -
    - )} -
    - )} -
    - )} - -
    -
    - - -
    -
    - { - setDropdownOpen(!dropdownOpen); - }} - > - Export - - } - align='end' - direction='bottom' - open={dropdownOpen} - > - - - - - -
    -
    -
    - } - > -
    - - - - - - - } - /> -
    - - - {/* ===== TABLE CONTENT ===== */} - {!isSubmitted ? ( -
    - Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
    - ) : isLoading ? ( -
    - -
    - ) : data.length === 0 ? ( -
    - Tidak ada data yang dapat ditampilkan... -
    - ) : ( - <> - - columns={columns} - data={data} - pageSize={10} - className={{ - containerClassName: 'mb-0', - headerRowClassName: cn( - TABLE_DEFAULT_STYLING, - 'whitespace-nowrap' - ), - bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'), - paginationClassName: 'hidden', - }} - /> - {meta && ( -
    - -
    - )} - - )} -
    - ); -}; - -export default ReportExpenseTable; diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx new file mode 100644 index 00000000..704d1f6f --- /dev/null +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; + +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import ReportExpenseTab from './tab/ReportExpenseTab'; + +const ReportExpenseTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Laporan Biaya Operasional', + content: , + }, + ]; + + return ( +
    + +
    + ); +}; + +export default ReportExpenseTabs; diff --git a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx similarity index 99% rename from src/components/pages/report/expense/pdf/ReportExpenseExport.tsx rename to src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx index a7ff8599..6ec2c559 100644 --- a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx +++ b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx @@ -2,6 +2,7 @@ import { ReportExpense } from '@/types/api/report/report-expense'; import { formatCurrency, formatDate } from '@/lib/helper'; import jsPDF from 'jspdf'; import autoTable, { UserOptions } from 'jspdf-autotable'; + interface jsPDFWithAutoTable extends jsPDF { lastAutoTable: { finalY: number; diff --git a/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx new file mode 100644 index 00000000..d64c0199 --- /dev/null +++ b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx @@ -0,0 +1,109 @@ +import * as XLSX from 'xlsx'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { formatDate } from '@/lib/helper'; + +export const generateReportExpenseExcel = async ( + data: ReportExpense[] +): Promise => { + // Group by supplier + const groupedBySupplier: Record = {}; + data.forEach((item) => { + const supplierName = item.supplier?.name || 'Unknown Supplier'; + if (!groupedBySupplier[supplierName]) { + groupedBySupplier[supplierName] = []; + } + groupedBySupplier[supplierName].push(item); + }); + + const workbook = XLSX.utils.book_new(); + + Object.entries(groupedBySupplier).forEach(([supplierName, supplierData]) => { + const totals = supplierData.reduce( + (acc, item) => ({ + qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), + total_pengajuan: + acc.total_pengajuan + + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), + total_realisasi: + acc.total_realisasi + + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + }), + { + qty_pengajuan: 0, + total_pengajuan: 0, + qty_realisasi: 0, + total_realisasi: 0, + } + ); + + const excelData = supplierData.map((item, index) => ({ + No: index + 1, + 'No. PO': item.po_number || '', + 'No. Referensi': item.reference_number || '', + 'Tanggal Realisasi': item.realization_date + ? formatDate(item.realization_date, 'DD MMM YYYY') + : '', + 'Tanggal Transaksi': item.transaction_date + ? formatDate(item.transaction_date, 'DD MMM YYYY') + : '', + Kategori: item.category || '', + Produk: item.pengajuan?.nonstock?.name || '', + Lokasi: item.kandang?.location?.name || '', + Kandang: item.kandang?.name || '', + 'Qty Pengajuan': item.pengajuan?.qty || 0, + 'Harga Pengajuan': item.pengajuan?.price || 0, + 'Total Pengajuan': + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + 'Qty Realisasi': item.realisasi?.qty || 0, + 'Harga Realisasi': item.realisasi?.price || 0, + 'Total Realisasi': + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + 'Status Pencairan': item.latest_approval?.step_name || '', + })); + + excelData.push({ + No: 'Total' as unknown as number, + 'No. PO': '', + 'No. Referensi': '', + 'Tanggal Realisasi': '', + 'Tanggal Transaksi': '', + Kategori: '', + Produk: '', + Lokasi: '', + Kandang: '', + 'Qty Pengajuan': totals.qty_pengajuan, + 'Harga Pengajuan': 0, + 'Total Pengajuan': totals.total_pengajuan, + 'Qty Realisasi': totals.qty_realisasi, + 'Harga Realisasi': 0, + 'Total Realisasi': totals.total_realisasi, + 'Status Pencairan': '', + }); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + const colWidths = [ + { wch: 5 }, + { wch: 20 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 30 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 20 }, + { wch: 15 }, + { wch: 20 }, + ]; + worksheet['!cols'] = colWidths; + + const sheetName = supplierName.slice(0, 31); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/expense/filter/ReportExpenseFilter.ts b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts new file mode 100644 index 00000000..b8bd3c56 --- /dev/null +++ b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts @@ -0,0 +1,73 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type ReportExpenseFilterProps = { + location_id: string | null; + supplier_id: string | null; + kandang_id: string | null; + nonstock_id: string | null; + realization_date: string | null; + category: string | null; +}; + +export type ReportExpenseFilterFormType = { + location_id: OptionType | null; + supplier_id: OptionType | null; + kandang_id: OptionType | null; + nonstock_id: OptionType | null; + realization_date: string | null; + category: OptionType | null; +}; + +export const ReportExpenseFilterSchema = yup.object({ + location_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + supplier_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Supplier wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + kandang_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + nonstock_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Produk wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + realization_date: yup.string().nullable(), + category: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kategori wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), +}) as yup.ObjectSchema; + +export type ReportExpenseFilterValues = yup.InferType< + typeof ReportExpenseFilterSchema +>; diff --git a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx b/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx deleted file mode 100644 index 65505a5f..00000000 --- a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { StyleSheet } from '@react-pdf/renderer'; - -const pdfStyles = StyleSheet.create({ - page: { - fontSize: 18, - fontFamily: 'Helvetica', - padding: 20, - backgroundColor: '#FFFFFF', - }, - header: { - marginBottom: 20, - }, - logo: { - width: 120, - height: 30, - marginBottom: 8, - }, - companyInfo: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 4, - color: '#1f74bf', - }, - address: { - fontSize: 7, - color: '#666666', - maxWidth: 400, - marginBottom: 10, - }, - divider: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - marginBottom: 15, - }, - titleSection: { - flexDirection: 'row', - marginBottom: 20, - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - title: { - fontSize: 18, - fontWeight: 'bold', - flex: 3, - color: '#1f74bf', - }, - poInfo: { - flex: 1, - fontSize: 7, - textAlign: 'right', - }, - sectionTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellHeaderLast: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellNarrow: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'center', - }, - tableCellNarrowHeader: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'center', - }, - tableCellWrap: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - flexWrap: 'wrap', - }, - tableCellWrapHeader: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - // Nested header styles - tableHeaderGroup: { - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupLast: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupTitle: { - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'center', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - tableSubHeaderRow: { - flexDirection: 'row', - }, - // Specific width columns - tableCellXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellXSmallHeader: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellSmallHeader: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellMediumHeader: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRightXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - grandTotalRow: { - flexDirection: 'row', - borderTopWidth: 1, - borderTopColor: '#000000', - borderTopStyle: 'solid', - }, - grandTotalLabel: { - flex: 3, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - }, - grandTotalValue: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 0, - }, - allocationSection: { - marginBottom: 8, - }, - allocationTable: { - borderWidth: 1, - borderColor: '#000000', - }, - innerTable: { - marginTop: 5, - borderWidth: 1, - borderColor: '#000000', - }, - innerRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - innerCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - innerCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - innerCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - innerCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - footer: { - marginTop: 30, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - footerCompany: { - fontSize: 18, - fontWeight: 'bold', - textAlign: 'right', - flex: 1, - color: '#1f74bf', - }, - specialInstructionTable: { - width: '60%', - maxWidth: 300, - borderWidth: 1, - borderColor: '#000000', - flex: 1, - }, -}); - -export default pdfStyles; diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx new file mode 100644 index 00000000..3e13c539 --- /dev/null +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ColumnDef } from '@tanstack/react-table'; + +type ReportExpenseColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode; + }>; + }; + +const ReportExpenseSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ReportExpenseColumn[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    + +
    + +
    + + ); +}; + +export default ReportExpenseSkeleton; diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx new file mode 100644 index 00000000..2581ec5c --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -0,0 +1,755 @@ +'use client'; + +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { useFormik } from 'formik'; +import { + ReportExpenseFilterSchema, + type ReportExpenseFilterValues, +} from '@/components/pages/report/expense/filter/ReportExpenseFilter'; +import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; +import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; +import Table from '@/components/Table'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ReportExpenseApi } from '@/services/api/report'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import Pagination from '@/components/Pagination'; +import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; +import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF'; +import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX'; +import toast from 'react-hot-toast'; +import { + KandangApi, + LocationApi, + NonstockApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { ColumnDef } from '@tanstack/react-table'; +import { httpClient } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; + +interface ReportExpenseTabProps { + tabId: string; +} + +interface FilterParams { + location_id?: string; + supplier_id?: string; + kandang_id?: string; + nonstock_id?: string; + realization_date?: string; + category?: string; + search?: string; +} + +const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSuppliers, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstocks, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name', 'search'); + + const categoryOptions = useMemo( + () => [ + { value: 'BOP', label: 'BOP' }, + { value: 'NON-BOP', label: 'Non BOP' }, + ], + [] + ); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + location_id: null, + supplier_id: null, + kandang_id: null, + nonstock_id: null, + realization_date: null, + category: null, + }, + validationSchema: ReportExpenseFilterSchema, + onSubmit: (values) => { + setFilterParams({ + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + supplier_id: values.supplier_id?.value + ? String(values.supplier_id.value) + : undefined, + kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + nonstock_id: values.nonstock_id?.value + ? String(values.nonstock_id.value) + : undefined, + realization_date: values.realization_date || undefined, + category: values.category?.value + ? String(values.category.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + + // ===== FILTER VALUES ===== + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + const supplierValue = useMemo( + () => formik.values.supplier_id, + [formik.values.supplier_id] + ); + const kandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + const nonstockValue = useMemo( + () => formik.values.nonstock_id, + [formik.values.nonstock_id] + ); + const categoryValue = useMemo( + () => formik.values.category, + [formik.values.category] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + if (filterParams.location_id) count += 1; + if (filterParams.supplier_id) count += 1; + if (filterParams.kandang_id) count += 1; + if (filterParams.nonstock_id) count += 1; + if (filterParams.realization_date) count += 1; + if (filterParams.category) count += 1; + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: reportExpenseResponse, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) + params.append('category', filterParams.category); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`${ReportExpenseApi.basePath}?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const data: ReportExpense[] = useMemo( + () => + isResponseSuccess(reportExpenseResponse) + ? (reportExpenseResponse.data as ReportExpense[]) || [] + : [], + [reportExpenseResponse] + ); + + const meta = useMemo( + () => + isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta + ? reportExpenseResponse.meta + : null, + [reportExpenseResponse] + ); + + // ===== EXPORT DATA FETCHER ===== + const reportExpenseExport = useCallback(async (): Promise< + ReportExpense[] | null + > => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) params.append('category', filterParams.category); + params.append('limit', '100'); + params.append('page', '1'); + + const response = await httpClient>( + `${ReportExpenseApi.basePath}?${params.toString()}` + ); + + return isResponseSuccess(response) ? response.data : null; + }, [filterParams]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await reportExpenseExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateReportExpenseExcel(allDataForExport); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [reportExpenseExport]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allData = await reportExpenseExport(); + if (!allData || allData.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const pdfParams = { + location_name: locationValue?.label, + supplier_name: supplierValue?.label, + realization_date: formik.values.realization_date || undefined, + }; + + await generateReportExpensePDF(allData, pdfParams); + + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + reportExpenseExport, + locationValue, + supplierValue, + kandangValue, + nonstockValue, + categoryValue, + formik.values.realization_date, + ]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
    + + + +
    + + + Export + +
    + + +
    + + } + > + + + +
    + ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + handleExportExcel, + handleExportPDF, + setTabActions, + ]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + // ===== TABLE COLUMNS DEFINITION ===== + const columns = useMemo((): ColumnDef[] => { + return [ + { + header: 'No', + cell: (props) => (page - 1) * pageSize + props.row.index + 1, + }, + { + header: 'No. PO', + accessorKey: 'po_number', + }, + { + header: 'No. Referensi', + accessorKey: 'reference_number', + }, + { + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: ({ row }) => { + return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Tanggal Transaksi', + accessorKey: 'transaction_date', + cell: ({ row }) => { + return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Kategori', + accessorKey: 'category', + }, + { + header: 'Produk', + accessorFn: (row) => row.pengajuan?.nonstock?.name, + }, + { + header: 'Supplier', + accessorFn: (row) => row.supplier?.name, + }, + { + header: 'Lokasi', + accessorFn: (row) => row.kandang?.location?.name, + }, + { + header: 'Kandang', + accessorFn: (row) => row.kandang?.name, + }, + { + header: 'Pengajuan', + columns: [ + { + header: 'Qty', + id: 'qty_pengajuan', + accessorFn: (row) => row.pengajuan?.qty, + cell: ({ row }) => + row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_pengajuan', + accessorFn: (row) => row.pengajuan?.price, + cell: ({ row }) => + formatCurrency(row.original.pengajuan?.price || 0), + }, + { + header: 'Total', + id: 'total_pengajuan', + accessorFn: (row) => + (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), + cell: ({ row }) => { + const total = + (row.original.pengajuan?.qty || 0) * + (row.original.pengajuan?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Realisasi', + columns: [ + { + header: 'Qty', + id: 'qty_realisasi', + accessorFn: (row) => row.realisasi?.qty, + cell: ({ row }) => + row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_realisasi', + accessorFn: (row) => row.realisasi?.price, + cell: ({ row }) => + formatCurrency(row.original.realisasi?.price || 0), + }, + { + header: 'Total', + id: 'total_realisasi', + accessorFn: (row) => + (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), + cell: ({ row }) => { + const total = + (row.original.realisasi?.qty || 0) * + (row.original.realisasi?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Status Pencairan', + cell: (props) => ( + + ), + }, + { + header: 'Status BOP', + cell: (props) => ( + + ), + }, + ]; + }, [page, pageSize]); + + return ( + <> +
    + {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
    + +
    + ) : !data || data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> +
    + {meta && ( +
    + + setPage((currPage) => + currPage > 1 ? currPage - 1 : currPage + ) + } + onNextPage={() => + setPage((currPage) => + meta && meta.total_pages && currPage < meta.total_pages + ? currPage + 1 + : currPage + ) + } + onPageChange={(pageNumber) => setPage(pageNumber)} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> +
    + )} + + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    + +
    + {/* Modal Body */} +
    + { + formik.setFieldValue('location_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setKandangInputValue} + onMenuScrollToBottom={loadMoreKandangs} + isClearable + isDisabled={!formik.values.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('supplier_id', val); + }} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('nonstock_id', val); + }} + onInputChange={setNonstockInputValue} + onMenuScrollToBottom={loadMoreNonstocks} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('category', val); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue( + 'realization_date', + e.target.value || null + ); + }} + className={{ wrapper: 'w-full' }} + /> +
    + + {/* Modal Footer */} +
    + + +
    + +
    + + ); +}; + +export default ReportExpenseTab; diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index ffb0d3f1..de924f62 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useFinanceTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index d132be9a..1bcab6bc 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -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[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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 ? ( - - {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + 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 }) => ( + + {formatCurrency(row.accounts_receivable)} + + ), + 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 ? ( + + + {formatTitleCase(row.status)} + ) : ( '-' ), - }, - { - 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 */} - + Laporan > Kontrol Pembayaran Customer - + - - - 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') - : '-'} - - + + 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') + : '-'} + {/* TODO: Uncomment when BE is ready */} - {/* - Filter Tanggal: Tanggal DO - */} - - - Customer: {params.params?.customer_name || 'Semua Customer'} - - - - - Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} - - + {/* + Filter Tanggal: Tanggal DO + */} + + Customer: {params.params?.customer_name || 'Semua Customer'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - + {customerReport.customer.name} - - + + Alamat: {customerReport.customer.address || '-'} - + {/* Table */} { valueKey: 'accounts_receivable', value: customerReport.initial_balance, align: 'right', - color: - customerReport.initial_balance < 0 ? '#DC2626' : 'black', + color: customerReport.initial_balance < 0 ? 'red' : 'black', } : undefined } diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 3238d46e..e8bfda5e 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -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 }, diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index edcd360f..1b5fc933 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -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: ({ 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 !== '-' ? ( + + + {(row as unknown as DebtRow).due_status} + + + ) : ( + '-' + ), + footer: '', + }, + { + key: 'total_price', + header: 'Nominal Pembelian (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).total_price)} + + ), + footer: total ? formatCurrency(total.total_price || 0) : '', + footerAlign: 'right', + }, + { + key: 'payment_price', + header: 'Pembayaran (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).payment_price)} + + ), + footer: total ? formatCurrency(total.payment_price || 0) : '', + footerAlign: 'right', + }, + { + key: 'balance', + header: 'Sisa Saldo Hutang (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).balance)} + + ), + 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 !== '-' ? ( + + + {(row as unknown as DebtRow).status} + + + ) : ( + '-' + ), + 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) => ( {/* Title and Supplier Info */} - + Laporan > Rekapitulasi Hutang ke Supplier - + - - - 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') - : '-'} - - + + 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') + : '-'} + {params.params?.filter_by && ( - - - 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} - - + + 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} + )} - - - Supplier: {params.params?.supplier_name || 'Semua Supplier'} - - - - - Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} - - + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - + {supplierReport.supplier.name} - - + + {supplierReport.supplier.category} - + {/* Table */} - - {/* Table Header */} - - - No - - - No. PR - - - No. PO - - - Tgl Terima/Bayar - - - Tgl PO - - - Aging - - - Area - - - Gudang - - - Jatuh Tempo - - - Status Jatuh Tempo - - - Nominal Pembelian (Rp) - - - Pembayaran (Rp) - - - Sisa Saldo Hutang (Rp) - - - Status - - - No. Perjalanan - - - - {/* Initial Balance Row */} - - - {/* NO */} - - - {/* No. PR */} - - - {/* No. PO */} - - - {/* Tgl Terima/Bayar */} - - - {/* Tgl PO */} - - - {/* Aging */} - - - {/* Area */} - - - {/* Gudang */} - - - {/* Jatuh Tempo */} - - - {/* Status Jatuh Tempo */} - - - {/* Nominal Pembelian (Rp) */} - - - {/* Pembayaran (Rp) */} - - []} + 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', - }, - ]} - > - - {' '} - {/* Sisa Saldo Hutang (Rp) */} - {formatCurrency(supplierReport.initial_balance || 0)} - - - - {/* Status */} - - - - - - - {/* Table Body */} - {supplierReport.rows.map((item, index) => ( - - - {index + 1} - - - {item.pr_number || '-'} - - - {item.po_number || '-'} - - - - {item.received_date - ? item.received_date != '-' - ? formatDate(item.received_date, 'DD MMM YY') - : '-' - : '-'} - - - - - {item.po_date - ? item.po_date != '-' - ? formatDate(item.po_date, 'DD MMM YY') - : '-' - : '-'} - - - - {formatNumber(item.aging)} Hari - - - {item.area?.name || '-'} - - - {item.warehouse?.name || '-'} - - - - {item.due_date - ? item.due_date != '-' - ? formatDate(item.due_date, 'DD MMM YY') - : '-' - : '-'} - - - - {item.due_status && item.due_status !== '-' ? ( - - - {item.due_status} - - - ) : ( - - - )} - - - {formatCurrency(item.total_price)} - - - {formatCurrency(item.payment_price)} - - - {formatCurrency(item.balance)} - - - {item.status && item.status !== '-' ? ( - - - {item.status} - - - ) : ( - - - )} - - - {item.travel_number || '-'} - - - ))} - - {/* Summary Row */} - {supplierReport.total && ( - - - Total - - - - - - - - - - - - - - - {formatNumber(supplierReport.total.aging)} Hari - - - - - - - - - - - - - - - - {formatCurrency(supplierReport.total.total_price)} - - - - - {formatCurrency(supplierReport.total.payment_price)} - - - - {formatCurrency(supplierReport.total.debt_price)} - - - - - - - - - )} - + } + : undefined + } + /> ))} diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx index e5de3ae2..b19ee86e 100644 --- a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -2,7 +2,7 @@ import ExcelJS from 'exceljs'; import { formatDate } from '@/lib/helper'; -import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; interface DebtSupplierExportExcelParams { data: DebtSupplier[]; diff --git a/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts new file mode 100644 index 00000000..60359038 --- /dev/null +++ b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts @@ -0,0 +1,31 @@ +import * as yup from 'yup'; + +export type CustomerPaymentFilterType = { + start_date: string | null; + end_date: string | null; + customer_ids: string | null; + filter_by: string | null; +}; + +export const CustomerPaymentFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + customer_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), +}); + +export type CustomerPaymentFilterValues = yup.InferType< + typeof CustomerPaymentFilterSchema +>; diff --git a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx index b9397f8f..385877fc 100644 --- a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx +++ b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx @@ -1,7 +1,6 @@ import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; import Table from '@/components/Table'; import { DebtRow } from '@/types/api/report/debt-supplier'; -import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; const DebtSupplierSkeleton = ({ diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4e0e3f25..1c546058 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -2,17 +2,22 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { useSelect } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; -// import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; +import { + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, + cn, +} from '@/lib/helper'; import { CustomerPaymentReport, CustomerPaymentSummary, @@ -20,20 +25,31 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; -import Modal from '@/components/Modal'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; +import { + CustomerPaymentFilterSchema, + CustomerPaymentFilterType, +} from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; +import { OptionType } from '@/components/table/TableRowSizeSelector'; +import { Color } from '@/types/theme'; interface CustomerPaymentTabProps { tabId: string; } +interface FilterParams { + customer_ids?: string; + start_date?: string; + end_date?: string; + filter_by?: string; +} + const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -46,31 +62,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== FILTER STATE ===== - const [appliedFilterCustomer, setAppliedFilterCustomer] = useState< - typeof customerOptions - >([]); - // TODO: Uncomment when BE is ready - // const [appliedFilterSales, setAppliedFilterSales] = useState< - // typeof salesOptions - // >([]); - const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); - const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [filterParams, setFilterParams] = useState({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const [filterCustomer, setFilterCustomer] = useState( - [] - ); - // TODO: Uncomment when BE is ready - // const [filterSales, setFilterSales] = useState([]); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - const filterModal = useModal(); const dataTypeOptions = useMemo( @@ -81,10 +76,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { [] ); - const [filterByType, setFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const { options: customerOptions, setInputValue: setCustomerInputValue, @@ -92,108 +83,67 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - // TODO: Uncomment when BE is ready - // const { - // options: salesOptions, - // setInputValue: setSalesInputValue, - // isLoadingOptions: isLoadingSales, - // loadMore: loadMoreSales, - // hasMore: hasMoreSales, - // } = useSelect(UserApi.basePath, 'id', 'name', 'search'); - - const getPaymentStatusColor = (notes: string) => { - const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'bg-info/10 text-black border-info'; - } - - if (normalizedValue.includes('belum')) { - return 'bg-warning/10 text-black border-warning'; - } - - return 'bg-gray-100 text-black border-gray-300'; - }; - - const getPaymentStatusIndicatorColor = (notes: string) => { - const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'bg-info'; - } - - if (normalizedValue.includes('belum')) { - return 'bg-warning'; - } - - return 'bg-gray-400'; - }; - - const getPaymentStatusText = (notes: string) => { - return notes - .toLowerCase() - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - - // ===== FILTER HANDLERS ===== - const handleFilterModalOpen = useCallback(() => { - setFilterCustomer(appliedFilterCustomer); - // setFilterSales(appliedFilterSales); - setFilterByType(appliedFilterByType); - setFilterStartDate(appliedFilterStartDate); - setFilterEndDate(appliedFilterEndDate); + const handleFilterModalOpen = () => { filterModal.openModal(); - }, [ - filterModal, - appliedFilterCustomer, - appliedFilterByType, - appliedFilterStartDate, - appliedFilterEndDate, - ]); + formik.validateForm(); + }; - const handleResetFilters = useCallback(() => { - setIsSubmitted(false); - setFilterCustomer([]); - setFilterByType(null); - setFilterStartDate(''); - setFilterEndDate(''); - setAppliedFilterCustomer([]); - setAppliedFilterByType(null); - setAppliedFilterStartDate(''); - setAppliedFilterEndDate(''); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + customer_ids: null, + filter_by: null, + }, + validationSchema: CustomerPaymentFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + customer_ids: values.customer_ids || undefined, + filter_by: values.filter_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); + + const getPaymentStatusBadgeColor = (notes: string): Color => { + const normalizedValue = notes.toLowerCase(); + + if (normalizedValue === 'lunas') { + return 'primary'; } - }, [dateErrorShown]); - const handleApplyFilters = useCallback(() => { - setAppliedFilterCustomer(filterCustomer); - setAppliedFilterByType(filterByType); - setAppliedFilterStartDate(filterStartDate); - setAppliedFilterEndDate(filterEndDate); - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - }, [ - filterModal, - filterCustomer, - filterByType, - filterStartDate, - filterEndDate, - ]); + if (normalizedValue.includes('belum')) { + return 'warning'; + } + return 'neutral'; + }; + + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterStartDate(value); + formik.setFieldValue('start_date', value || null); - if (value && filterEndDate) { + if (value && formik.values.end_date) { const startDate = new Date(value); - const endDateObj = new Date(filterEndDate); + const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); @@ -214,16 +164,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [filterEndDate, dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterEndDate(value); + formik.setFieldValue('end_date', value || null); - if (value && filterStartDate) { - const startDateObj = new Date(filterStartDate); + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { @@ -244,41 +194,46 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [filterStartDate, dateErrorShown] + [formik, dateErrorShown] ); + // ===== FILTER HELPERS ===== + const customerIdsValue = useMemo(() => { + if (!formik.values.customer_ids) return []; + return customerOptions.filter((opt) => + formik.values.customer_ids?.split(',').includes(String(opt.value)) + ); + }, [formik.values.customer_ids, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; // Date filter (start_date + end_date = 1 filter) - if (appliedFilterStartDate || appliedFilterEndDate) { + if (filterParams.start_date || filterParams.end_date) { count += 1; } // Customer filter - if (appliedFilterCustomer.length > 0) { + if (filterParams.customer_ids) { count += 1; } // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) - if (appliedFilterByType) { + if (filterParams.filter_by) { count += 1; } - // TODO: Uncomment when BE is ready - // // Sales filter - // if (appliedFilterSales.length > 0) { - // count += 1; - // } - return count; - }, [ - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterCustomer, - appliedFilterByType, - ]); + }, [filterParams]); const hasFilters = activeFiltersCount > 0; @@ -287,21 +242,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { isSubmitted ? () => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, page: currentPage, limit: pageSize, }; @@ -333,21 +280,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { CustomerPaymentReport[] | null > => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, limit: 100, page: 1, }; @@ -364,13 +303,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [ - appliedFilterCustomer, - // appliedFilterSales, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, - ]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -410,21 +343,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return; } + const customerName = filterParams.customer_ids + ? customerOptions + .filter((opt) => + filterParams.customer_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Customer' + : 'Semua Customer'; + await generateCustomerPaymentPDF({ data: allDataForExport, params: { - customer_name: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((c) => c.label).join(', ') - : undefined, - // TODO: Uncomment when BE is ready - // sales: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((s) => s.label).join(', ') - // : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - filter_by: appliedFilterByType?.value as + customer_name: customerName, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, @@ -436,11 +370,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [customerPaymentExport]); + }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( @@ -451,13 +385,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -467,42 +401,55 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - - Export -
    - +
    + + + Export + +
    + +
    } - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
    ); @@ -517,7 +464,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setTabActions, ]); - // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); @@ -735,17 +681,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } return ( - - {getPaymentStatusText(value)} - + ); }, }, @@ -931,95 +870,86 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
    -
    -
    - -
    - -
    +
    +
    +
    + +
    + +
    - + +
    + + { + formik.setFieldValue( + 'customer_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v: OptionType) => String(v.value)).join(',') + : null + ); + }} + onInputChange={setCustomerInputValue} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
    - { - setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> - - {/* TODO: Uncomment when BE is ready */} - {/*
    - { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
    */} - - { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> - - {/* Action Buttons */} -
    -
    - - -
    + {/* Modal Footer */} +
    + + +
    + ); diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 5e7781bf..635bece8 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -3,8 +3,6 @@ import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -33,7 +31,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; @@ -131,7 +129,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); }, - onReset: (values) => { + onReset: () => { setFilterParams({ start_date: undefined, end_date: undefined, @@ -172,10 +170,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { : [], [debtSupplier] ); - const meta = - isResponseSuccess(debtSupplier) && debtSupplier?.meta - ? debtSupplier.meta - : null; // ===== EXPORT DATA FETCHER ===== const debtSupplierExport = useCallback(async (): Promise< @@ -271,13 +265,13 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, [debtSupplierExport]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( tabId, -
    +
    { /> - - Export -
    - +
    + + + Export + +
    + +
    } - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
    ); diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 1e2d2824..1e3f4109 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -1,14 +1,19 @@ 'use client'; +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const LogisticStockTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + const tabs = [ { id: '1', label: 'Rekapitulasi Pembelian Per Supplier', - content: , + content: , }, // { // id: '2', @@ -23,8 +28,20 @@ const LogisticStockTabs = () => { ]; return ( -
    - +
    +
    ); }; diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx deleted file mode 100644 index bd6f301a..00000000 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx +++ /dev/null @@ -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: ( - - {item.expedition || '-'} - - ), - align: 'center', - }, - { key: 'delivery_number', value: item.delivery_number || '-' }, - ]); -}; - -const createPDFDocument = ( - supplierReports: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] -) => ( - - - {/* Title and Parameters */} - - - Laporan > Rekapitulasi Pembelian Per Supplier - - - - - Jenis Tanggal:{' '} - {params.filter_by === 'received_date' - ? 'Tanggal Terima' - : 'Tanggal PO'} - - - {getParameterText(params).map((param, index) => ( - - {param} - - ))} - - - - {/* Supplier Sections */} - {supplierReports.map( - ( - supplierReport: LogisticPurchasePerSupplierReport, - supplierIndex: number - ) => { - return ( - - - {supplierReport.supplier.name} - - - - - ); - } - )} - - -); - -export const generatePurchasesPerSupplierPDF = async ( - data: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] -): Promise => { - 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; - } -}; diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx new file mode 100644 index 00000000..83f34a28 --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -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[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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 ? ( + + + {row.expedition} + + + ) : ( + '-' + ), + footer: '', + }, + { + key: 'delivery_number', + header: 'Surat Jalan', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.delivery_number || '-', + footer: '', + }, +]; + +const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { + return ( + + {params.data.map((supplierReport, supplierIndex) => ( + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + + Jenis Tanggal:{' '} + {params.params?.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + 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') + : '-'} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + Area: {params.params?.area_name || 'Semua Area'} + + + Produk: {params.params?.product_name || 'Semua Produk'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + {supplierReport.supplier.name} + + {supplierReport.supplier.address && ( + + Alamat: {supplierReport.supplier.address} + + )} + + + {/* Table */} + + + ))} + + ); +}; + +export const generatePurchasesPerSupplierPDF = async ( + params: PurchasesPerSupplierExportParams +): Promise => { + 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; + } +}; diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx new file mode 100644 index 00000000..110bd65e --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx @@ -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 => { + 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); +}; diff --git a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts new file mode 100644 index 00000000..b3d9943b --- /dev/null +++ b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts @@ -0,0 +1,39 @@ +import * as yup from 'yup'; + +export type PurchasesPerSupplierFilterType = { + start_date: string | null; + end_date: string | null; + area_ids: string | null; + supplier_ids: string | null; + product_ids: string | null; + product_category_ids: string | null; + filter_by: string | null; + sort_by: string | null; +}; + +export const PurchasesPerSupplierFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + area_ids: yup.string().nullable(), + supplier_ids: yup.string().nullable(), + product_ids: yup.string().nullable(), + product_category_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); + +export type PurchasesPerSupplierFilterValues = yup.InferType< + typeof PurchasesPerSupplierFilterSchema +>; diff --git a/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx new file mode 100644 index 00000000..a5268b2f --- /dev/null +++ b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { ColumnDef } from '@tanstack/react-table'; + +const PurchasePerSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default PurchasePerSupplierSkeleton; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5366f3cd..87c5ee8d 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1,37 +1,54 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; -import useSWR from 'swr'; +import Button from '@/components/Button'; import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; +import { useSelect } from '@/components/input/SelectInput'; +import Modal, { useModal } from '@/components/Modal'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/report/logistic-stock'; -import Table from '@/components/Table'; -import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; -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 toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; +import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; +import useSWR from 'swr'; +import { useFormik } from 'formik'; +import { + PurchasesPerSupplierFilterSchema, + PurchasesPerSupplierFilterType, +} from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; -const PurchasesPerSupplierTab = () => { +interface PurchasesPerSupplierTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + supplier_id?: string; + product_id?: string; + product_category_id?: string; + start_date?: string; + end_date?: string; + sort_by?: string; + filter_by?: string; +} + +const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -39,31 +56,17 @@ const PurchasesPerSupplierTab = () => { // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const [pageSize] = useState(10); // ===== SUBMISSION STATE ===== + const [filterParams, setFilterParams] = useState({}); const [isSubmitted, setIsSubmitted] = useState(false); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - supplier_id: [] as string[], - product_id: [] as string[], - product_category_id: [] as string[], - received_date: '', - po_date: '', - start_date: '', - end_date: '', - sort_by: '', - filter_by: 'received_date', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); + const filterModal = useModal(); + // ===== OPTIONS ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -100,151 +103,206 @@ const PurchasesPerSupplierTab = () => { [] ); - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + area_ids: null, + supplier_ids: null, + product_ids: null, + product_category_ids: null, + filter_by: null, + sort_by: null, }, - [updateFilter] + validationSchema: PurchasesPerSupplierFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + area_id: values.area_ids || undefined, + supplier_id: values.supplier_ids || undefined, + product_id: values.product_ids || undefined, + product_category_id: values.product_category_ids || undefined, + filter_by: values.filter_by || undefined, + sort_by: values.sort_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); + + // ===== DATE CHANGE HANDLERS ===== + const handleStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('start_date', value || null); + + if (value && formik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.end_date); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }, + [formik, dateErrorShown] ); - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'supplier_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + const handleEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('end_date', value || null); + + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }, - [updateFilter] + [formik, dateErrorShown] ); - const productChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_ids) return []; + const ids = formik.values.area_ids.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_ids, areaOptions]); - const productCategoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_category_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + const supplierIdsValue = useMemo(() => { + if (!formik.values.supplier_ids) return []; + const ids = formik.values.supplier_ids.split(','); + return supplierOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.supplier_ids, supplierOptions]); - const dataTypeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const filterValue = - (newVal?.value as 'received_date' | 'po_date') || 'received_date'; - updateFilter('filter_by', filterValue); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); + const productIdsValue = useMemo(() => { + if (!formik.values.product_ids) return []; + const ids = formik.values.product_ids.split(','); + return productOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.product_ids, productOptions]); - const sortByHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; - updateFilter('sort_by', sortValue); - setIsSubmitted(false); - }, - [updateFilter] - ); + const productCategoryIdsValue = useMemo(() => { + if (!formik.values.product_category_ids) return []; + const ids = formik.values.product_category_ids.split(','); + return productCategoryOptions.filter((opt) => + ids.includes(String(opt.value)) + ); + }, [formik.values.product_category_ids, productCategoryOptions]); - const startDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('start_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by, dataTypeOptions]); - const endDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('end_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); + const sortByValue = useMemo(() => { + if (!formik.values.sort_by) return null; + return ( + sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null + ); + }, [formik.values.sort_by, sortByOptions]); - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('supplier_id', []); - updateFilter('product_id', []); - updateFilter('product_category_id', []); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - updateFilter('start_date', ''); - updateFilter('end_date', ''); - updateFilter('sort_by', ''); - updateFilter('filter_by', 'received_date'); - setIsSubmitted(false); - }, [updateFilter]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; - const handleSubmit = useCallback(() => { - setIsSubmitted(true); - setCurrentPage(1); - }, []); + if (filterParams.start_date || filterParams.end_date) { + count += 1; + } + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.supplier_id) { + count += 1; + } + + if (filterParams.product_id) { + count += 1; + } + + if (filterParams.product_category_id) { + count += 1; + } + + if (filterParams.filter_by) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( isSubmitted ? () => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') - : undefined, - product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') - : undefined, - product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') - : undefined, - received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined - : undefined, - po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined - : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, page: currentPage, limit: pageSize, }; @@ -258,8 +316,8 @@ const PurchasesPerSupplierTab = () => { params.supplier_id, params.product_id, params.product_category_id, - params.received_date, - params.po_date, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -278,44 +336,19 @@ const PurchasesPerSupplierTab = () => { [purchasePerSupplier] ); - const meta = - isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta - ? purchasePerSupplier.meta - : null; - // ===== EXPORT DATA FETCHER ===== const logisticPurchasePerSupplierExport = useCallback(async (): Promise< LogisticPurchasePerSupplierReport[] | null > => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') - : undefined, - product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') - : undefined, - product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') - : undefined, - received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined - : undefined, - po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined - : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, limit: 100, page: 1, }; @@ -325,8 +358,8 @@ const PurchasesPerSupplierTab = () => { params.supplier_id, params.product_id, params.product_category_id, - params.received_date, - params.po_date, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -338,7 +371,7 @@ const PurchasesPerSupplierTab = () => { return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; - }, [tableFilterState]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -355,98 +388,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); @@ -462,62 +411,58 @@ const PurchasesPerSupplierTab = () => { return; } - const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' - : 'Semua Area'; + const areaName = filterParams.area_id + ? areaOptions + .filter((opt) => + filterParams.area_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const supplierName = - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id - .map( - (id) => - supplierOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Supplier' - : 'Semua Supplier'; + const supplierName = filterParams.supplier_id + ? supplierOptions + .filter((opt) => + filterParams.supplier_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; - const productName = - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id - .map( - (id) => - productOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Produk' - : 'Semua Produk'; + const productName = filterParams.product_id + ? productOptions + .filter((opt) => + filterParams.product_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Produk' + : 'Semua Produk'; - const productCategoryName = - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id - .map( - (id) => - productCategoryOptions.find((opt) => opt.value === Number(id)) - ?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Kategori Produk' - : 'Semua Kategori Produk'; + const productCategoryName = filterParams.product_category_id + ? productCategoryOptions + .filter((opt) => + filterParams.product_category_id + ?.split(',') + .includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kategori Produk' + : 'Semua Kategori Produk'; const exportParams = { area_name: areaName, supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, - filter_by: tableFilterState.filter_by || 'received_date', - start_date: tableFilterState.start_date || '', - end_date: tableFilterState.end_date || '', + filter_by: filterParams.filter_by, + start_date: filterParams.start_date, + end_date: filterParams.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.'); @@ -526,33 +471,108 @@ const PurchasesPerSupplierTab = () => { } }, [ logisticPurchasePerSupplierExport, - tableFilterState, + filterParams, areaOptions, supplierOptions, productOptions, productCategoryOptions, ]); - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setCurrentPage(page); - }; + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; + useEffect(() => { + setTabActions( + tabId, +
    + - const handleNextPage = () => { - if (meta && currentPage < meta.total_pages) { - setCurrentPage(currentPage + 1); - } - }; + +
    + - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; + Export + +
    + + +
    + + } + > + + + +
    + ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); const getTableColumns = ( summary: LogisticPurchasePerSupplierSummary @@ -566,11 +586,11 @@ const PurchasesPerSupplierTab = () => { cell: (props) => props.row.index + 1, footer: () =>
    Total
    , }, - { id: 'received_date', header: 'Tanggal Terima', accessorKey: 'receive_date', + enableSorting: false, cell: (props) => { const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); @@ -580,6 +600,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', + enableSorting: false, cell: (props) => { const value = props.row.original.po_date; return formatDate(value, 'DD MMM YYYY'); @@ -589,6 +610,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', + enableSorting: false, cell: (props) => { const value = props.row.original.po_number; return value || '-'; @@ -598,6 +620,7 @@ const PurchasesPerSupplierTab = () => { id: 'product_name', header: 'Nama Produk', accessorKey: 'product.name', + enableSorting: false, cell: (props) => { const product = props.row.original.product; return product?.name || '-'; @@ -607,6 +630,7 @@ const PurchasesPerSupplierTab = () => { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'warehouse.name', + enableSorting: false, cell: (props) => { const warehouse = props.row.original.warehouse; return warehouse?.name || '-'; @@ -616,6 +640,7 @@ const PurchasesPerSupplierTab = () => { id: 'qty', header: 'QTY', accessorKey: 'qty', + enableSorting: false, cell: (props) => { const value = props.row.original.qty; return
    {formatNumber(value)}
    ; @@ -630,6 +655,7 @@ const PurchasesPerSupplierTab = () => { id: 'price', header: 'Harga Beli (Rp)', accessorKey: 'unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.unit_price; return
    {formatCurrency(value)}
    ; @@ -644,6 +670,7 @@ const PurchasesPerSupplierTab = () => { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', accessorKey: 'purchase_value', + enableSorting: false, cell: (props) => { const value = props.row.original.purchase_value; return
    {formatCurrency(value)}
    ; @@ -658,6 +685,7 @@ const PurchasesPerSupplierTab = () => { id: 'transport', header: 'Transport (Rp)', accessorKey: 'transport_unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_unit_price; return
    {formatCurrency(value)}
    ; @@ -672,6 +700,7 @@ const PurchasesPerSupplierTab = () => { id: 'value_transport', header: 'Value Transport (Rp)', accessorKey: 'transport_value', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_value; return
    {formatCurrency(value)}
    ; @@ -686,6 +715,7 @@ const PurchasesPerSupplierTab = () => { id: 'total', header: 'Jumlah (Rp)', accessorKey: 'total_amount', + enableSorting: false, cell: (props) => { const value = props.row.original.total_amount; return
    {formatCurrency(value)}
    ; @@ -700,6 +730,7 @@ const PurchasesPerSupplierTab = () => { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition', + enableSorting: false, cell: (props) => { const value = props.row.original.expedition; return value || '-'; @@ -709,6 +740,7 @@ const PurchasesPerSupplierTab = () => { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'delivery_number', + enableSorting: false, cell: (props) => { const value = props.row.original.delivery_number; return value || '-'; @@ -719,156 +751,50 @@ const PurchasesPerSupplierTab = () => { }; return ( -
    - -
    - - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - isLoading={isLoadingAreas} - isClearable - /> - - (tableFilterState.supplier_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={supplierChangeHandler} - isLoading={isLoadingSuppliers} - isClearable - /> - - (tableFilterState.product_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productChangeHandler} - isLoading={isLoadingProducts} - isClearable - /> -
    -
    - - (tableFilterState.product_category_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productCategoryChangeHandler} - isLoading={isLoadingProductCategories} - isClearable - /> -
    - option.value === tableFilterState.filter_by - ) || null - } - onChange={dataTypeChangeHandler} - isLoading={false} - isClearable={false} - /> - option.value === tableFilterState.sort_by - ) || null - } - onChange={sortByHandler} - isLoading={false} - isClearable={false} - /> -
    -
    - - -
    -
    -
    - - - - Export - - - } - align='end' - > - - - - - -
    - + <> +
    {!isSubmitted ? ( -
    - Silakan pilih filter dan klik tombol Submit untuk menampilkan data. -
    + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? ( -
    - -
    + + } + title='Memuat Data Pembelian Per Supplier' + subtitle='Silakan tunggu sebentar...' + /> ) : data.length === 0 ? ( -
    - Tidak ada data yang dapat ditampilkan... -
    + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((supplierReport) => { const summary = supplierReport.summary || { @@ -889,15 +815,17 @@ const PurchasesPerSupplierTab = () => { title={supplierReport.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} className={{ - wrapper: 'w-full rounded-2xl', + wrapper: 'w-full rounded-lg border-none', body: 'p-0', title: - 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', subtitle: - 'px-3 pb-1 bg-primary text-white text-sm font-normal', + 'px-2 pb-1.5 bg-primary text-white text-xs font-normal', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} + defaultCollapsed={true} >
    { renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full mb-0!', - tableWrapperClassName: 'overflow-x-auto', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -927,22 +856,190 @@ const PurchasesPerSupplierTab = () => { ); }) )} - - {meta && data.length > 0 && ( -
    - +
    + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    +
    - )} - +
    +
    + {/* Date Filter */} +
    + +
    + +
    + +
    +
    + + {/* Area Filter */} + { + formik.setFieldValue( + 'area_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Supplier Filter */} + { + formik.setFieldValue( + 'supplier_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Filter */} + { + formik.setFieldValue( + 'product_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingProducts} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Category Filter */} + { + formik.setFieldValue( + 'product_category_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingProductCategories} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Filter By Type */} + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'filter_by', + val?.value?.toString() || null + ); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> + + {/* Sort By */} + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'sort_by', + val?.value?.toString() || null + ); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
    + + {/* Modal Footer */} +
    + + +
    + +
    + ); }; diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx new file mode 100644 index 00000000..8a02a0c2 --- /dev/null +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; +import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; +import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; + +const MarketingReportContent = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Penjualan Harian', + content: , + }, + { + id: '2', + label: 'HPP Harian Kandang', + content: , + }, + ]; + + return ( +
    + +
    + ); +}; + +export default MarketingReportContent; diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx new file mode 100644 index 00000000..c5e1a3a5 --- /dev/null +++ b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx @@ -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[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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 ? ( + + + {formatTitleCase(row.marketing_type)} + + + ) : ( + '-' + ), + }, + { + 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 ( + + + {/* Title and Parameters */} + + + Laporan > Penjualan Harian + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + + {/* Table */} + + + + + + ); +}; + +export default DailyMarketingReportPDF; diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx new file mode 100644 index 00000000..d43213f1 --- /dev/null +++ b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx @@ -0,0 +1,121 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { DailyMarketingRow, SalesSummary } from '@/types/api/report/marketing'; + +interface DailyMarketingExportExcelParams { + data: DailyMarketingRow[]; + summaryTotal?: SalesSummary; + period?: string; +} + +export const generateDailyMarketingExcel = async ( + params: DailyMarketingExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== DAILY MARKETING WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Jual', key: 'soDate', width: 15 }, + { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, + { header: 'Aging', key: 'aging', width: 10 }, + { header: 'Gudang', key: 'warehouse', width: 25 }, + { header: 'Pelanggan', key: 'customer', width: 25 }, + { header: 'No. DO', key: 'doNumber', width: 15 }, + { header: 'Sales/Marketing', key: 'sales', width: 20 }, + { header: 'No. Polisi', key: 'vehicleNumber', width: 15 }, + { header: 'Marketing Type', key: 'marketingType', width: 15 }, + { header: 'Produk', key: 'product', width: 20 }, + { header: 'Kuantitas', key: 'qty', width: 12 }, + { header: 'Bobot Rata-Rata (Kg)', key: 'averageWeight', width: 20 }, + { header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 }, + { header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 }, + { header: 'HPP (Rp)', key: 'hppPrice', width: 15 }, + { header: 'HPP Amount (Rp)', key: 'hppAmount', width: 20 }, + { header: 'Total (Rp)', key: 'salesAmount', width: 20 }, + ]; + + const worksheet = workbook.addWorksheet('Laporan Marketing Harian'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: DailyMarketingRow, index: number) => { + worksheet.addRow({ + no: index + 1, + soDate: formatDate(item.so_date, 'DD-MMM-YYYY'), + realizationDate: formatDate(item.realization_date, 'DD-MMM-YYYY'), + aging: `${item.aging_days} hari`, + warehouse: item.warehouse?.name || '', + customer: item.customer?.name || '', + doNumber: item.do_number || '', + sales: item.sales?.name || '', + vehicleNumber: formatVechicleNumber(item.vehicle_number), + marketingType: item.marketing_type || '', + product: item.product?.name || '', + qty: formatNumber(item.qty || 0), + averageWeight: formatNumber(item.average_weight_kg || 0), + totalWeight: formatNumber(item.total_weight_kg || 0), + salesPrice: formatCurrency(item.sales_price_per_kg || 0), + hppPrice: formatCurrency(item.hpp_price_per_kg || 0), + hppAmount: formatCurrency(item.hpp_amount || 0), + salesAmount: formatCurrency(item.sales_amount || 0), + }); + }); + + // Add TOTAL row if summary data is available + if (params.summaryTotal) { + worksheet.addRow({ + no: 'TOTAL', + soDate: '', + realizationDate: '', + aging: '', + warehouse: '', + customer: '', + doNumber: '', + sales: '', + vehicleNumber: '', + marketingType: '', + product: '', + qty: formatNumber(params.summaryTotal.total_qty || 0), + averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0), + totalWeight: formatNumber(params.summaryTotal.total_weight_kg || 0), + salesPrice: formatCurrency(params.summaryTotal.average_sales_price || 0), + hppPrice: formatCurrency(params.summaryTotal.total_hpp_price_per_kg || 0), + hppAmount: formatCurrency(params.summaryTotal.total_hpp_amount || 0), + salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0), + }); + } + + worksheet.columns.forEach((column) => { + if (column.width && column.width < 10) { + column.width = 10; + } + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-marketing-harian-${params.period}.xlsx` + : `laporan-marketing-harian-${currentDate}.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); +}; diff --git a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx new file mode 100644 index 00000000..bf68a195 --- /dev/null +++ b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx @@ -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[] => [ + { + 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[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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 ( + + + {/* Title and Parameters */} + + + Laporan > HPP Harian Kandang + + + + Area: {params.params?.area_name || 'Semua Area'} + + + Lokasi: {params.params?.location_name || 'Semua Lokasi'} + + + Kandang: {params.params?.kandang_name || 'Semua Kandang'} + + + Periode:{' '} + {params.params?.period + ? formatDate(params.params.period, 'DD MMM YYYY') + : '-'} + + Rentang Bobot: {weightRangeText} + {params.params?.show_unrecorded === 'true' && ( + Tampilkan: Tanpa Recording + )} + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + + {/* Rekapitulasi Section */} + + + Rekapitulasi + + + + + {/* Detail Per Kandang Section */} + + + Detail Per Kandang + + + + + + ); +}; + +export const generateHppPerKandangPDF = async ( + params: HppPerKandangExportParams, + allFeedSuppliers: string, + allDocSuppliers: string +): Promise => { + 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; + } +}; diff --git a/src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx new file mode 100644 index 00000000..20faaa13 --- /dev/null +++ b/src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx @@ -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 => { + 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); +}; diff --git a/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts new file mode 100644 index 00000000..85c765a9 --- /dev/null +++ b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts @@ -0,0 +1,42 @@ +import * as yup from 'yup'; + +export type DailyMarketingReportFilterType = { + search: string | null; + area_id: string | null; + location_id: string | null; + warehouse_id: string | null; + customer_id: string | null; + start_date: string | null; + end_date: string | null; + marketing_type: string | null; + filter_by: string | null; + sort_by: string | null; +}; + +export const DailyMarketingReportFilterSchema = yup.object({ + search: yup.string().nullable(), + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + warehouse_id: yup.string().nullable(), + customer_id: yup.string().nullable(), + start_date: yup.string().nullable(), + end_date: yup + .string() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + marketing_type: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); + +export type DailyMarketingReportFilterValues = yup.InferType< + typeof DailyMarketingReportFilterSchema +>; diff --git a/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts new file mode 100644 index 00000000..57d2dcd2 --- /dev/null +++ b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts @@ -0,0 +1,40 @@ +import * as yup from 'yup'; + +export type HppPerKandangFilterType = { + area_id: string | null; + location_id: string | null; + kandang_id: string | null; + weight_min: string | null; + weight_max: string | null; + period: string | null; + sort_by: string | null; + show_unrecorded: boolean | null; +}; + +export const HppPerKandangFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + weight_min: yup.string().nullable(), + weight_max: yup + .string() + .nullable() + .test( + 'is-greater-than-min', + 'Rentang bobot max tidak boleh lebih kecil dari min', + function (value) { + const { weight_min } = this.parent; + if (!weight_min || !value) return true; + const weightMinNum = parseFloat(weight_min) || 0; + const weightMaxNum = parseFloat(value) || 0; + return weightMaxNum >= weightMinNum; + } + ), + period: yup.string().required('Periode wajib diisi'), + sort_by: yup.string().nullable(), + show_unrecorded: yup.boolean().nullable(), +}); + +export type HppPerKandangFilterValues = yup.InferType< + typeof HppPerKandangFilterSchema +>; diff --git a/src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx b/src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx new file mode 100644 index 00000000..ad68b8f6 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { DailyMarketingRow } from '@/types/api/report/marketing.d'; +import { ColumnDef } from '@tanstack/react-table'; + +const DailyMarketingReportSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default DailyMarketingReportSkeleton; diff --git a/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx new file mode 100644 index 00000000..42a6cf56 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppPerKandangSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default HppPerKandangSkeleton; diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx new file mode 100644 index 00000000..a336b671 --- /dev/null +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -0,0 +1,939 @@ +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; +import { CustomerApi } from '@/services/api/master-data'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, + formatTitleCase, +} from '@/lib/helper'; +import { + DailyMarketingRow, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; +import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX'; +import { pdf } from '@react-pdf/renderer'; +import toast from 'react-hot-toast'; +import { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { + DailyMarketingReportFilterSchema, + DailyMarketingReportFilterType, +} from '@/components/pages/report/marketing/filter/DailyMarketingFilter'; +import SelectInput from '@/components/input/SelectInput'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; +import { useEffect as useEffectHook } from 'react'; +import { httpClient } from '@/services/http/client'; +import { isResponseError } from '@/lib/api-helper'; +import { + MARKETING_DATE_FILTER_TYPE_OPTIONS, + MARKETING_TYPE_OPTIONS, +} from '@/config/constant'; +import Badge from '@/components/Badge'; + +interface DailyMarketingTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + warehouse_id?: string; + customer_id?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + marketing_type?: string; + sort_by?: string; +} + +const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== SEARCH STATE ===== + const [searchValue, setSearchValue] = useState(''); + + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { options: warehouseOptions, isLoadingOptions: isLoadingWarehouses } = + useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + + const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = + useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + search: null, + area_id: null, + location_id: null, + warehouse_id: null, + customer_id: null, + start_date: null, + end_date: null, + filter_by: null, + marketing_type: null, + sort_by: null, + }, + validationSchema: DailyMarketingReportFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + warehouse_id: values.warehouse_id || undefined, + customer_id: values.customer_id || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + filter_by: values.filter_by || undefined, + marketing_type: values.marketing_type || undefined, + sort_by: values.sort_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + }, + }); + + // ===== SEARCH CHANGE HANDLER ===== + const searchChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, + [] + ); + + // ===== DERIVED VALUES ===== + const areaValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const warehouseValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id + ) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + const customerValue = useMemo(() => { + if (!formik.values.customer_id) return null; + return ( + customerOptions.find( + (opt) => String(opt.value) === formik.values.customer_id + ) || null + ); + }, [formik.values.customer_id, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + MARKETING_DATE_FILTER_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.filter_by + ) || null + ); + }, [formik.values.filter_by]); + + const marketingTypeValue = useMemo(() => { + if (!formik.values.marketing_type) return null; + return ( + MARKETING_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.marketing_type + ) || null + ); + }, [formik.values.marketing_type]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.warehouse_id) { + count += 1; + } + + if (filterParams.customer_id) { + count += 1; + } + + if (filterParams.start_date || filterParams.end_date) { + count += 1; + } + + if (filterParams.filter_by) { + count += 1; + } + + if (filterParams.marketing_type) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: dailyMarketings, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) + params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) + params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); + + return ['daily-marketing-report', params.toString()]; + } + : null, + ([, params]) => + MarketingReportApi.getAllDailyMarketingFetcher( + `${MarketingReportApi.basePath}?${params}` + ) + ); + + const data: DailyMarketingRow[] = useMemo( + () => + isResponseSuccess(dailyMarketings) + ? (dailyMarketings?.data as DailyMarketingRow[]) || [] + : [], + [dailyMarketings] + ); + + const summaryTotal = useMemo( + () => + isResponseSuccess(dailyMarketings) && dailyMarketings?.total + ? dailyMarketings.total + : undefined, + [dailyMarketings] + ); + + // ===== EXPORT DATA FETCHER ===== + const dailyMarketingsExport = useCallback(async (): Promise< + DailyMarketingRow[] | null + > => { + const params = new URLSearchParams(); + + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); + params.set('limit', '9999999'); + + const queryString = `?${params.toString()}`; + + try { + const response = await httpClient( + `${MarketingReportApi.basePath}${queryString}` + ); + + if (isResponseError(response)) { + return null; + } + + return response.data || []; + } catch { + return null; + } + }, [filterParams, searchValue]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await dailyMarketingsExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const period = + filterParams.start_date && filterParams.end_date + ? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}` + : undefined; + + await generateDailyMarketingExcel({ + data: allDataForExport, + summaryTotal: summaryTotal, + period: period, + }); + + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [filterParams, dailyMarketingsExport, summaryTotal]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await dailyMarketingsExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const dailyMarketingReportPdfBlob = await pdf( + + ).toBlob(); + + const dailyMarketingReportPdfUrl = URL.createObjectURL( + dailyMarketingReportPdfBlob + ); + window.open(dailyMarketingReportPdfUrl, '_blank'); + + toast.success('PDF berhasil dibuat.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [dailyMarketingsExport, summaryTotal]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffectHook(() => { + setTabActions( + tabId, +
    + + } + className={{ + wrapper: 'w-full min-w-48 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + + + + +
    + + + Export + +
    + + +
    + + } + > + + + +
    + ); + }, [ + tabId, + searchValue, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
    TOTAL
    , + }, + { + id: 'so_date', + header: 'Tanggal Jual', + accessorKey: 'so_date', + cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), + footer: () =>
    ALL
    , + }, + { + id: 'realization_date', + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: (props) => + formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), + footer: () =>
    -
    , + }, + { + id: 'aging_days', + header: 'Aging', + accessorKey: 'aging_days', + cell: (props) => `${props.row.original.aging_days} hari`, + footer: () =>
    -
    , + }, + { + id: 'warehouse', + header: 'Gudang', + accessorKey: 'warehouse', + cell: ({ row }) => row.original.warehouse.name, + footer: () =>
    -
    , + }, + { + id: 'customer', + header: 'Pelanggan', + accessorKey: 'customer', + cell: ({ row }) => row.original.customer.name, + footer: () =>
    -
    , + }, + { + id: 'do_number', + header: 'No. DO', + accessorKey: 'do_number', + footer: () =>
    -
    , + }, + { + id: 'sales_person', + header: 'Sales/Marketing', + accessorKey: 'sales', + cell: (props) => props.row.original.sales.name, + footer: () =>
    -
    , + }, + { + id: 'vehicle_number', + header: 'No. Polisi', + accessorKey: 'vehicle_number', + cell: (props) => ( + + {formatVechicleNumber(props.row.original.vehicle_number)} + + ), + footer: () =>
    -
    , + }, + { + id: 'marketing_type', + header: 'Marketing Type', + accessorKey: 'marketing_type', + cell: (props) => ( + + {formatTitleCase(props.row.original.marketing_type || '-')} + + ), + footer: () =>
    -
    , + }, + { + id: 'product', + header: 'Produk', + accessorKey: 'product', + cell: ({ row }) => row.original.product.name, + footer: () =>
    -
    , + }, + { + id: 'qty', + header: 'Kuantitas', + accessorKey: 'qty', + cell: (props) => formatNumber(props.row.original.qty), + footer: () => ( +
    + {summaryTotal?.total_qty + ? formatNumber(summaryTotal.total_qty) + : '-'} +
    + ), + }, + { + id: 'average_weight', + header: 'Bobot Rata-Rata (Kg)', + accessorKey: 'average_weight_kg', + cell: (props) => formatNumber(props.row.original.average_weight_kg), + footer: () => ( +
    + {summaryTotal?.average_weight_kg + ? formatNumber(summaryTotal.average_weight_kg) + : '-'} +
    + ), + }, + { + id: 'total_weight', + header: 'Bobot Total (Kg)', + accessorKey: 'total_weight_kg', + cell: (props) => formatNumber(props.row.original.total_weight_kg), + footer: () => ( +
    + {summaryTotal?.total_weight_kg + ? formatNumber(summaryTotal.total_weight_kg) + : '-'} +
    + ), + }, + { + id: 'sales_price', + header: 'Harga Jual (Rp)', + accessorKey: 'sales_price_per_kg', + cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), + footer: () => ( +
    + {summaryTotal?.average_sales_price + ? formatNumber(summaryTotal.average_sales_price) + : '-'} +
    + ), + }, + { + id: 'hpp_price', + header: 'HPP (Rp)', + accessorKey: 'hpp_price_per_kg', + cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + footer: () => ( +
    + {summaryTotal?.total_hpp_price_per_kg + ? formatCurrency(summaryTotal.total_hpp_price_per_kg) + : '-'} +
    + ), + }, + { + id: 'sales_amount', + header: 'Total (Rp)', + accessorKey: 'sales_amount', + cell: (props) => formatCurrency(props.row.original.sales_amount), + footer: () => ( +
    + {summaryTotal?.total_sales_amount + ? formatCurrency(summaryTotal.total_sales_amount) + : '-'} +
    + ), + }, + ]; + return tableColumns; + }; + + return ( + <> +
    + {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( + + } + title='Memuat Data Penjualan Harian' + subtitle='Silakan tunggu sebentar...' + /> + ) : data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( +
    0} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    +
    +
    + {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Warehouse Filter */} + { + formik.setFieldValue( + 'warehouse_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Customer Filter */} + { + formik.setFieldValue( + 'customer_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Date Range Filter */} +
    + +
    + { + formik.setFieldValue('start_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={ + !!formik.errors.start_date && formik.touched.start_date + } + /> + {formik.errors.start_date && formik.touched.start_date && ( +
    + {formik.errors.start_date} +
    + )} + { + formik.setFieldValue('end_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={!!formik.errors.end_date && formik.touched.end_date} + /> + {formik.errors.end_date && formik.touched.end_date && ( +
    + {formik.errors.end_date} +
    + )} +
    +
    + + {/* Filter By Date Type */} + { + formik.setFieldValue( + 'filter_by', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Marketing Type Filter */} + { + formik.setFieldValue( + 'marketing_type', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> +
    + + {/* Modal Footer */} +
    + + +
    + +
    + + ); +}; + +export default DailyMarketingTab; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx new file mode 100644 index 00000000..991c6546 --- /dev/null +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -0,0 +1,1061 @@ +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import NumberInput from '@/components/input/NumberInput'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { ProjectFlockKandangApi } from '@/services/api/production'; +import { SaleReportApi } from '@/services/api/report/marketing-sale'; +import Table from '@/components/Table'; +import { ColumnDef, Row, flexRender } from '@tanstack/react-table'; +import { formatCurrency, formatNumber } from '@/lib/helper'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +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 { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { + HppPerKandangFilterSchema, + HppPerKandangFilterType, +} from '@/components/pages/report/marketing/filter/HppPerKandangFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; +import { useEffect as useEffectHook } from 'react'; + +interface HppPerKandangTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + kandang_id?: string; + weight_min?: string; + weight_max?: string; + period?: string; + sort_by?: string; + show_unrecorded?: boolean; +} + +const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== VALIDATION STATE ===== + const [weightMaxError, setWeightMaxError] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); + + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect( + ProjectFlockKandangApi.basePath, + 'id', + 'name_with_period', + 'search' + ); + + const showUnrecordedOptions = useMemo( + () => [ + { value: 'false', label: 'Sembunyikan' }, + { value: 'true', label: 'Tampilkan' }, + ], + [] + ); + + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + weight_min: null, + weight_max: null, + period: null, + sort_by: null, + show_unrecorded: null, + }, + validationSchema: HppPerKandangFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + kandang_id: values.kandang_id || undefined, + weight_min: values.weight_min || undefined, + weight_max: values.weight_max || undefined, + period: values.period || undefined, + sort_by: values.sort_by || undefined, + show_unrecorded: + values.show_unrecorded !== null ? values.show_unrecorded : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); + + // ===== WEIGHT CHANGE HANDLERS ===== + const handleWeightMinChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_min', value || null); + + if (value && formik.values.weight_max) { + const weightMin = parseFloat(value) || 0; + const weightMax = parseFloat(formik.values.weight_max) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setWeightMaxError(''); + } + }, + [formik, dateErrorShown] + ); + + const handleWeightMaxChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_max', value || null); + + if (value && formik.values.weight_min) { + const weightMin = parseFloat(formik.values.weight_min) || 0; + const weightMax = parseFloat(value) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + [formik, dateErrorShown] + ); + + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_id) return []; + const ids = formik.values.area_id.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_id, areaOptions]); + + const locationIdsValue = useMemo(() => { + if (!formik.values.location_id) return []; + const ids = formik.values.location_id.split(','); + return locationOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.location_id, locationOptions]); + + const kandangIdsValue = useMemo(() => { + if (!formik.values.kandang_id) return []; + const ids = formik.values.kandang_id.split(','); + return kandangOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.kandang_id, kandangOptions]); + + const showUnrecordedValue = useMemo(() => { + if (formik.values.show_unrecorded === null) return null; + return ( + showUnrecordedOptions.find( + (opt) => opt.value === String(formik.values.show_unrecorded) + ) || null + ); + }, [formik.values.show_unrecorded, showUnrecordedOptions]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.period) { + count += 1; + } + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.kandang_id) { + count += 1; + } + + if (filterParams.weight_min || filterParams.weight_max) { + count += 1; + } + + if (filterParams.show_unrecorded !== undefined) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: hppPerKandang, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, + }; + + return ['hpp-per-kandang-report', params]; + } + : null, + ([, params]) => + SaleReportApi.getHppPerKandangReport( + params.area_id, + params.location_id, + params.kandang_id, + params.weight_min, + params.weight_max, + params.period, + params.sort_by, + params.show_unrecorded + ) + ); + + const data: HppPerKandangReport['rows'] = useMemo( + () => + isResponseSuccess(hppPerKandang) + ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] + : [], + [hppPerKandang] + ); + + const summaryTotal = + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total + ? hppPerKandang.data.summary.total + : undefined; + + const perWeightRangeSummary = useMemo( + () => + isResponseSuccess(hppPerKandang) && + hppPerKandang?.data?.summary?.per_weight_range + ? hppPerKandang.data.summary.per_weight_range + : [], + [hppPerKandang] + ); + + // ===== EXPORT DATA FETCHER ===== + const hppPerKandangExport = + useCallback(async (): Promise => { + const params = { + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, + limit: 10000, + page: 1, + }; + + const response = await SaleReportApi.getHppPerKandangReport( + params.area_id, + params.location_id, + params.kandang_id, + params.weight_min, + params.weight_max, + params.period, + params.sort_by, + params.show_unrecorded + ); + + return isResponseSuccess(response) ? response.data : null; + }, [filterParams]); + + // ===== TABLE COLUMNS DEFINITION ===== + const allFeedSuppliers = useMemo(() => { + const suppliers = new Set(); + data.forEach((item: HppPerKandangRow) => { + item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => { + suppliers.add(s.alias || s.name); + }); + }); + return Array.from(suppliers).join(' | '); + }, [data]); + + const allDocSuppliers = useMemo(() => { + const suppliers = new Set(); + data.forEach((item: HppPerKandangRow) => { + item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => { + suppliers.add(s.alias || s.name); + }); + }); + return Array.from(suppliers).join(' | '); + }, [data]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await hppPerKandangExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateHppPerKandangExcel({ + data: allDataForExport, + allFeedSuppliers, + allDocSuppliers, + }); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await hppPerKandangExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const areaName = filterParams.area_id + ? areaOptions + .filter((opt) => + filterParams.area_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; + + const locationName = filterParams.location_id + ? locationOptions + .filter((opt) => + filterParams.location_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Lokasi' + : 'Semua Lokasi'; + + const kandangName = filterParams.kandang_id + ? kandangOptions + .filter((opt) => + filterParams.kandang_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kandang' + : 'Semua Kandang'; + + await generateHppPerKandangPDF( + { + data: allDataForExport, + params: { + area_name: areaName, + location_name: locationName, + kandang_name: kandangName, + period: filterParams.period, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + show_unrecorded: + filterParams.show_unrecorded?.toString() || 'false', + sort_by: filterParams.sort_by, + }, + }, + allFeedSuppliers, + allDocSuppliers + ); + + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + hppPerKandangExport, + filterParams, + areaOptions, + locationOptions, + kandangOptions, + allFeedSuppliers, + allDocSuppliers, + ]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffectHook(() => { + setTabActions( + tabId, +
    + + + +
    + + + Export + +
    + + +
    + + } + > + + + +
    + ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
    TOTAL
    , + }, + { + id: 'kandang_name', + header: 'Kandang', + accessorKey: 'kandang.name', + cell: (props) => { + const row = props.row.original; + return row.name_with_periode || row.kandang?.name || '-'; + }, + footer: () =>
    ALL
    , + }, + { + id: 'weight_range', + header: 'Rentang Bobot', + accessorKey: 'weight_range', + cell: (props) => { + const weightRange = props.row.original.weight_range; + return weightRange + ? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}` + : '-'; + }, + footer: () =>
    -
    , + }, + { + id: 'avg_weight_kg', + header: 'Rata-Rata Bobot (KG)', + accessorKey: 'avg_weight_kg', + cell: (props) => { + const value = props.row.original.avg_weight_kg; + return
    {formatNumber(value)}
    ; + }, + footer: () => ( +
    + {formatNumber(summaryTotal?.average_weight_kg || 0)} +
    + ), + }, + { + id: 'egg_production_pieces', + header: 'Sisa Telur (Butir)', + accessorKey: 'egg_production_pieces', + cell: (props) => { + const value = props.row.original.egg_production_pieces; + return
    {formatNumber(value)}
    ; + }, + footer: () => ( +
    + {formatNumber(summaryTotal?.total_egg_production_pieces || 0)} +
    + ), + }, + { + id: 'egg_production_kg', + header: 'Sisa Telur (KG)', + accessorKey: 'egg_production_kg', + cell: (props) => { + const value = props.row.original.egg_production_kg; + return
    {formatNumber(value)}
    ; + }, + footer: () => ( +
    + {formatNumber(summaryTotal?.total_egg_production_kg || 0)} +
    + ), + }, + { + id: 'feed_suppliers', + header: 'Feed (Supplier)', + accessorKey: 'feed_suppliers', + cell: (props) => { + const suppliers = props.row.original.feed_suppliers; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); + }, + footer: () => ( +
    + {allFeedSuppliers || '-'} +
    + ), + }, + { + id: 'doc_suppliers', + header: 'DOC (Supplier)', + accessorKey: 'doc_suppliers', + cell: (props) => { + const suppliers = props.row.original.doc_suppliers; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); + }, + footer: () => ( +
    + {allDocSuppliers || '-'} +
    + ), + }, + { + id: 'average_doc_price_rp', + header: 'Rata-Rata Harga DOC (RP)', + accessorKey: 'average_doc_price_rp', + cell: (props) => { + const value = props.row.original.average_doc_price_rp; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)} +
    + ), + }, + { + id: 'egg_hpp_rp_per_kg', + header: 'HPP Telur (RP/KG)', + accessorKey: 'egg_hpp_rp_per_kg', + cell: (props) => { + const value = props.row.original.egg_hpp_rp_per_kg; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)} +
    + ), + }, + { + id: 'egg_value_rp', + header: 'Nilai Nominal Sisa Telur (RP)', + accessorKey: 'egg_value_rp', + cell: (props) => { + const value = props.row.original.egg_value_rp; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summaryTotal?.total_egg_value_rp || 0)} +
    + ), + }, + ]; + return tableColumns; + }; + + // ===== CUSTOM ROW RENDERER ===== + const renderCustomRow = useCallback( + (row: Row) => { + if (row.index === data.length - 1) { + const defaultRow = ( +
    + {row.getVisibleCells().map((cell) => ( + + ))} + + ); + + const customRows = [ + + + , + ]; + + if (perWeightRangeSummary.length > 0) { + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index = 0) => { + customRows.push( + + + + + + + + + + + + + + ); + } + ); + } + + return [defaultRow, ...customRows]; + } + + return null; + }, + [data, perWeightRangeSummary] + ); + + return ( + <> +
    + {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( + + } + title='Memuat Data HPP Per Kandang' + subtitle='Silakan tunggu sebentar...' + /> + ) : data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( +
    + {flexRender(cell.column.columnDef.cell, cell.getContext())} +
    + Rekapitulasi per rentang bobot +
    {index + 1}ALL{item.label} + {formatNumber(item.avg_weight_kg)} + + {formatNumber(item.egg_production_pieces)} + + {formatNumber(item.egg_production_kg)} + + {item.feed_suppliers + ?.map((s) => s.alias || s.name) + .join(' | ') || '-'} + + {item.doc_suppliers + ?.map((s) => s.alias || s.name) + .join(' | ') || '-'} + + {formatCurrency(item.average_doc_price_rp)} + + {formatCurrency(item.egg_hpp_rp_per_kg)} + + {formatCurrency(item.egg_value_rp)} +
    0} + renderCustomRow={renderCustomRow} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    +
    +
    + {/* Period Filter */} +
    + { + formik.setFieldValue('period', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + required={true} + isError={!!formik.errors.period && formik.touched.period} + /> + {formik.errors.period && formik.touched.period && ( +
    + {formik.errors.period} +
    + )} +
    + + {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Kandang Filter */} + { + formik.setFieldValue( + 'kandang_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingKandangs} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Weight Range Filter */} +
    + +
    + +
    + +
    +
    + + {/* Show Unrecorded Filter */} + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'show_unrecorded', + val?.value === 'true' || null + ); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
    + + {/* Modal Footer */} +
    + + +
    + +
    + + ); +}; + +export default HppPerKandangTab; diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx deleted file mode 100644 index d79d4c94..00000000 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ /dev/null @@ -1,527 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import toast from 'react-hot-toast'; - -import { Icon } from '@iconify/react'; -import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import Card from '@/components/Card'; -import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; - -import { BaseKandang } from '@/types/api/master-data/kandang'; -import { AreaApi, LocationApi } from '@/services/api/master-data'; -import { - ProjectFlockApi, - ProjectFlockKandangApi, -} from '@/services/api/production'; -import { - BaseProjectFlockKandang, - ProjectFlockKandang, -} from '@/types/api/production/project-flock-kandang'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import Pagination from '@/components/Pagination'; -import { ProductionResultReportApi } from '@/services/api/report/production-result'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { httpClient } from '@/services/http/client'; -import { ProductionResult } from '@/types/api/report/production-result'; -import ProductionResultReportPDF from './ProductionResultReportPDF'; -import { pdf } from '@react-pdf/renderer'; - -const ProductionResultContent = () => { - const [projectFlockKandangs, setProjectFlockKandangs] = useState< - ProjectFlockKandang[] | null - >(null); - const [projectFlockKandangMetadata, setProjectFlockKandangMetadata] = - useState< - | { - page: number; - limit: number; - total_pages: number; - total_results: number; - } - | undefined - >(undefined); - - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - - const [isLoadingSearch, setIsLoadingSearch] = useState(false); - - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); - - const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); - - const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState( - null - ); - const [selectedProjectFlock, setSelectedProjectFlock] = - useState(null); - const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = - useState(null); - - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreaOptions, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name'); - - const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedArea(val as OptionType); - - setSelectedLocation(null); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; - - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', - }); - - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; - - const { - setInputValue: setProjectFlockInputValue, - options: projectFlockOptions, - isLoadingOptions: isLoadingProjectFlockOptions, - loadMore: loadMoreProjectFlocks, - } = useSelect( - ProjectFlockApi.basePath, - 'id', - 'flock_name', - 'search', - { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) - : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) - : '', - category: 'LAYING', - } - ); - - const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedProjectFlock(val as OptionType); - - setSelectedProjectFlockKandang(null); - }; - - const { - setInputValue: setProjectFlockKandangInputValue, - options: projectFlockKandangOptions, - isLoadingOptions: isLoadingProjectFlockKandangOptions, - loadMore: loadMoreProjectFlockKandangs, - } = useSelect( - ProjectFlockKandangApi.basePath, - 'id', - 'kandang.name', - 'search', - { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) - : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) - : '', - project_flock_id: selectedProjectFlock - ? ((selectedProjectFlock as OptionType).value as string) - : '', - } - ); - - const projectFlockKandangChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedProjectFlockKandang(val as OptionType); - }; - - const exportToExcelHandler = async () => { - setIsLoadingExportingToExcel(true); - - await ProductionResultReportApi.exportProductionResultToExcel( - projectFlockKandangs - ); - - setIsLoadingExportingToExcel(false); - }; - - const exportToPdfHandler = async () => { - setIsLoadingExportingToPdf(true); - - try { - let projectFlockKandangsData: BaseProjectFlockKandang[] = []; - - if (selectedProjectFlockKandang) { - const projectFlockKandangResponse = - await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number - ); - - projectFlockKandangsData = isResponseSuccess( - projectFlockKandangResponse - ) - ? [projectFlockKandangResponse.data] - : []; - } else { - const projectFlockKandangsResponse = - await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, - }); - - projectFlockKandangsData = isResponseSuccess( - projectFlockKandangsResponse - ) - ? projectFlockKandangsResponse.data - : []; - } - - const mappedProductionResults: { - projectFlockKandang: BaseProjectFlockKandang; - productionResult: ProductionResult[] | null; - }[] = await Promise.all( - projectFlockKandangsData.map(async (projectFlockKandang) => { - const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; - const getProductionResultRes = await httpClient< - BaseApiResponse - >(getProductionResultPath); - - return { - projectFlockKandang, - productionResult: isResponseSuccess(getProductionResultRes) - ? getProductionResultRes.data - : null, - }; - }) - ); - - if (mappedProductionResults.length === 0) { - toast.error('Tidak ada data untuk diexport.'); - setIsLoadingExportingToPdf(false); - return; - } - - const openPdf = async () => { - const productionResultPdfBlob = await pdf( - - ).toBlob(); - - const productionResultReportPdfUrl = URL.createObjectURL( - productionResultPdfBlob - ); - window.open(productionResultReportPdfUrl, '_blank'); - }; - - await openPdf(); - } catch (error) { - console.error(error); - toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); - } - - setIsLoadingExportingToPdf(false); - }; - - const searchHandler = async () => { - setProjectFlockKandangs(null); - setIsLoadingSearch(true); - - try { - if (selectedProjectFlockKandang) { - const projectFlockKandangResponse = - await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number - ); - - if ( - !projectFlockKandangResponse || - isResponseError(projectFlockKandangResponse) - ) { - throw new Error(); - } - - setProjectFlockKandangs([projectFlockKandangResponse.data]); - setProjectFlockKandangMetadata({ - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }); - setIsLoadingSearch(false); - return; - } - - const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, - }); - - if ( - !projectFlockKandangsResponse || - isResponseError(projectFlockKandangsResponse) - ) { - throw new Error(); - } - - setProjectFlockKandangs(projectFlockKandangsResponse.data); - setProjectFlockKandangMetadata(projectFlockKandangsResponse.meta); - setIsLoadingSearch(false); - } catch (error) { - toast.error('Gagal mencari data! Coba lagi.'); - setProjectFlockKandangs(null); - setProjectFlockKandangMetadata(undefined); - setIsLoadingSearch(false); - } - }; - - const resetHandler = () => { - setProjectFlockKandangs(null); - setSelectedArea(null); - setSelectedLocation(null); - setSelectedProjectFlock(null); - setSelectedProjectFlockKandang(null); - // resetFilter(); - }; - - return ( -
    - -
    -

    - Laporan Hasil Produksi -

    -
    - - {/* Filters */} -
    -
    - - - - - - - -
    - -
    -
    - - - - - Export{' '} - - - } - > - - - - - -
    -
    -
    -
    - -
    - {isLoadingSearch && ( - - )} - - {!isLoadingSearch && !projectFlockKandangs && ( -

    - Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -

    - )} - - {!isLoadingSearch && projectFlockKandangs?.length === 0 && ( -

    - Tidak ada data kandang project flock yang dapat ditampilkan. -

    - )} - - {!isLoadingSearch && projectFlockKandangs && ( - - {projectFlockKandangs.map((projectFlockKandang) => ( - - ))} - -
    - - setPage((currPage) => - currPage > 1 ? currPage - 1 : currPage - ) - } - onNextPage={() => - setPage((currPage) => - projectFlockKandangMetadata?.total_pages && - currPage < projectFlockKandangMetadata.total_pages - ? currPage + 1 - : currPage - ) - } - onPageChange={(pageNumber) => setPage(pageNumber)} - rowOptions={[10, 20, 50, 100]} - onRowChange={setPageSize} - /> -
    -
    - )} -
    -
    - ); -}; - -export default ProductionResultContent; diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx deleted file mode 100644 index 9bc27c4b..00000000 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ /dev/null @@ -1,388 +0,0 @@ -'use client'; - -import React from 'react'; -import { - Document, - Page, - StyleSheet, - Text, - View, - Image, -} 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'; - -type MappedProductionResultsItem = { - projectFlockKandang: BaseProjectFlockKandang; - productionResult: ProductionResult[] | null; -}; - -interface ProductionResultReportPDFProps { - mappedProductionResults?: MappedProductionResultsItem[]; -} - -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, - }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 420, - 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', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - position: 'absolute', - fontSize: 8, - bottom: 22, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', - }, - - section: { - marginTop: 12, - borderWidth: 1, - borderColor: '#000', - padding: 8, - }, - - sectionHeader: { - marginBottom: 6, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - sectionTitle: { - fontSize: 10, - fontWeight: 'bold', - }, - 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', - fontStyle: 'italic', - }, -}); - -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)], - - // BW - ['BW', valueText(pr.bw)], - ['Std BW', valueText(pr.std_bw)], - ['Uniformity', valueText(pr.uniformity)], - ['Std Uniformity', valueText(pr.std_uniformity)], - - // Dep - ['Dep Kum', valueText(pr.dep_kum)], - ['Dep Std', valueText(pr.dep_std)], - - // 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)], - - // 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)], - - // % - ['% Utuh', valueText(pr.persen_utuh)], - ['% Putih', valueText(pr.persen_putih)], - ['% Retak', valueText(pr.persen_retak)], - ['% Pecah', valueText(pr.persen_pecah)], - - // 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 ( - - {rows.map(([label, value], idx) => { - const isLast = idx === rows.length - 1; - return ( - - {label} - {value} - - ); - })} - - ); -} - -/** - * 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 ( - - {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 ( - - - {headerLeft} - {headerRight} - - - - - ); - })} - - ); -} - -/** - * ✅ Main PDF Component - */ -const ProductionResultReportPDF = ({ - mappedProductionResults = [], -}: ProductionResultReportPDFProps) => { - return ( - - - {/* Header */} - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - - - - - PT LUMBUNG TELUR INDONESIA - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - - - - - Laporan Production Result - - {/* Sections per ProjectFlockKandang */} - {mappedProductionResults.length === 0 ? ( - - Tidak ada data. - - ) : ( - 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}`; - - const projectName = pfk?.project_flock?.name ?? ''; - - const locationName = pfk?.project_flock?.location?.name ?? ''; - - const areaName = pfk?.project_flock?.area?.name ?? ''; - - return ( - 0} // each kandang starts on a new page for clarity - > - - - {projectName - ? `${projectName} • ${kandangName}` - : kandangName} - - - {[areaName, locationName].filter(Boolean).join(' • ')} - - - - {item.productionResult && item.productionResult.length > 0 ? ( - - ) : ( - - Tidak ada production result untuk kandang ini. - - )} - - ); - }) - )} - - {/* Footer */} - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - - - - ); -}; - -export default ProductionResultReportPDF; diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx new file mode 100644 index 00000000..6f5e4410 --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; +import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; + +const ProductionResultTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Hasil Produksi', + content: , + }, + ]; + + return ( +
    + +
    + ); +}; + +export default ProductionResultTabs; diff --git a/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx new file mode 100644 index 00000000..76336569 --- /dev/null +++ b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx @@ -0,0 +1,550 @@ +'use client'; + +import React from 'react'; +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; + productionResult: ProductionResult[] | null; +}; + +interface ProductionResultReportPDFProps { + mappedProductionResults?: MappedProductionResultsItem[]; +} + +const styles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, + tableSection: { + marginBottom: 12, + }, + tableTitle: { + fontSize: 10, + fontWeight: 'bold', + marginBottom: 6, + color: '#333', + }, + emptyText: { + fontSize: 8, + color: '#666', + fontStyle: 'italic', + }, +}); + +function valueText(v: unknown) { + if (v === null || v === undefined) return '-'; + if (typeof v === 'number') return formatNumber(v); + return String(v); +} + +// ======================================== +// TABLE 1: WOA & BW +// ======================================== +const getBwTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 2: DEPLESI +// ======================================== +const getDepTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 3: BUTIRAN +// ======================================== +const getButiranTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 4: BERAT (KG) +// ======================================== +const getKgTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 5: PERSENTASE +// ======================================== +const getPersenTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 6: PRODUKSI (HD, FI, EM, EW) +// ======================================== +const getProduksi1TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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), + }, +]; + +// ======================================== +// TABLE 7: PRODUKSI (FCR, HH) +// ======================================== +const getProduksi2TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ 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 + */ +const ProductionResultReportPDF = ({ + mappedProductionResults = [], +}: ProductionResultReportPDFProps) => { + return ( + + {mappedProductionResults.length === 0 ? ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + + + Tidak ada data. + + + + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; + + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + const projectName = pfk?.project_flock?.name ?? ''; + + const locationName = pfk?.project_flock?.location?.name ?? ''; + + const areaName = pfk?.project_flock?.area?.name ?? ''; + + const hasData = + item.productionResult && item.productionResult.length > 0; + + return ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + + + + {hasData ? ( + <> + {/* Table 1: WOA & BW */} + + 1. WOA & Body Weight + + + + {/* Table 2: Deplesi */} + + 2. Deplesi + + + + {/* Table 3: Butiran */} + + 3. Butiran + + + + {/* Table 4: Berat (Kg) */} + + 4. Berat (Kg) + + + + {/* Table 5: Persentase */} + + 5. Persentase + + + + {/* Table 6: Produksi (HD, FI, EM, EW) */} + + + 6. Produksi (HD, FI, EM, EW) + + + + + {/* Table 7: Produksi (FCR, HH) */} + + 7. Produksi (FCR, HH) + + + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + + + ); + }) + )} + + ); +}; + +export default ProductionResultReportPDF; diff --git a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx new file mode 100644 index 00000000..af0380c0 --- /dev/null +++ b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx @@ -0,0 +1,135 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatNumber } from '@/lib/helper'; +import { ProductionResult } from '@/types/api/report/production-result'; + +interface ProductionResultExportExcelParams { + data: ProductionResult[]; + period?: string; +} + +export const generateProductionResultExcel = async ( + params: ProductionResultExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== PRODUCTION RESULT WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 6 }, + { header: 'Project Flock', key: 'projectFlockName', width: 25 }, + { + header: 'Category', + key: 'projectFlockCategory', + width: 18, + }, + { header: 'Kandang', key: 'kandangName', width: 18 }, + { header: 'Week of Age (WOA)', key: 'woa', width: 20 }, + { header: 'Body Weight (BW)', key: 'bw', width: 18 }, + { header: 'Body Weight (Std BW)', key: 'stdBw', width: 22 }, + { header: 'Uniformity (%)', key: 'uniformity', width: 16 }, + { header: 'Uniformity Std (%)', key: 'stdUniformity', width: 20 }, + { header: 'Depletion Cumulative', key: 'depKum', width: 22 }, + { header: 'Depletion Standard', key: 'depStd', width: 20 }, + { header: 'Telur Utuh', key: 'butiranUtuh', width: 14 }, + { header: 'Telur Putih', key: 'butiranPutih', width: 14 }, + { header: 'Telur Retak', key: 'butiranRetak', width: 14 }, + { header: 'Telur Pecah', key: 'butiranPecah', width: 14 }, + { header: 'Jumlah Telur', key: 'butiranJumlah', width: 16 }, + { header: 'Total Telur', key: 'totalButir', width: 14 }, + { header: 'Utuh (Kg)', key: 'kgUtuh', width: 12 }, + { header: 'Putih (Kg)', key: 'kgPutih', width: 12 }, + { header: 'Retak (Kg)', key: 'kgRetak', width: 12 }, + { header: 'Pecah (Kg)', key: 'kgPecah', width: 12 }, + { header: 'Jumlah (Kg)', key: 'kgJumlah', width: 14 }, + { header: 'Total Weight (Kg)', key: 'totalKg', width: 20 }, + { header: 'Utuh (%)', key: 'persenUtuh', width: 12 }, + { header: 'Putih (%)', key: 'persenPutih', width: 12 }, + { header: 'Retak (%)', key: 'persenRetak', width: 12 }, + { header: 'Pecah (%)', key: 'persenPecah', width: 12 }, + { header: 'Hen Day (HD)', key: 'hd', width: 15 }, + { header: 'Hen Day Std (HD Std)', key: 'hdStd', width: 22 }, + { header: 'Feed Intake (FI)', key: 'fi', width: 18 }, + { header: 'Feed Intake Std (FI Std)', key: 'fiStd', width: 25 }, + { header: 'Egg Mass (EM)', key: 'em', width: 16 }, + { header: 'Egg Mass Std (EM Std)', key: 'emStd', width: 23 }, + { header: 'Egg Weight (EW)', key: 'ew', width: 18 }, + { header: 'Egg Weight Std (EW Std)', key: 'ewStd', width: 25 }, + { header: 'Feed Conversion Ratio (FCR)', key: 'fcr', width: 30 }, + { + header: 'Feed Conversion Ratio Std (FCR Std)', + key: 'fcrStd', + width: 35, + }, + { header: 'Hen House (HH)', key: 'hh', width: 18 }, + { header: 'Hen House Std (HH Std)', key: 'hhStd', width: 25 }, + ]; + + const worksheet = workbook.addWorksheet('Production Result'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: ProductionResult, index: number) => { + worksheet.addRow({ + no: index + 1, + projectFlockName: item.project_flock?.name || '', + projectFlockCategory: item.project_flock?.category || '', + kandangName: item.project_flock?.kandang?.name || '', + woa: formatNumber(item.woa || 0), + bw: formatNumber(item.bw || 0), + stdBw: formatNumber(item.std_bw || 0), + uniformity: formatNumber(item.uniformity || 0), + stdUniformity: item.std_uniformity || '', + depKum: formatNumber(item.dep_kum || 0), + depStd: formatNumber(item.dep_std || 0), + butiranUtuh: formatNumber(item.butiran_utuh || 0), + butiranPutih: formatNumber(item.butiran_putih || 0), + butiranRetak: formatNumber(item.butiran_retak || 0), + butiranPecah: formatNumber(item.butiran_pecah || 0), + butiranJumlah: formatNumber(item.butiran_jumlah || 0), + totalButir: formatNumber(item.total_butir || 0), + kgUtuh: formatNumber(item.kg_utuh || 0), + kgPutih: formatNumber(item.kg_putih || 0), + kgRetak: formatNumber(item.kg_retak || 0), + kgPecah: formatNumber(item.kg_pecah || 0), + kgJumlah: formatNumber(item.kg_jumlah || 0), + totalKg: formatNumber(item.total_kg || 0), + persenUtuh: formatNumber(item.persen_utuh || 0), + persenPutih: formatNumber(item.persen_putih || 0), + persenRetak: formatNumber(item.persen_retak || 0), + persenPecah: formatNumber(item.persen_pecah || 0), + hd: formatNumber(item.hd || 0), + hdStd: formatNumber(item.hd_std || 0), + fi: formatNumber(item.fi || 0), + fiStd: formatNumber(item.fi_std || 0), + em: formatNumber(item.em || 0), + emStd: formatNumber(item.em_std || 0), + ew: formatNumber(item.ew || 0), + ewStd: formatNumber(item.ew_std || 0), + fcr: formatNumber(item.fcr || 0), + fcrStd: formatNumber(item.fcr_std || 0), + hh: formatNumber(item.hh || 0), + hhStd: formatNumber(item.hh_std || 0), + }); + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-hasil-produksi-${params.period}.xlsx` + : `laporan-hasil-produksi-${currentDate}.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); +}; diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts new file mode 100644 index 00000000..6df3759e --- /dev/null +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -0,0 +1,59 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type ProductionResultFilterProps = { + area_id: string | null; + location_id: string | null; + project_flock_id: string | null; + kandang_id: string | null; +}; + +export type ProductionResultFilterFormType = { + area_id: OptionType | null; + location_id: OptionType | null; + project_flock_id: OptionType | null; + kandang_id: OptionType | null; +}; + +export const ProductionResultFilterSchema = yup.object({ + area_id: yup + .mixed() + .required('Area wajib dipilih') + .test('is-not-empty', 'Area wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + location_id: yup + .mixed() + .required('Lokasi wajib dipilih') + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + project_flock_id: yup + .mixed() + .required('Project Flock wajib dipilih') + .test('is-not-empty', 'Project Flock wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + kandang_id: yup + .mixed() + .required('Kandang wajib dipilih') + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), +}) as yup.ObjectSchema; + +export type ProductionResultFilterValues = yup.InferType< + typeof ProductionResultFilterSchema +>; diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx new file mode 100644 index 00000000..07d33233 --- /dev/null +++ b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProductionResult } from '@/types/api/report/production-result'; +import { ColumnDef } from '@tanstack/react-table'; + +type ProductionResultColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { + row: { original: ProductionResult }; + }) => React.ReactNode; + }>; + }; + +const ProductionResultSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ProductionResultColumn[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
    +
    +
    + +
    + + ); +}; + +export default ProductionResultSkeleton; diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx new file mode 100644 index 00000000..9ac5faf6 --- /dev/null +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -0,0 +1,842 @@ +'use client'; + +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; +import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable'; +import { useFormik } from 'formik'; +import { + ProductionResultFilterSchema, + type ProductionResultFilterValues, +} from '@/components/pages/report/production-result/filter/ProductionResultFilter'; + +import { BaseKandang } from '@/types/api/master-data/kandang'; +import { AreaApi, LocationApi } from '@/services/api/master-data'; +import { + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; +import { + BaseProjectFlockKandang, + ProjectFlockKandang, +} from '@/types/api/production/project-flock-kandang'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { ProductionResultReportApi } from '@/services/api/report/production-result'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { httpClient } from '@/services/http/client'; +import { ColumnDef } from '@tanstack/react-table'; +import { ProductionResult } from '@/types/api/report/production-result'; +import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; +import { pdf } from '@react-pdf/renderer'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import { cn, formatNumber } from '@/lib/helper'; +import Pagination from '@/components/Pagination'; +import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton'; + +interface ProductionResultTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + project_flock_id?: string; + project_flock_kandang_id?: string; +} + +const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const filterModal = useModal(); + + // ===== TABLE COLUMNS ===== + const productionResultColumns: ColumnDef[] = [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'woa', + header: 'WOA', + }, + { + accessorKey: 'bw', + header: 'BW', + cell: (props) => formatNumber(props.row.original.bw), + }, + { + accessorKey: 'std_bw', + header: 'STD BW', + cell: (props) => formatNumber(props.row.original.std_bw), + }, + { + accessorKey: 'uniformity', + header: 'Uniformity', + cell: (props) => formatNumber(props.row.original.uniformity), + }, + { + accessorKey: 'std_uniformity', + header: 'STD Uniformity', + }, + { + accessorKey: 'dep_kum', + header: 'Dep Kum', + cell: (props) => formatNumber(props.row.original.dep_kum), + }, + { + accessorKey: 'dep_std', + header: 'Dep STD', + cell: (props) => formatNumber(props.row.original.dep_std), + }, + { + header: 'Butiran', + columns: [ + { + accessorKey: 'butiran_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.butiran_utuh), + }, + { + accessorKey: 'butiran_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.butiran_putih), + }, + { + accessorKey: 'butiran_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.butiran_retak), + }, + { + accessorKey: 'butiran_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.butiran_pecah), + }, + { + accessorKey: 'butiran_jumlah', + header: 'Jumlah (Butir)', + cell: (props) => formatNumber(props.row.original.butiran_jumlah), + }, + { + accessorKey: 'total_butir', + header: 'Total Butir', + cell: (props) => formatNumber(props.row.original.total_butir), + }, + ], + }, + { + header: 'Kg', + columns: [ + { + accessorKey: 'kg_utuh', + header: 'Utuh (Kg)', + cell: (props) => formatNumber(props.row.original.kg_utuh), + }, + { + accessorKey: 'kg_putih', + header: 'Putih (Kg)', + cell: (props) => formatNumber(props.row.original.kg_putih), + }, + { + accessorKey: 'kg_retak', + header: 'Retak (Kg)', + cell: (props) => formatNumber(props.row.original.kg_retak), + }, + { + accessorKey: 'kg_pecah', + header: 'Pecah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_pecah), + }, + { + accessorKey: 'kg_jumlah', + header: 'Jumlah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_jumlah), + }, + { + accessorKey: 'total_kg', + header: 'Total Kg', + cell: (props) => formatNumber(props.row.original.total_kg), + }, + ], + }, + { + header: '%', + columns: [ + { + accessorKey: 'persen_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.persen_utuh), + }, + { + accessorKey: 'persen_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.persen_putih), + }, + { + accessorKey: 'persen_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.persen_retak), + }, + { + accessorKey: 'persen_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.persen_pecah), + }, + ], + }, + ]; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + project_flock_id: null, + kandang_id: null, + }, + validationSchema: ProductionResultFilterSchema, + validateOnBlur: true, + validateOnChange: true, + onSubmit: (values) => { + setFilterParams({ + area_id: values.area_id?.value + ? String(values.area_id.value) + : undefined, + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + project_flock_id: values.project_flock_id?.value + ? String(values.project_flock_id.value) + : undefined, + project_flock_kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + + // ===== OPTIONS ===== + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreas, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) + : '', + }); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + isLoadingOptions: isLoadingProjectFlocks, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) + : '', + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) + : '', + category: 'LAYING', + } + ); + + const { + setInputValue: setProjectFlockKandangInputValue, + options: projectFlockKandangOptions, + isLoadingOptions: isLoadingProjectFlockKandangs, + loadMore: loadMoreProjectFlockKandangs, + } = useSelect( + ProjectFlockKandangApi.basePath, + 'id', + 'kandang.name', + 'search', + { + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) + : '', + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) + : '', + project_flock_id: formik.values.project_flock_id?.value + ? String(formik.values.project_flock_id.value) + : '', + } + ); + + // ===== FILTER HELPERS ===== + const areaValue = useMemo( + () => formik.values.area_id, + [formik.values.area_id] + ); + + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + + const projectFlockValue = useMemo( + () => formik.values.project_flock_id, + [formik.values.project_flock_id] + ); + + const projectFlockKandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) count += 1; + if (filterParams.location_id) count += 1; + if (filterParams.project_flock_id) count += 1; + if (filterParams.project_flock_kandang_id) count += 1; + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: projectFlockKandangsData, isLoading } = useSWR< + BaseApiResponse + >( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.area_id) + params.append('area_id', filterParams.area_id); + if (filterParams.project_flock_id) + params.append('project_flock_id', filterParams.project_flock_id); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`/production/project-flock-kandangs?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const projectFlockKandangs = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.data + : null, + [projectFlockKandangsData] + ); + + const projectFlockKandangMetadata = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.meta + : undefined, + [projectFlockKandangsData] + ); + + // ===== EXPORT HANDLERS ===== + const exportToExcelHandler = useCallback(async () => { + setIsExcelExportLoading(true); + + try { + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; + + if (filterParams.project_flock_kandang_id) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + Number(filterParams.project_flock_kandang_id) + ); + + projectFlockKandangsFetch = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, + }); + + projectFlockKandangsFetch = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const productionResultData: ProductionResult[] = []; + + for (const kandang of projectFlockKandangsFetch) { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${kandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + if (isResponseSuccess(getProductionResultRes)) { + productionResultData.push( + ...(getProductionResultRes.data?.map((result) => ({ + ...result, + project_flock: { + ...result.project_flock, + name: + projectFlockValue?.label || + kandang.project_flock?.name || + `Project Flock #${kandang.project_flock_id}`, + category: kandang.project_flock?.category || '', + kandang: { + ...result.project_flock?.kandang, + name: + kandang.kandang?.name || `Kandang #${kandang.kandang_id}`, + }, + }, + })) || []) + ); + } + } + + if (productionResultData.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsExcelExportLoading(false); + return; + } + + await generateProductionResultExcel({ + data: productionResultData, + period: '', + }); + } catch { + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [filterParams, projectFlockValue]); + + const exportToPdfHandler = useCallback(async () => { + setIsPdfExportLoading(true); + + try { + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; + + if (filterParams.project_flock_kandang_id) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + Number(filterParams.project_flock_kandang_id) + ); + + projectFlockKandangsFetch = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, + }); + + projectFlockKandangsFetch = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const mappedProductionResults: { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; + }[] = await Promise.all( + projectFlockKandangsFetch.map(async (projectFlockKandang) => { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + return { + projectFlockKandang, + productionResult: isResponseSuccess(getProductionResultRes) + ? getProductionResultRes.data + : null, + }; + }) + ); + + if (mappedProductionResults.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsPdfExportLoading(false); + return; + } + + const openPdf = async () => { + const productionResultPdfBlob = await pdf( + + ).toBlob(); + + const productionResultReportPdfUrl = URL.createObjectURL( + productionResultPdfBlob + ); + window.open(productionResultReportPdfUrl, '_blank'); + }; + + await openPdf(); + } catch (error) { + console.error(error); + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } + + setIsPdfExportLoading(false); + }, [filterParams]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
    + + + +
    + + + Export + +
    + + +
    + + } + > + + + +
    + ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + exportToExcelHandler, + exportToPdfHandler, + setTabActions, + ]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + return ( + <> +
    + {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
    + +
    + ) : !projectFlockKandangs || projectFlockKandangs.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> + {projectFlockKandangs.map( + (projectFlockKandang: ProjectFlockKandang) => ( + + ) + )} + +
    + + setPage((currPage) => + currPage > 1 ? currPage - 1 : currPage + ) + } + onNextPage={() => + setPage((currPage) => + projectFlockKandangMetadata?.total_pages && + currPage < projectFlockKandangMetadata.total_pages + ? currPage + 1 + : currPage + ) + } + onPageChange={(pageNumber) => setPage(pageNumber)} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> +
    + + )} +
    + + {/* Filter Modal */} + + {/* Modal Header */} +
    +
    + +

    Filter Data

    +
    + +
    + +
    + {/* Modal Body */} +
    + { + formik.setFieldValue('area_id', val); + formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} + isClearable + isError={formik.touched.area_id && Boolean(formik.errors.area_id)} + errorMessage={formik.errors.area_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('location_id', val); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + isDisabled={!formik.values.area_id} + isError={ + formik.touched.location_id && Boolean(formik.errors.location_id) + } + errorMessage={formik.errors.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('project_flock_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} + isClearable + isDisabled={!formik.values.location_id} + isError={ + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) + } + errorMessage={formik.errors.project_flock_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} + isClearable + isDisabled={!formik.values.project_flock_id} + isError={ + formik.touched.kandang_id && Boolean(formik.errors.kandang_id) + } + errorMessage={formik.errors.kandang_id} + className={{ wrapper: 'w-full' }} + /> +
    + + {/* Modal Footer */} +
    + + +
    + +
    + + ); +}; + +export default ProductionResultContent; diff --git a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx similarity index 73% rename from src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx rename to src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx index e1dfd515..25b1bd28 100644 --- a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx @@ -4,12 +4,10 @@ import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { ProductionResult } from '@/types/api/report/production-result'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -30,7 +28,6 @@ const ProductionResultProjectFlockKandangTable = ({ setPage, setPageSize, toQueryString: getTableFilterQueryString, - reset: resetFilter, } = useTableFilter({ initial: { filter_by: '', @@ -52,8 +49,6 @@ const ProductionResultProjectFlockKandangTable = ({ } ); - const [open, setOpen] = useState(false); - const [sorting, setSorting] = useState([]); const productionResultColumns: ColumnDef[] = [ @@ -270,93 +265,60 @@ const ProductionResultProjectFlockKandangTable = ({ } }, [sorting]); - useEffect(() => { - if (!open) { - setOpen( + return ( + 0 : false - ); - } - }, [productionResults, isResponseSuccess]); - - return ( - - -
    {kandangName}
    - - -
    + + data={ + isResponseSuccess(productionResults) ? productionResults?.data : [] } - className='w-full!' - titleClassName='w-full p-0!' - > -
    - {/*
    -
    - -
    -
    */} - - - data={ - isResponseSuccess(productionResults) - ? productionResults?.data - : [] - } - columns={productionResultColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingProductionResults} - sorting={sorting} - setSorting={setSorting} - renderFooter={false} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(productionResults) && - productionResults?.data?.length === 0, - }), - headerColumnClassName: - 'px-4 py-3 border-x border-base-content/10 text-base-content/50', - }} - /> -
    - + columns={productionResultColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingProductionResults} + sorting={sorting} + setSorting={setSorting} + renderFooter={false} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> ); }; diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx deleted file mode 100644 index 988c16b2..00000000 --- a/src/components/pages/report/sale/SaleReportTabs.tsx +++ /dev/null @@ -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: , - }, - ]; - - return ( -
    - -
    - ); -}; - -export default SaleReportTabs; diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx deleted file mode 100644 index 3a76d8f4..00000000 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ /dev/null @@ -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 ( - - - {/* Title and Parameters */} - - - Laporan > HPP Harian Kandang - - - {getParameterText(params).map((param, index) => ( - - {param} - - ))} - - - - {/* Rekapitulasi Section */} - - Rekapitulasi - - - - {/* Detail Per Kandang Section */} - - Detail Per Kandang - - - - - ); -}; - -export const generateHppPerKandangPDF = async ( - data: HppPerKandangExportParams['data'], - params: HppPerKandangExportParams['params'] -): Promise => { - 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; - } -}; diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx deleted file mode 100644 index 7bd774f3..00000000 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ /dev/null @@ -1,979 +0,0 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; -import useSWR from 'swr'; -import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; -import DateInput from '@/components/input/DateInput'; -import NumberInput from '@/components/input/NumberInput'; -import { AreaApi } from '@/services/api/master-data'; -import { LocationApi } from '@/services/api/master-data'; -import { ProjectFlockKandangApi } from '@/services/api/production'; -import { SaleReportApi } from '@/services/api/report/marketing-sale'; -import Table from '@/components/Table'; -import { ColumnDef, Row, flexRender } from '@tanstack/react-table'; -import { formatCurrency, formatNumber } from '@/lib/helper'; -import { - HppPerKandangReport, - HppPerKandangRow, - HppPerKandangPerWeightRange, -} from '@/types/api/report/hpp-per-kandang'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -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 toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; -import { Icon } from '@iconify/react'; - -const HppPerKandangTab = () => { - // ===== STATE MANAGEMENT ===== - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); - const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; - - // ===== SUBMISSION STATE ===== - const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== VALIDATION STATE ===== - const [weightMaxError, setWeightMaxError] = useState(''); - - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - location_id: [] as string[], - kandang_id: [] as string[], - weight_min: '', - weight_max: '', - period: '', - sort_by: '', - show_unrecorded: false, - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); - - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreas, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocations, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - loadMore: loadMoreKandangs, - } = useSelect( - ProjectFlockKandangApi.basePath, - 'id', - 'name_with_period', - 'search' - ); - - const showUnrecordedOptions: OptionType[] = [ - { value: 'false', label: 'Sembunyikan' }, - { value: 'true', label: 'Tampilkan' }, - ]; - - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'location_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'kandang_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const weightMinChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); - setIsSubmitted(false); - - if (weightMaxError) { - setWeightMaxError(''); - } - }, - [updateFilter, weightMaxError] - ); - - const weightMaxChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - const weightMax = val ? parseFloat(val) || 0 : 0; - const weightMin = tableFilterState.weight_min - ? parseFloat(tableFilterState.weight_min) - : 0; - - if (weightMax < weightMin) { - setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min'); - toast.error('Rentang bobot max tidak boleh lebih kecil dari min'); - return; - } - - setWeightMaxError(''); - updateFilter('weight_max', val ? String(weightMax) : ''); - setIsSubmitted(false); - }, - [updateFilter, tableFilterState.weight_min] - ); - - const periodChangeHandler = useCallback>( - (e) => { - const val = e.target.value; - updateFilter('period', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const showUnrecordedChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('show_unrecorded', newVal?.value === 'true'); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('location_id', []); - updateFilter('kandang_id', []); - updateFilter('weight_min', ''); - updateFilter('weight_max', ''); - updateFilter('period', ''); - updateFilter('sort_by', ''); - updateFilter('show_unrecorded', false); - setIsSubmitted(false); - }, [updateFilter]); - - const handleSubmit = useCallback(() => { - if (!tableFilterState.period) { - toast.error('Periode wajib diisi'); - return; - } - setIsSubmitted(true); - }, [tableFilterState.period]); - - // ===== DATA FETCHING ===== - const { data: hppPerKandang, isLoading } = useSWR( - isSubmitted - ? () => { - const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, - }; - - return ['hpp-per-kandang-report', params]; - } - : null, - ([, params]) => - SaleReportApi.getHppPerKandangReport( - params.area_id, - params.location_id, - params.kandang_id, - params.weight_min, - params.weight_max, - params.period, - params.sort_by, - params.show_unrecorded - ) - ); - - const data: HppPerKandangReport['rows'] = useMemo( - () => - isResponseSuccess(hppPerKandang) - ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] - : [], - [hppPerKandang] - ); - - const summaryTotal = - isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total - ? hppPerKandang.data.summary.total - : undefined; - - const perWeightRangeSummary = useMemo( - () => - isResponseSuccess(hppPerKandang) && - hppPerKandang?.data?.summary?.per_weight_range - ? hppPerKandang.data.summary.per_weight_range - : [], - [hppPerKandang] - ); - - const period = - isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period - ? hppPerKandang.data.period - : undefined; - - // ===== EXPORT DATA FETCHER ===== - const hppPerKandangExport = - useCallback(async (): Promise => { - const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, - limit: 10000, - page: 1, - }; - - const response = await SaleReportApi.getHppPerKandangReport( - params.area_id, - params.location_id, - params.kandang_id, - params.weight_min, - params.weight_max, - params.period, - params.sort_by, - params.show_unrecorded - ); - - return isResponseSuccess(response) ? response.data : null; - }, [tableFilterState]); - - // ===== TABLE COLUMNS DEFINITION ===== - const allFeedSuppliers = useMemo(() => { - const suppliers = new Set(); - data.forEach((item: HppPerKandangRow) => { - item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => { - suppliers.add(s.alias || s.name); - }); - }); - return Array.from(suppliers).join(' | '); - }, [data]); - - const allDocSuppliers = useMemo(() => { - const suppliers = new Set(); - data.forEach((item: HppPerKandangRow) => { - item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => { - suppliers.add(s.alias || s.name); - }); - }); - return Array.from(suppliers).join(' | '); - }, [data]); - - // ===== EXPORT HANDLERS ===== - const handleExportExcel = useCallback(async () => { - setIsExcelExportLoading(true); - try { - const allDataForExport = await hppPerKandangExport(); - - if ( - !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 - ) { - toast.error('Tidak ada data untuk diekspor.'); - 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, - }); - - 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, - ]); - - const handleExportPDF = useCallback(async () => { - setIsPdfExportLoading(true); - try { - const allDataForExport = await hppPerKandangExport(); - - if ( - !allDataForExport || - !allDataForExport?.rows || - allDataForExport.rows.length === 0 - ) { - toast.error('Tidak ada data untuk diekspor.'); - return; - } - - const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' - : 'Semua Area'; - - const locationName = - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id - .map( - (id) => - locationOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Lokasi' - : 'Semua Lokasi'; - - const kandangName = - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id - .map( - (id) => - kandangOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .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, - }); - - toast.success('PDF berhasil dibuat dan diunduh.'); - } catch { - toast.error('Gagal membuat PDF. Silakan coba lagi.'); - } finally { - setIsPdfExportLoading(false); - } - }, [ - hppPerKandangExport, - tableFilterState, - areaOptions, - locationOptions, - kandangOptions, - ]); - - const getTableColumns = (): ColumnDef[] => { - const tableColumns: ColumnDef[] = [ - { - id: 'no', - header: 'No', - cell: (props) => props.row.index + 1, - footer: () =>
    TOTAL
    , - }, - { - id: 'kandang_name', - header: 'Kandang', - accessorKey: 'kandang.name', - cell: (props) => { - const row = props.row.original; - return row.name_with_periode || row.kandang?.name || '-'; - }, - footer: () =>
    ALL
    , - }, - { - id: 'weight_range', - header: 'Rentang Bobot', - accessorKey: 'weight_range', - cell: (props) => { - const weightRange = props.row.original.weight_range; - return weightRange - ? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}` - : '-'; - }, - footer: () =>
    -
    , - }, - { - id: 'avg_weight_kg', - header: 'Rata-Rata Bobot (KG)', - accessorKey: 'avg_weight_kg', - cell: (props) => { - const value = props.row.original.avg_weight_kg; - return
    {formatNumber(value)}
    ; - }, - footer: () => ( -
    - {formatNumber(summaryTotal?.average_weight_kg || 0)} -
    - ), - }, - { - id: 'egg_production_pieces', - header: 'Sisa Telur (Butir)', - accessorKey: 'egg_production_pieces', - cell: (props) => { - const value = props.row.original.egg_production_pieces; - return
    {formatNumber(value)}
    ; - }, - footer: () => ( -
    - {formatNumber(summaryTotal?.total_egg_production_pieces || 0)} -
    - ), - }, - { - id: 'egg_production_kg', - header: 'Sisa Telur (KG)', - accessorKey: 'egg_production_kg', - cell: (props) => { - const value = props.row.original.egg_production_kg; - return
    {formatNumber(value)}
    ; - }, - footer: () => ( -
    - {formatNumber(summaryTotal?.total_egg_production_kg || 0)} -
    - ), - }, - { - id: 'feed_suppliers', - header: 'Feed (Supplier)', - accessorKey: 'feed_suppliers', - cell: (props) => { - const suppliers = props.row.original.feed_suppliers; - return ( - suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-' - ); - }, - footer: () => ( -
    - {allFeedSuppliers || '-'} -
    - ), - }, - { - id: 'doc_suppliers', - header: 'DOC (Supplier)', - accessorKey: 'doc_suppliers', - cell: (props) => { - const suppliers = props.row.original.doc_suppliers; - return ( - suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-' - ); - }, - footer: () => ( -
    - {allDocSuppliers || '-'} -
    - ), - }, - { - id: 'average_doc_price_rp', - header: 'Rata-Rata Harga DOC (RP)', - accessorKey: 'average_doc_price_rp', - cell: (props) => { - const value = props.row.original.average_doc_price_rp; - return
    {formatCurrency(value)}
    ; - }, - footer: () => ( -
    - {formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)} -
    - ), - }, - { - id: 'egg_hpp_rp_per_kg', - header: 'HPP Telur (RP/KG)', - accessorKey: 'egg_hpp_rp_per_kg', - cell: (props) => { - const value = props.row.original.egg_hpp_rp_per_kg; - return
    {formatCurrency(value)}
    ; - }, - footer: () => ( -
    - {formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)} -
    - ), - }, - { - id: 'egg_value_rp', - header: 'Nilai Nominal Sisa Telur (RP)', - accessorKey: 'egg_value_rp', - cell: (props) => { - const value = props.row.original.egg_value_rp; - return
    {formatCurrency(value)}
    ; - }, - footer: () => ( -
    - {formatCurrency(summaryTotal?.total_egg_value_rp || 0)} -
    - ), - }, - ]; - return tableColumns; - }; - - // ===== CUSTOM ROW RENDERER ===== - const renderCustomRow = useCallback( - (row: Row) => { - if (row.index === data.length - 1) { - const defaultRow = ( -
    - {row.getVisibleCells().map((cell) => ( - - ))} - - ); - - const customRows = [ - - - , - ]; - - if (perWeightRangeSummary.length > 0) { - perWeightRangeSummary.forEach( - (item: HppPerKandangPerWeightRange, index = 0) => { - customRows.push( - - - - - - - - - - - - - - ); - } - ); - } - - return [defaultRow, ...customRows]; - } - - return null; - }, - [data, perWeightRangeSummary] - ); - - return ( -
    - HPP Harian Kandang (${period})` - : 'Laporan > HPP Harian Kandang' - } - className={{ wrapper: 'w-full', body: 'p-1!' }} - > -
    - - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - onInputChange={setAreaInputValue} - onMenuScrollToBottom={loadMoreAreas} - isLoading={isLoadingAreas} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.location_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={locationChangeHandler} - onInputChange={setLocationInputValue} - onMenuScrollToBottom={loadMoreLocations} - isLoading={isLoadingLocations} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.kandang_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={kandangChangeHandler} - onInputChange={setKandangInputValue} - onMenuScrollToBottom={loadMoreKandangs} - isLoading={isLoadingKandangs} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> -
    - -
    -
    - - -
    - - opt.value === 'true') || - null - : showUnrecordedOptions.find((opt) => opt.value === 'false') || - null - } - onChange={showUnrecordedChangeHandler} - /> -
    - -
    - - - - Export - - - } - align='end' - > - - - - - -
    - -
    - - {!isSubmitted ? ( -
    - Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
    - ) : isLoading ? ( -
    - -
    - ) : data.length === 0 ? ( -
    - Tidak ada data yang dapat ditampilkan... -
    - ) : ( -
    - {flexRender(cell.column.columnDef.cell, cell.getContext())} -
    - Rekapitulasi per rentang bobot -
    {index + 1}ALL{item.label} - {formatNumber(item.avg_weight_kg)} - - {formatNumber(item.egg_production_pieces)} - - {formatNumber(item.egg_production_kg)} - - {item.feed_suppliers - ?.map((s) => s.alias || s.name) - .join(' | ') || '-'} - - {item.doc_suppliers - ?.map((s) => s.alias || s.name) - .join(' | ') || '-'} - - {formatCurrency(item.average_doc_price_rp)} - - {formatCurrency(item.egg_hpp_rp_per_kg)} - - {formatCurrency(item.egg_value_rp)} -
    0} - renderCustomRow={renderCustomRow} - className={{ - containerClassName: 'w-full mt-6', - tableWrapperClassName: 'overflow-x-auto mt-4', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - )} - - - ); -}; - -export default HppPerKandangTab; diff --git a/src/config/constant.ts b/src/config/constant.ts index 170a27bf..66d0af7d 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -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', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 20ee5292..dc638b29 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -195,11 +195,6 @@ export const ROUTE_PERMISSIONS: Record = { '/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'], diff --git a/src/figma-make/components/base/date-picker.tsx b/src/figma-make/components/base/date-picker.tsx index abd3414f..b19fff3d 100644 --- a/src/figma-make/components/base/date-picker.tsx +++ b/src/figma-make/components/base/date-picker.tsx @@ -46,15 +46,15 @@ export function DatePicker({ }); }; - const formatDateInput = (dateStr: string) => { - if (!dateStr) return ''; - const d = new Date(dateStr + 'T00:00:00'); - return d.toLocaleDateString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - }); - }; + // const formatDateInput = (dateStr: string) => { + // if (!dateStr) return ''; + // const d = new Date(dateStr + 'T00:00:00'); + // return d.toLocaleDateString('en-GB', { + // day: '2-digit', + // month: '2-digit', + // year: 'numeric', + // }); + // }; const displayFormatter = formatDisplay || defaultFormatDisplay; diff --git a/src/figma-make/components/base/date-range-picker.tsx b/src/figma-make/components/base/date-range-picker.tsx index d1bca47e..01b197b7 100644 --- a/src/figma-make/components/base/date-range-picker.tsx +++ b/src/figma-make/components/base/date-range-picker.tsx @@ -13,11 +13,6 @@ import { } from '@/figma-make/components/base/popover'; import { Input } from '@/figma-make/components/base/input'; -interface DateRange { - from: string; - to: string; -} - interface DateRangePickerProps { dateFrom: string; dateTo: string; diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 8cef1da7..601025ad 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -86,17 +86,18 @@ export function DailyChecklistContent() { searchParams.get('category') || '' ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); - const { - data: phases, - isLoading: isLoadingPhases, - mutate: refreshPhases, - } = useSWR< + const { data: phases } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -104,11 +105,7 @@ export function DailyChecklistContent() { keepPreviousData: true, }); - const { - data: employeesRes, - isLoading: isLoadingEmployees, - mutate: refreshEmployees, - } = useSWR( + const { data: employeesRes } = useSWR( `${EmployeeApi.basePath}?page=1&limit=500&kandang_id=${kandangId}&is_active=true`, EmployeeApi.getAllFetcher, { diff --git a/src/figma-make/components/pages/dashboard/Dashboard.tsx b/src/figma-make/components/pages/dashboard/Dashboard.tsx index 8953ccf6..a924c2b3 100644 --- a/src/figma-make/components/pages/dashboard/Dashboard.tsx +++ b/src/figma-make/components/pages/dashboard/Dashboard.tsx @@ -16,12 +16,7 @@ import { SelectValue, } from '@/figma-make/components/base/select'; import { Badge } from '@/figma-make/components/base/badge'; -import { - Calendar as CalendarIcon, - Users, - AlertCircle, - Info, -} from 'lucide-react'; +import { Users, AlertCircle, Info } from 'lucide-react'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { BarChart, @@ -71,11 +66,7 @@ export function Dashboard() { const [kandangFilter, setKandangFilter] = useState('ALL'); const [categoryFilter, setCategoryFilter] = useState('ALL'); - const { - data: summaryResponse, - isLoading: isLoadingSummary, - mutate: refreshSummary, - } = useSWR< + const { data: summaryResponse, isLoading: isLoadingSummary } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -86,11 +77,16 @@ export function Dashboard() { httpClientFetcher ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const kandangColorMap: { [key: string]: string } = {}; (kandangOptions || []).forEach((k, index) => { diff --git a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx index 634d8716..6509a91d 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -38,11 +38,6 @@ import { KandangApi } from '@/services/api/master-data'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import RequirePermission from '@/components/helper/RequirePermission'; -interface Kandang { - id: string; - name: string; -} - const STATUS_OPTIONS = [ { value: 'ALL', label: 'Semua Status' }, { value: 'DRAFT', label: 'Draft' }, @@ -98,11 +93,16 @@ export function ListDailyChecklistContent() { } ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const checklistList = isResponseSuccess(checklistListRes) ? checklistListRes.data || [] diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 846a2670..88f04a80 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -17,7 +17,7 @@ import { DialogFooter, } from '@/figma-make/components/base/dialog'; import { toast } from 'sonner'; -import { notFound, useRouter, useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; import Link from 'next/link'; @@ -90,16 +90,16 @@ interface ChecklistData { }; } -interface AssignmentQueryResult { - task_id: number; - employee_id: string; - checked: boolean; - note: string | null; - employees: { - id: number; - name: string; - } | null; -} +// interface AssignmentQueryResult { +// task_id: number; +// employee_id: string; +// checked: boolean; +// note: string | null; +// employees: { +// id: number; +// name: string; +// } | null; +// } const CATEGORY_LABELS: { [key: string]: string } = { pullet_open: 'Pullet Open', @@ -124,7 +124,7 @@ export function DetailDailyChecklistContent() { const [loading, setLoading] = useState(true); const [header, setHeader] = useState(null); - const [detailRows, setDetailRows] = useState([]); + const [, setDetailRows] = useState([]); const [phaseGroups, setPhaseGroups] = useState([]); const [employees, setEmployees] = useState<{ id: string; name: string }[]>( [] @@ -381,7 +381,7 @@ export function DetailDailyChecklistContent() { // Convert to array and group by time_type const grouped: PhaseGroup[] = []; - phaseMap.forEach((phaseData, phaseId) => { + phaseMap.forEach((phaseData) => { const timeGroups: { [timeType: string]: { activities: { @@ -570,9 +570,6 @@ export function DetailDailyChecklistContent() { return null; } - const isReadOnly = - header.status === 'APPROVED' || header.status === 'REJECTED'; - return (
    @@ -680,7 +677,7 @@ export function DetailDailyChecklistContent() { {header.status === 'REJECTED' && header.reject_reason && (
    - +
    - diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx index 33ad2608..429538a6 100644 --- a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; @@ -295,10 +295,6 @@ export function MasterConfigurationContent() { } }; - const handleExport = (format: string) => { - toast.success(`Data berhasil diekspor ke ${format}`); - }; - if (isLoadingDailyChecklistConfigurations && !dailyChecklistConfigurations) { return (
    diff --git a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx index f8b67e7a..099aa32a 100644 --- a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx +++ b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx @@ -1,15 +1,7 @@ 'use client'; import { useState } from 'react'; -import { - Plus, - Download, - ChevronDown, - MoreVertical, - Pencil, - Trash2, - Search, -} from 'lucide-react'; +import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; import { Label } from '@/figma-make/components/base/label'; @@ -93,11 +85,16 @@ export function MasterEmployeeContent() { keepPreviousData: true, } ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const [showModal, setShowModal] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -373,7 +370,7 @@ export function MasterEmployeeContent() { updateFilter('status', value === 'all' ? '' : value); }} > - + diff --git a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx index f1c32198..9c040e33 100644 --- a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx +++ b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { Card, CardContent } from '@/figma-make/components/base/card'; -import { Badge } from '@/figma-make/components/base/badge'; import { Label } from '@/figma-make/components/base/label'; import { Select, @@ -11,8 +10,6 @@ import { SelectTrigger, SelectValue, } from '@/figma-make/components/base/select'; -import { toast } from 'sonner'; -import { useRouter } from 'next/navigation'; import { useSelect } from '@/components/input/SelectInput'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; import useSWR from 'swr'; @@ -26,7 +23,6 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { cn } from '@/lib/helper'; import { ColumnDef } from '@tanstack/react-table'; -import { report } from 'process'; import { PhaseApi } from '@/services/api/daily-checklist/phase'; import { EmployeeApi } from '@/services/api/daily-checklist/employee'; import { Button } from '@/figma-make/components/base/button'; @@ -66,8 +62,6 @@ const YEAR_OPTIONS = [ // }; export function DailyChecklistReportsContent() { - const router = useRouter(); - const currentMonth = useMemo(() => new Date().getMonth() + 1, []); const currentYear = useMemo(() => new Date().getFullYear(), []); @@ -100,11 +94,7 @@ export function DailyChecklistReportsContent() { }, }); - const { - data: reportResponse, - isLoading: isLoadingReport, - mutate: refreshReport, - } = useSWR< + const { data: reportResponse, isLoading: isLoadingReport } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -116,7 +106,7 @@ export function DailyChecklistReportsContent() { } ); - const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + const { options: areaOptions } = useSelect( AreaApi.basePath, 'id', 'name', @@ -127,33 +117,53 @@ export function DailyChecklistReportsContent() { } ); - const { options: locationOptions, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', 'search', { + const { options: locationOptions } = useSelect( + LocationApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', area_id: tableFilterState.area_id, - }); + } + ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', area_id: tableFilterState.area_id, location_id: tableFilterState.location_id, - }); + } + ); - const { options: phaseOptions, isLoadingOptions: isLoadingPhases } = - useSelect(PhaseApi.basePath, 'id', 'name', 'search', { + const { options: phaseOptions } = useSelect( + PhaseApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); - const { options: employeeOptions, isLoadingOptions: isLoadingEmployees } = - useSelect(EmployeeApi.basePath, 'id', 'name', 'search', { + const { options: employeeOptions } = useSelect( + EmployeeApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '500', kandang_id: tableFilterState.kandang_id, - }); + } + ); const currentMonthMaxDay = new Date( Number(tableFilterState.tahun), diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 665c81f1..9a802f80 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -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(' '); diff --git a/src/lib/marketing-calculation.ts b/src/lib/marketing-calculation.ts index 5715ec88..5ad5a1e6 100644 --- a/src/lib/marketing-calculation.ts +++ b/src/lib/marketing-calculation.ts @@ -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); diff --git a/src/services/api/daily-checklist/daily-checklist.ts b/src/services/api/daily-checklist/daily-checklist.ts index b8f72201..2ea3991c 100644 --- a/src/services/api/daily-checklist/daily-checklist.ts +++ b/src/services/api/daily-checklist/daily-checklist.ts @@ -12,7 +12,6 @@ import { } from '@/types/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; import { toast } from 'sonner'; -import { formatDate } from '@/lib/helper'; export class DailyChecklistApiService extends BaseApiService< DailyChecklist, @@ -316,7 +315,7 @@ export class DailyChecklistApiService extends BaseApiService< wb, `laporan-daily-checklist-${params.get('tahun')}-${params.get('bulan')}.xlsx` ); - } catch (error) { + } catch { toast.error('Gagal melakukan export daily checklist! Coba lagi.'); } } diff --git a/src/services/api/expense.ts b/src/services/api/expense.ts index 2a2fb1a7..b9256506 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -1,5 +1,4 @@ import axios from 'axios'; -import { sleep } from '@/lib/helper'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse, GroupedApprovals } from '@/types/api/api-general'; import { diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index 76f9b2ea..0a34009b 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -17,12 +17,6 @@ import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; * 💡 Helper untuk membuat respons dummy * @param data Data yang akan dimasukkan ke dalam body respons */ -const createDummyResponse = (data: T): BaseApiResponse => ({ - code: 200, - status: 'success', - message: 'Data retrieved successfully (MOCK)', - data: data, -}); export class SalesOrderService extends BaseApiService< Marketing, @@ -168,7 +162,7 @@ class MarketingExportService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'marketing.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export marketing! Coba lagi.'); } } diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index f15de21d..ea33ba70 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -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, diff --git a/src/services/api/production/transfer-to-laying.ts b/src/services/api/production/transfer-to-laying.ts index 27c88536..6b4beed7 100644 --- a/src/services/api/production/transfer-to-laying.ts +++ b/src/services/api/production/transfer-to-laying.ts @@ -178,7 +178,7 @@ export class TransferToLayingService extends BaseApiService< }); return mappedFlockKandangsAvailableQty; - } catch (error) { + } catch { return undefined; } } @@ -219,7 +219,7 @@ export class TransferToLayingService extends BaseApiService< }); return mappedFlockKandangsMaxTargetQty; - } catch (error) { + } catch { return undefined; } } @@ -273,7 +273,7 @@ export class TransferToLayingService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'transfer-ke-laying.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export transfer to laying! Coba lagi.'); } } diff --git a/src/services/api/report.ts b/src/services/api/report.ts index d5061d33..5a5e06c8 100644 --- a/src/services/api/report.ts +++ b/src/services/api/report.ts @@ -1,8 +1,7 @@ import { BaseApiService } from '@/services/api/base'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { ReportExpense } from '@/types/api/report/report-expense'; -import axios from 'axios'; export class ReportExpenseApiService extends BaseApiService< ReportExpense, diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts index f55336ac..0f7c5cbb 100644 --- a/src/services/api/report/marketing-report.ts +++ b/src/services/api/report/marketing-report.ts @@ -8,7 +8,7 @@ import { DailyMarketingReport, DailyMarketingReportResponse, } from '@/types/api/report/marketing'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { formatDate } from '@/lib/helper'; export class MarketingReportApiService extends BaseApiService< @@ -68,7 +68,7 @@ export class MarketingReportApiService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); } } diff --git a/src/stores/closing/closing-tab.store.ts b/src/stores/closing/closing-tab.store.ts new file mode 100644 index 00000000..1f81c26a --- /dev/null +++ b/src/stores/closing/closing-tab.store.ts @@ -0,0 +1,21 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { + createClosingTabSlice, + ClosingTabSlice, +} from '@/stores/closing/slices/closing-tab.slice'; + +export type ClosingTabStore = ClosingTabSlice; + +export const useClosingTabStore = create()( + devtools( + (...args) => ({ + ...createClosingTabSlice(...args), + }), + { + name: 'ClosingTabStore', + } + ) +); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts new file mode 100644 index 00000000..cd47bbdc --- /dev/null +++ b/src/stores/closing/slices/closing-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ClosingTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createClosingTabSlice: StateCreator< + ClosingTabSlice, + [], + [], + ClosingTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); diff --git a/src/stores/finance-tab/finance-tab.store.ts b/src/stores/finance-tab/finance-tab.store.ts deleted file mode 100644 index 9b5cf096..00000000 --- a/src/stores/finance-tab/finance-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type FinanceTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useFinanceTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'FinanceTabStore', - } - ) -); diff --git a/src/stores/production/chickin/chickin.store.ts b/src/stores/production/chickin/chickin.store.ts new file mode 100644 index 00000000..697b1de4 --- /dev/null +++ b/src/stores/production/chickin/chickin.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; +import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; + +export type ChickinStore = ChickinApprovalSlice; + +export const useChickinStore = create()( + devtools( + (...args) => ({ + ...createChickinApprovalSlice(...args), + }), + { + name: 'ChickinStore', + } + ) +); diff --git a/src/stores/production/chickin/slices/chickin-approval.slice.ts b/src/stores/production/chickin/slices/chickin-approval.slice.ts new file mode 100644 index 00000000..30f0a857 --- /dev/null +++ b/src/stores/production/chickin/slices/chickin-approval.slice.ts @@ -0,0 +1,58 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ChickinApprovalSlice = { + // State + isChickinApproveModalOpen: boolean; + selectedChickinForApproval: ProjectFlockKandang | null; + isChickinApproveLoading: boolean; + chickinApproveCallback: ((notes?: string) => Promise) | null; + + // Actions + openChickinApproveModal: ( + data: ProjectFlockKandang, + callback: (notes?: string) => Promise + ) => void; + closeChickinApproveModal: () => void; + setChickinApproveLoading: (loading: boolean) => void; + resetChickinApproval: () => void; +}; + +export const createChickinApprovalSlice: StateCreator< + ChickinApprovalSlice, + [], + [], + ChickinApprovalSlice +> = (set) => ({ + // Initial state + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + + // Actions + openChickinApproveModal: (data, callback) => + set({ + isChickinApproveModalOpen: true, + selectedChickinForApproval: data, + chickinApproveCallback: callback, + }), + + closeChickinApproveModal: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + chickinApproveCallback: null, + }), + + setChickinApproveLoading: (loading) => + set({ isChickinApproveLoading: loading }), + + resetChickinApproval: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + }), +}); diff --git a/src/stores/production/project-flock-closing/project-flock-closing.store.ts b/src/stores/production/project-flock-closing/project-flock-closing.store.ts new file mode 100644 index 00000000..b6543b97 --- /dev/null +++ b/src/stores/production/project-flock-closing/project-flock-closing.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice'; +import { ProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice'; + +export type ProjectFlockClosingStore = ProjectFlockClosingSlice; + +export const useProjectFlockClosingStore = create()( + devtools( + (...args) => ({ + ...createProjectFlockClosingSlice(...args), + }), + { + name: 'ProjectFlockClosingStore', + } + ) +); diff --git a/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts new file mode 100644 index 00000000..ccffd387 --- /dev/null +++ b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts @@ -0,0 +1,69 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ProjectFlockClosingSlice = { + // State + isClosingModalOpen: boolean; + selectedProjectFlockKandang: ProjectFlockKandang | null; + projectFlockId: number | null; + isKandangClosed: boolean; + isClosingLoading: boolean; + closingCallback: ((action: 'close' | 'unclose') => Promise) | null; + + // Actions + openClosingModal: ( + data: ProjectFlockKandang, + projectFlockId: number, + isClosed: boolean, + callback: (action: 'close' | 'unclose') => Promise + ) => void; + closeClosingModal: () => void; + setClosingLoading: (loading: boolean) => void; + resetClosing: () => void; +}; + +export const createProjectFlockClosingSlice: StateCreator< + ProjectFlockClosingSlice, + [], + [], + ProjectFlockClosingSlice +> = (set) => ({ + // Initial state + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + isClosingLoading: false, + closingCallback: null, + + // Actions + openClosingModal: (data, projectFlockId, isClosed, callback) => + set({ + isClosingModalOpen: true, + selectedProjectFlockKandang: data, + projectFlockId, + isKandangClosed: isClosed, + closingCallback: callback, + }), + + closeClosingModal: () => + set({ + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + closingCallback: null, + }), + + setClosingLoading: (loading) => set({ isClosingLoading: loading }), + + resetClosing: () => + set({ + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + isClosingLoading: false, + closingCallback: null, + }), +}); diff --git a/src/stores/production/project-flock/project-flock.store.ts b/src/stores/production/project-flock/project-flock.store.ts new file mode 100644 index 00000000..97402132 --- /dev/null +++ b/src/stores/production/project-flock/project-flock.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createProjectFlockSlice } from '@/stores/production/project-flock/slices/project-flock.slice'; +import { ProjectFlockSlice } from '@/types/stores'; + +export type ProjectFlockStore = ProjectFlockSlice; + +export const useProjectFlockStore = create()( + devtools( + (...args) => ({ + ...createProjectFlockSlice(...args), + }), + { + name: 'ProjectFlockStore', + } + ) +); diff --git a/src/stores/production/project-flock/slices/project-flock.slice.ts b/src/stores/production/project-flock/slices/project-flock.slice.ts new file mode 100644 index 00000000..03f3205d --- /dev/null +++ b/src/stores/production/project-flock/slices/project-flock.slice.ts @@ -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, + }), +}); diff --git a/src/stores/uniformity/slices/uniformity.slice.ts b/src/stores/production/uniformity/slices/uniformity.slice.ts similarity index 100% rename from src/stores/uniformity/slices/uniformity.slice.ts rename to src/stores/production/uniformity/slices/uniformity.slice.ts diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/production/uniformity/uniformity.store.ts similarity index 80% rename from src/stores/uniformity/uniformity.store.ts rename to src/stores/production/uniformity/uniformity.store.ts index c8d759d6..740d10b6 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/production/uniformity/uniformity.store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createUniformitySlice } from '@/stores/uniformity/slices/uniformity.slice'; +import { createUniformitySlice } from '@/stores/production/uniformity/slices/uniformity.slice'; import { UniformitySlice } from '@/types/stores'; export type UniformityStore = UniformitySlice; diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts new file mode 100644 index 00000000..aad47d17 --- /dev/null +++ b/src/stores/report/report-tab.store.ts @@ -0,0 +1,21 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { + createReportTabSlice, + ReportTabSlice, +} from '@/stores/report/slices/report-tab.slice'; + +export type ReportTabStore = ReportTabSlice; + +export const useReportTabStore = create()( + devtools( + (...args) => ({ + ...createReportTabSlice(...args), + }), + { + name: 'ReportTabStore', + } + ) +); diff --git a/src/stores/report/slices/report-tab.slice.ts b/src/stores/report/slices/report-tab.slice.ts new file mode 100644 index 00000000..6582eaed --- /dev/null +++ b/src/stores/report/slices/report-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ReportTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createReportTabSlice: StateCreator< + ReportTabSlice, + [], + [], + ReportTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 31a0248d..eca09586 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,7 +1,3 @@ -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'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; diff --git a/src/types/api/daily-checklist/employee.d.ts b/src/types/api/daily-checklist/employee.d.ts index 6010dfa1..36aa1dc4 100644 --- a/src/types/api/daily-checklist/employee.d.ts +++ b/src/types/api/daily-checklist/employee.d.ts @@ -1,6 +1,4 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { BaseLocation } from '@/types/api/master-data/location'; -import { BaseUser } from '@/types/api/user'; import { BaseKandang } from '@/types/api/master-data/kandang'; export type BaseEmployee = { diff --git a/src/types/api/dashboard/dashboard.d.ts b/src/types/api/dashboard/dashboard.d.ts index 749b469a..7622155c 100644 --- a/src/types/api/dashboard/dashboard.d.ts +++ b/src/types/api/dashboard/dashboard.d.ts @@ -1,5 +1,3 @@ -import { SuccessApiResponse } from '@/types/api/api-general'; - export interface Dashboard { statistics_data: DashboardStatisticsData[]; charts: DashboardComparisonCharts | DashboardOverviewCharts; diff --git a/src/types/api/expense.d.ts b/src/types/api/expense.d.ts index 3ca57dd0..abeab48a 100644 --- a/src/types/api/expense.d.ts +++ b/src/types/api/expense.d.ts @@ -1,9 +1,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; -import { BaseLocation, Location } from '@/types/api/master-data/location'; -import { BaseKandang, Kandang } from '@/types/api/master-data/kandang'; -import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; -import { BaseNonstock, Nonstock } from '@/types/api/master-data/nonstock'; -import { BaseUser } from '@/types/api/user'; +import { BaseLocation } from '@/types/api/master-data/location'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; +import { BaseNonstock } from '@/types/api/master-data/nonstock'; export type BaseExpense = { id: number; diff --git a/src/types/api/inventory/product-warehouse.d.ts b/src/types/api/inventory/product-warehouse.d.ts index 8bed1aba..060be2ab 100644 --- a/src/types/api/inventory/product-warehouse.d.ts +++ b/src/types/api/inventory/product-warehouse.d.ts @@ -11,6 +11,7 @@ export type BaseProductWarehouse = { quantity: number; product: Product; warehouse: Warehouse; + week?: number | null; }; export type ProductWarehouse = BaseMetadata & BaseProductWarehouse; diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index f75e4060..6789dffc 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -1,5 +1,4 @@ import { BaseMetadata, CreatedUser } from '@/types/api/api-general'; -import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { Supplier } from '@/types/api/master-data/supplier'; import { Uom } from '@/types/api/master-data/uom'; @@ -38,6 +37,7 @@ export type StockLog = { id: number; increase: number; decrease: number; + stock: number; loggable_type: string; loggable_id: number; notes: string; diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 80a0b90b..a867d983 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -6,7 +6,6 @@ import { } from '@/types/api/api-general'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { Kandang } from '@/types/api/master-data/kandang'; -import { id } from 'react-day-picker/locale'; import { Warehouse } from '@/types/api/master-data/warehouse'; /** @@ -17,6 +16,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; diff --git a/src/types/api/master-data/fcr.d.ts b/src/types/api/master-data/fcr.d.ts deleted file mode 100644 index 45ad25e5..00000000 --- a/src/types/api/master-data/fcr.d.ts +++ /dev/null @@ -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; diff --git a/src/types/api/master-data/nonstock.d.ts b/src/types/api/master-data/nonstock.d.ts index e4e79d8e..834c57db 100644 --- a/src/types/api/master-data/nonstock.d.ts +++ b/src/types/api/master-data/nonstock.d.ts @@ -1,4 +1,4 @@ -import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general'; +import { BaseMetadata, flags } from '@/types/api/api-general'; import { BaseUom } from '@/types/api/master-data/uom'; import { BaseSupplier } from '@/types/api/master-data/supplier'; diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index 7fd2c7c1..c1b9b4b6 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -1,7 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Uom } from '@/types/api/master-data/uom'; import { ProductCategory } from '@/types/api/master-data/product-category'; -import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; export type BaseProduct = { id: number; diff --git a/src/types/api/production/project-flock-kandang.d.ts b/src/types/api/production/project-flock-kandang.d.ts index 111ca98b..67c3cfae 100644 --- a/src/types/api/production/project-flock-kandang.d.ts +++ b/src/types/api/production/project-flock-kandang.d.ts @@ -1,7 +1,6 @@ import { Kandang } from '@/type/master-data/kandang'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApproval } from '@/types/api/api-general'; export type BaseProjectFlockKandang = { diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 34889bf8..172c24b5 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -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[]; diff --git a/src/types/api/production/transfer-to-laying.d.ts b/src/types/api/production/transfer-to-laying.d.ts index c162ed82..8123e9e6 100644 --- a/src/types/api/production/transfer-to-laying.d.ts +++ b/src/types/api/production/transfer-to-laying.d.ts @@ -1,10 +1,5 @@ -import { - BaseApiResponse, - BaseMetadata, - CreatedUser, - flags, -} from '@/types/api/api-general'; -import { BaseKandang, Kandang } from '@/types/api/master-data/kandang'; +import { BaseMetadata, CreatedUser } from '@/types/api/api-general'; +import { BaseKandang } from '@/types/api/master-data/kandang'; import { WarehouseType } from '@/types/api/master-data/warehouse'; export type BaseTransferToLaying = { diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 60004ae0..d39719a3 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -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[]; }; diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts index 20c60725..38797668 100644 --- a/src/types/api/report/marketing.d.ts +++ b/src/types/api/report/marketing.d.ts @@ -1,13 +1,10 @@ import { BaseApiResponse, BaseMetadata } from '@/types/api/api-general'; -import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; +import { BaseCustomer } from '@/types/api/master-data/customer'; import { BaseWarehouseArea, BaseWarehouseKandang, BaseWarehouseLocation, - Warehouse, } from '@/types/api/master-data/warehouse'; -import { Location } from '@/types/api/master-data/location'; -import { Area } from '@/types/api/master-data/area'; import { BaseProduct } from '@/types/api/master-data/product'; import { BaseUser } from '@/types/api/user'; diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index 3918820d..bf9b94eb 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -1,6 +1,5 @@ import { BaseApproval, CreatedUser } from '@/types/api/api-general'; import { Supplier } from '@/types/api/master-data/supplier'; -import { Location } from '@/types/api/master-data/location'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { Kandang } from '@/types/api/master-data/kandang'; diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 47d2c1fd..c358e2a1 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -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; +};
    Nama Aktivitas + Aksi