diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx index bffe1707..aabf48de 100644 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -96,11 +96,6 @@ const ClosingProductionDataTabContent = ({ value={formatNumber(purchase.feed_used)} unit='Kg' /> - @@ -124,14 +119,12 @@ const ClosingProductionDataTabContent = ({ /> @@ -148,17 +141,17 @@ const ClosingProductionDataTabContent = ({ /> @@ -191,17 +184,37 @@ const ClosingProductionDataTabContent = ({ /> + + + + - + + {/* Laying Specific Fields */} + {performance.hen_day_act !== undefined && ( + <> + + + + )} + + {performance.egg_mass !== undefined && ( + <> + + + + )} + + {performance.egg_weight !== undefined && ( + <> + + + + )} + + {performance.hen_housed_act !== undefined && ( + <> + + + + )} diff --git a/src/components/pages/finance/FinanceDetail.tsx b/src/components/pages/finance/FinanceDetail.tsx index c7057efa..03291420 100644 --- a/src/components/pages/finance/FinanceDetail.tsx +++ b/src/components/pages/finance/FinanceDetail.tsx @@ -9,6 +9,7 @@ import Table from '@/components/Table'; import { FINANCE_INITIAL_BALANCE_STATUS, FINANCE_TRANSACTION_STATUS, + FINANCE_INJECTION_STATUS, } from '@/config/constant'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { FinanceApi } from '@/services/api/finance'; @@ -33,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { }, { label: 'Pihak', - value: finance.party.name, + value: finance.party.id ? finance.party.name : '-', }, { label: 'Tanggal', @@ -51,7 +52,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { const informasiTransfer = [ { label: 'No. Referensi', - value: finance.reference_number, + value: finance.reference_number ?? '-', }, { label: 'Nomor Rekening', @@ -69,7 +70,16 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { label: 'Sisa', value: formatCurrency(finance.income_amount), }, - ]; + ].filter((item) => { + // Hide party account number row if transaction type is INJECTION + if ( + FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && + item.label === `Rekening ${formatTitleCase(finance.party.type)}` + ) { + return false; + } + return true; + }); const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -162,7 +172,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => { )} - + {FINANCE_INJECTION_STATUS.includes(finance.transaction_type) && ( + + + + )} + + {delivery.document_path ? ( + + ) : delivery.document && + delivery.document instanceof File === false ? ( + + ) : ( + + )} ) : ( diff --git a/src/components/pages/marketing/detail/MarketingDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx index 4c9f1a34..677ea422 100644 --- a/src/components/pages/marketing/detail/MarketingDetail.tsx +++ b/src/components/pages/marketing/detail/MarketingDetail.tsx @@ -77,10 +77,6 @@ const MarketingDetail = ({ confirmationModal.openModal(); }; - const deliveryClickHandler = () => { - deliveryModal.openModal(); - }; - const deleteClickHandler = () => { deleteModal.openModal(); }; @@ -135,7 +131,7 @@ const MarketingDetail = ({
{initialValues?.latest_approval?.step_number == 1 && ( <> - {/* + - */} - + - {/* + - */} - + )} {initialValues?.latest_approval?.step_number != 1 && ( <> - {/* - */} - + )}
@@ -466,7 +426,7 @@ const MarketingDetail = ({
{initialValues?.latest_approval?.step_number != 3 && ( <> - {/* + - */} - + )} - {/* + - */} - +
- {/*
- {JSON.stringify(formik.values)} -
-
- {JSON.stringify(formik.errors)} -
*/} - {/* + - */} - + )} diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index 75aa3ba6..aa9dd9e8 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -11,7 +11,7 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { Kandang } from '@/types/api/master-data/kandang'; -import { KandangApi, WarehouseApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouseApi } from '@/services/api/inventory'; import NumberInput from '@/components/input/NumberInput'; @@ -180,9 +180,6 @@ const SalesOrderProductForm = ({ )} - {/* - {JSON.stringify(formik.errors)} - */}
{ const cols = [ - // { - // id: 'select', - // header: ({ - // table, - // }: { - // table: TanStack.Table; - // }) => ( - //
- // - //
- // ), - // cell: ({ - // row, - // }: { - // row: TanStack.Row; - // }) => ( - //
- // - //
- // ), - // }, { accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number, header: 'No. Pengiriman', @@ -188,18 +156,6 @@ const DeliveryOrderProductTable = ({ )} {!props.row.original.qty && '-'} - {/* {formType == 'add_deliver' && ( - - )} */}
), @@ -248,22 +204,6 @@ const DeliveryOrderProductTable = ({ Tambah Pengiriman - {/* {selectedRowIds.length > 0 && ( - - )} */} ); diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index e5bd30cb..39b17ef7 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -872,7 +872,7 @@ const RecordingTable = () => { 'mb-20': isResponseSuccess(recordings) && recordings?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full overflow-visible!', + 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: diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 1b58b16c..77d1608d 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,91 +1,93 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; +import { + UniformityDetailItem, + Uniformity, +} from '@/types/api/production/uniformity'; -interface BarChartData { - name: string; - uv: number; +interface UniformityChartProps { + uniformityData?: Uniformity | null; + uniformityDetails?: UniformityDetailItem[]; } -interface GaugeChartData { - value: number; - label: string; - kandang?: string; - week?: string; - currentValue?: number; - totalValue?: number; -} - -const UniformityChart = () => { - // TODO: Replace with actual API call - const barChartData: BarChartData[] = [ - { - name: '48-52', - uv: 80, - }, - { - name: '52-56', - uv: 120, - }, - { - name: '56-60', - uv: 160, - }, - { - name: '60-64', - uv: 200, - }, - { - name: '64-68', - uv: 160, - }, - { - name: '68-72', - uv: 120, - }, - { - name: '72-76', - uv: 80, - }, - { - name: '76-80', - uv: 120, - }, - { - name: '84-88', - uv: 160, - }, - { - name: '88-92', - uv: 200, - }, - { - name: '92-96', - uv: 160, - }, +const UniformityChart = ({ + uniformityData, + uniformityDetails, +}: UniformityChartProps) => { + const defaultUniformityDetails: UniformityDetailItem[] = [ + { id: 1, weight: 61, range: 'Ideal' }, + { id: 2, weight: 62, range: 'Ideal' }, + { id: 3, weight: 63, range: 'Ideal' }, + { id: 4, weight: 64, range: 'Ideal' }, + { id: 5, weight: 65, range: 'Ideal' }, + { id: 6, weight: 66, range: 'Ideal' }, + { id: 7, weight: 67, range: 'Ideal' }, ]; - // TODO: Replace with actual API call - // const gaugeChartData: GaugeChartData = { - // value: 0, - // label: '', - // kandang: 'Kandang Cirangga', - // week: 'Week 2', - // currentValue: 512, - // totalValue: 1024, - // }; + const detailsToUse = uniformityDetails || defaultUniformityDetails; - const gaugeChartData: GaugeChartData = { - value: 52, - label: 'Uniformity', - kandang: 'Kandang Cirangga', - week: 'Week 2', - currentValue: 512, - totalValue: 1024, - }; + const barChartData = useMemo(() => { + if (!uniformityData) { + return []; + } + + if (!detailsToUse || detailsToUse.length === 0) { + return []; + } + + const weights = detailsToUse.map((d) => d.weight); + const minWeight = Math.floor(Math.min(...weights) / 5) * 5; + const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5; + + const rangeSize = maxWeight - minWeight < 11 ? 4 : 5; + const ranges: string[] = []; + + for (let start = minWeight; start <= maxWeight; start += rangeSize) { + const end = start + rangeSize; + ranges.push(`${start}-${end}`); + } + + const totalIdealCount = detailsToUse.filter( + (d) => d.range === 'Ideal' + ).length; + + return ranges.map((range) => { + const [minStr, maxStr] = range.split('-').map(Number); + const min = minStr; + const max = maxStr; + + const birdsInRange = detailsToUse.filter( + (d) => d.weight >= min && d.weight < max + ).length; + + const hasIdeal = detailsToUse.some( + (d) => d.range === 'Ideal' && d.weight >= min && d.weight < max + ); + + return { + name: range, + uv: birdsInRange, + isIdeal: hasIdeal, + idealCount: hasIdeal ? totalIdealCount : undefined, + }; + }); + }, [uniformityData, detailsToUse]); + + const gaugeChartData = useMemo(() => { + if (!uniformityData) return undefined; + + return { + value: uniformityData.uniformity, + label: 'Uniformity', + week: `Week ${uniformityData.week}`, + currentValue: uniformityData.uniform_qty, + totalValue: uniformityData.chick_qty_of_weight, + }; + }, [uniformityData]); return (
@@ -98,14 +100,14 @@ const UniformityChart = () => { }} >
- {barChartData.length === 0 ? ( + {!uniformityData || barChartData.length === 0 ? ( ) : ( )}
- {gaugeChartData.value === 0 ? ( + {!uniformityData || !gaugeChartData ? ( {
- !isOpen && router.push('/production/uniformity')} - /> +
{ - // Uniformity data is never locked - checkbox is always enabled - return false; -}; - -const canApproveRejectUniformity = (uniformity: Uniformity): boolean => { - return uniformity.status === 'CREATED' || uniformity.status === 'Pengajuan'; -}; - -interface UniformityPreviewData { - id: string; - label: string; - value: string; -} - const UniformityConfirmationPreview = ({ uniformity, }: { uniformity?: Uniformity; }) => { - const data: UniformityPreviewData[] = [ + const data: DetailOptionType[] = [ { id: 'tanggal', label: 'Tanggal', @@ -91,7 +80,7 @@ const UniformityConfirmationPreview = ({ { id: 'file-uniformity', label: 'File Uniformity', - value: '-', // File name tidak tersedia di GET ALL response + value: '-', }, { id: 'status', @@ -100,7 +89,7 @@ const UniformityConfirmationPreview = ({ }, ]; - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { accessorKey: 'label', header: 'Label', @@ -148,7 +137,52 @@ const UniformityConfirmationPreview = ({ ); }; -const UniformityTable = ({ refresh }: { refresh?: () => void }) => { +const UniformityChartWrapper = ({ + uniformitySwrKey, +}: { + uniformitySwrKey: string; +}) => { + const { data: uniformities } = useSWR( + uniformitySwrKey, + UniformityApi.getAllFetcher + ); + + const uniformityData = useMemo(() => { + if (isResponseSuccess(uniformities) && uniformities?.data?.length > 0) { + return uniformities.data[0]; + } + return null; + }, [uniformities]); + + const shouldFetchDetails = !!uniformityData; + const uniformityDetailSwrKey = useMemo(() => { + if (!uniformityData) return null; + return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`; + }, [uniformityData]); + + const { data: uniformityDetailResponse } = useSWR( + uniformityDetailSwrKey, + shouldFetchDetails ? UniformityApi.getAllFetcher : null + ); + + const uniformityDetails = useMemo(() => { + if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { + const detailData = + uniformityDetailResponse.data as unknown as UniformityDetail; + return detailData.uniformity_details; + } + return undefined; + }, [shouldFetchDetails, uniformityDetailResponse]); + + return ( + + ); +}; + +const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); const isSuccess = useUniformityStore((s) => s.isSuccess); @@ -355,6 +389,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { mutate: refreshUniformities, } = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher); + useEffect(() => { + if (isSuccess) { + refreshUniformities(); + } + }, [isSuccess, refreshUniformities]); + // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -836,7 +876,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
- +
+

Uniformity 2025

+
+
+
+ {chartData.idealCount} of Birds +
+ {labelStr} +
+ + ); + } + return (

Uniformity 2025

@@ -105,9 +126,6 @@ const UniformityBarChart: React.FC = ({ data }) => { = ({ data }) => { radius={[25, 25, 0, 0]} /> } - /> + > + {data.map((entry, index) => ( + + ))} + ); diff --git a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx index eda3d0ab..04fbef9c 100644 --- a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx @@ -1,25 +1,29 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import Card from '@/components/Card'; -import { Icon } from '@iconify/react'; import { formatNumber } from '@/lib/helper'; +import { Icon } from '@iconify/react'; interface UniformityGaugeChartProps { value: number; label: string; - kandang?: string; week?: string; currentValue?: number; totalValue?: number; + onWeekChange?: (direction: 'prev' | 'next') => void; + hasPrevWeek?: boolean; + hasNextWeek?: boolean; } const UniformityGaugeChart: React.FC = ({ value, label, - kandang, week, currentValue, totalValue, + onWeekChange, + hasPrevWeek = false, + hasNextWeek = false, }) => { const numberOfSegments = 50; const filledSegments = Math.round((value / 100) * numberOfSegments); @@ -34,7 +38,7 @@ const UniformityGaugeChart: React.FC = ({ const inactiveColor = '#f0f0f0'; return ( -
+
@@ -73,34 +77,49 @@ const UniformityGaugeChart: React.FC = ({
- -
-
- -
-
-
- {kandang} - - - {week} - +
+ + +
+
+
+ + {week} + +
+
+ + {formatNumber(currentValue ?? 0)} + + From + + {formatNumber(totalValue ?? 0)} + +
-
- - {formatNumber(currentValue ?? 0)} - - From - {formatNumber(totalValue ?? 0)} -
-
-
-
+
+ + + ); }; diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index 0cc39d9a..4d0cd887 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -119,11 +119,13 @@ const UniformityDetail: React.FC = ({ const statusValue = latest_approval?.action ?? '-'; const valueMap: Record = { - tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'), - 'lokasi-farm': info_umum.lokasi_farm, - 'project-flock': info_umum.project_flock, - kandang: info_umum.kandang, - 'document-name': info_umum.file_name, + tanggal: info_umum?.tanggal + ? formatDate(info_umum.tanggal, 'DD MMMM YYYY') + : '-', + 'lokasi-farm': info_umum?.lokasi_farm ?? '-', + 'project-flock': info_umum?.project_flock ?? '-', + kandang: info_umum?.kandang ?? '-', + 'document-name': info_umum?.file_name ?? '-', 'approval-status': statusValue, }; diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index d98bba23..21be03d7 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -229,49 +229,52 @@ const UniformityDetailsPreview = ({ {/* Form Section */}
- {uniformity_details && uniformity_details.length > 0 ? ( + {info_umum || sampling || result ? (
{/* Sampling and Range */} -
-

Sampling and Range

- - data={samplingTableData} - columns={columnsSampling} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
- {/* Result */} -
-

Result

- - data={resultTableData} - columns={resultColumns} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
+ {sampling && ( +
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} - {/* Body Weight Details Button */} -
- -
- {/*{!uniformity_details || uniformity_details.length === 0 ? ( - <> - ) : null}*/} + {/* Result */} + {result && ( +
+

Result

+ + data={resultTableData} + columns={resultColumns} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} + + {!uniformity_details || uniformity_details.length === 0 ? ( +
+ +
+ ) : null} {/* Body Weight Details */} {uniformity_details && uniformity_details.length > 0 && ( diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 2daac10d..bbca72f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -97,12 +97,16 @@ const UniformityForm = ({ setInputValue: setLocationSelectInputValue, options: locationOptions, isLoadingOptions: isLoadingLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + }); // ===== FETCH PROJECT FLOCKS DATA ===== const projectFlocksUrl = useMemo(() => { const params = new URLSearchParams({ search: projectFlockSearchValue || '', + page: '1', limit: '100', }); if (selectedLocation) { @@ -141,6 +145,7 @@ const UniformityForm = ({ const approvedProjectFlockKandangsUrl = useMemo(() => { const params = new URLSearchParams({ step_name: 'Disetujui', + page: '1', limit: '100', }); return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 02ec70c1..436fab9a 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -28,7 +28,7 @@ const UniformityGaugeChartSkeleton: React.FC< const inactiveColor = '#f0f0f0'; return ( -
+
diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d6ef5952..b90a7c91 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -52,9 +52,14 @@ const PurchaseOrderAcceptApprovalForm = ({ const searchParams = useSearchParams(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [key, setKey] = useState(0); const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + useEffect(() => { + setKey((prev) => prev + 1); + }, [initialValues?.id]); + // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -164,6 +169,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validationSchema: PurchaseRequestAcceptApprovalFormSchema, validateOnChange: true, validateOnBlur: true, + enableReinitialize: false, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { action: 'APPROVED', @@ -238,7 +244,12 @@ const PurchaseOrderAcceptApprovalForm = ({ travel_number: item.travel_number || '', travel_document_path: item.travel_document_path || '', vehicle_number: item.vehicle_number || '', - expedition_vendor: null, + expedition_vendor: item.expedition_vendor + ? { + value: item.expedition_vendor.id, + label: item.expedition_vendor.name, + } + : null, expedition_vendor_id: item.expedition_vendor_id || 0, received_qty: item.total_qty || '', transport_per_item: item.transport_per_item || '', @@ -246,7 +257,7 @@ const PurchaseOrderAcceptApprovalForm = ({ }); formik.setFieldValue('items', updatedItems); } - }, [purchaseItems, initialValues]); + }, [purchaseItems, initialValues, key]); useEffect(() => { if ( @@ -336,7 +347,11 @@ const PurchaseOrderAcceptApprovalForm = ({ }; return ( -
+

{type === 'add' @@ -699,7 +714,9 @@ const PurchaseOrderAcceptApprovalForm = ({ color='warning' className='px-4' onClick={() => { - formik.resetForm(); + if (type === 'add') { + formik.resetForm(); + } setPurchaseOrderFormErrorMessage(''); onCancel?.(); onModalClose?.(); diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index 6fc8f7ea..ae6f744b 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -24,6 +24,7 @@ import { import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { isResponseError } from '@/lib/api-helper'; import Pagination from '@/components/Pagination'; +import { ProductionResultReportApi } from '@/services/api/report/production-result'; const ProductionResultContent = () => { const [projectFlockKandangs, setProjectFlockKandangs] = useState< @@ -145,8 +146,11 @@ const ProductionResultContent = () => { const exportToExcelHandler = async () => { setIsLoadingExportingToExcel(true); - // TODO: Implement export functionality in API service first if needed - toast.error('Fitur export belum tersedia'); + + await ProductionResultReportApi.exportProductionResultToExcel( + projectFlockKandangs + ); + setIsLoadingExportingToExcel(false); }; @@ -319,7 +323,13 @@ const ProductionResultContent = () => { align='end' direction='bottom' trigger={ -

diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 101dbb6d..7c256795 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -75,8 +75,13 @@ export const ROUTE_PERMISSIONS: Record = { '/expense/realization/edit/': ['lti.expense.update.realization'], // Finance - '/finance/': ['lti.finance.transaction.list'], - '/finance/detail/': ['lti.finance.transaction.detail'], + '/finance/': ['lti.finance.transactions.list'], + '/finance/detail/': [ + 'lti.finance.transactions.detail', + 'lti.finance.initial_balances.detail', + 'lti.finance.injections.detail', + 'lti.finance.payments.detail', + ], '/finance/add/': ['lti.finance.payments.create'], '/finance/detail/edit/': ['lti.finance.payments.update'], '/finance/add/initial-balance/': ['lti.finance.initial_balances.create'], @@ -94,10 +99,7 @@ export const ROUTE_PERMISSIONS: Record = { '/report/logistic-stock/': ['lti.repport.purchasesupplier.list'], '/report/expense/': ['lti.repport.expense.list'], '/report/marketing/': ['lti.repport.delivery.list'], - - // TODO: change to real permission - // '/report/production-result/': ['lti.repport.production_result.list'], - '/report/production-result/': ['lti.repport.delivery.list'], + '/report/production-result/': ['lti.repport.production_result.list'], // Inventory '/inventory/adjustment/': ['lti.inventory.list'], diff --git a/src/services/api/inventory.ts b/src/services/api/inventory.ts index fa406917..70a7c8f9 100644 --- a/src/services/api/inventory.ts +++ b/src/services/api/inventory.ts @@ -1,4 +1,5 @@ import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; import { CreateProductWarehousePayload, ProductWarehouse, @@ -20,11 +21,38 @@ export const ProductWarehouseApi = new BaseApiService< UpdateProductWarehousePayload >('/inventory/product-warehouses'); -export const MovementApi = new BaseApiService< +export class MovementApiService extends BaseApiService< Movement, CreateMovementPayload, unknown ->('/inventory/transfers'); +> { + constructor(basePath: string) { + super(basePath); + } + + async createMovement( + payload: CreateMovementPayload + ): Promise | undefined> { + const formData = new FormData(); + + // Append data as JSON string + formData.append('data', JSON.stringify(payload.data)); + + // Append documents if any + if (payload.documents && payload.documents.length > 0) { + payload.documents.forEach((file) => { + formData.append('documents', file); + }); + } + + return await this.customRequest>('', { + method: 'POST', + payload: formData as unknown as Record, + }); + } +} + +export const MovementApi = new MovementApiService('/inventory/transfers'); export const InventoryAdjustmentApi = new BaseApiService< InventoryAdjustment, diff --git a/src/services/api/report/production-result.ts b/src/services/api/report/production-result.ts index 98997e9b..251e3eb4 100644 --- a/src/services/api/report/production-result.ts +++ b/src/services/api/report/production-result.ts @@ -1,145 +1,12 @@ -import { sleep } from '@/lib/helper'; +import * as XLSX from 'xlsx'; +import toast from 'react-hot-toast'; +import { formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; import { BaseApiService } from '@/services/api/base'; -import { httpClientFetcher } from '@/services/http/client'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { ProductionResult } from '@/types/api/report/production-result'; - -// TODO: delete this dummy data -const PRODUCTION_RESULT_DUMMY_DATA: BaseApiResponse = { - code: 200, - status: 'success', - message: 'Get Laporan Hasil Produksi successfully', - meta: { - page: 1, - limit: 1, - total_pages: 2, - total_results: 2, - }, - data: [ - { - created_user: { - id: 1, - id_user: 1001, - email: 'user@example.com', - name: 'John Doe', - }, - project_flock: { - id: 1, - name: 'PROJECT', - category: 'LAYING', - kandang: { - id: 1, - name: 'Cikaum', - }, - }, - created_at: '2025-01-01T08:00:00Z', - updated_at: '2025-01-02T10:30:00Z', - - woa: 25, - - bw: 62.5, - std_bw: 60, - uniformity: 88, - std_uniformity: '90% up', - - dep_kum: 3.2, - dep_std: 2.5, - - butiran_utuh: 850, - butiran_putih: 50, - butiran_retak: 70, - butiran_pecah: 30, - butiran_jumlah: 1000, - total_butir: 1000, - - kg_utuh: 52.3, - kg_putih: 3.1, - kg_retak: 4.2, - kg_pecah: 1.9, - kg_jumlah: 61.5, - total_kg: 61.5, - - persen_utuh: 85, - persen_putih: 5, - persen_retak: 7, - persen_pecah: 3, - - hd: 92, - hd_std: 90, - fi: 115, - fi_std: 667, - em: 85, - em_std: 83, - ew: 62, - ew_std: 60, - fcr: 2.1, - fcr_std: 2.0, - hh: 96, - hh_std: 95, - }, - { - created_user: { - id: 1, - id_user: 1001, - email: 'user@example.com', - name: 'John Doe', - }, - project_flock: { - id: 1, - name: 'PROJECT', - category: 'LAYING', - kandang: { - id: 1, - name: 'Cikaum', - }, - }, - created_at: '2025-01-01T08:00:00Z', - updated_at: '2025-01-02T10:30:00Z', - - woa: 25, - - bw: 62.5, - std_bw: 60, - uniformity: 88, - std_uniformity: '90% up', - - dep_kum: 3.2, - dep_std: 2.5, - - butiran_utuh: 850, - butiran_putih: 50, - butiran_retak: 70, - butiran_pecah: 30, - butiran_jumlah: 1000, - total_butir: 1000, - - kg_utuh: 52.3, - kg_putih: 3.1, - kg_retak: 4.2, - kg_pecah: 1.9, - kg_jumlah: 61.5, - total_kg: 61.5, - - persen_utuh: 85, - persen_putih: 5, - persen_retak: 7, - persen_pecah: 3, - - hd: 92, - hd_std: 90, - fi: 115, - fi_std: 110, - em: 85, - em_std: 83, - ew: 62, - ew_std: 60, - fcr: 2.1, - fcr_std: 2.0, - hh: 96, - hh_std: 95, - }, - ], -}; +import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; export class ProductionResultReportApiService extends BaseApiService< ProductionResult, @@ -153,14 +20,117 @@ export class ProductionResultReportApiService extends BaseApiService< async getAllProductionResultFetcher( endpoint: string ): Promise> { - // return await httpClientFetcher>( - // endpoint - // ); + return await httpClientFetcher>( + endpoint + ); + } - await sleep(1000); + async exportProductionResultToExcel( + projectFlockKandangs: BaseProjectFlockKandang[] | null + ) { + try { + const mappedProductionResults: { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; + }[] = []; - return PRODUCTION_RESULT_DUMMY_DATA; + projectFlockKandangs?.forEach(async (projectFlockKandang) => { + const getProductionResultPath = `${this.basePath}/${projectFlockKandang.id}?page=1&limit=99999999`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + mappedProductionResults.push({ + projectFlockKandang, + productionResult: isResponseSuccess(getProductionResultRes) + ? getProductionResultRes.data + : null, + }); + }); + + const rows = mappedProductionResults; + if (!rows || rows.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + return; + } + + // Group by Project Flock Kandang Name + const groupedData: Record< + string, + Record[] + > = {}; + + rows.forEach((row) => { + const kandangName = row.projectFlockKandang.kandang.name || 'Unknown'; + if (!groupedData[kandangName]) { + groupedData[kandangName] = []; + } + + row.productionResult?.forEach((productionResult) => { + groupedData[kandangName].push({ + woa: productionResult.woa, + bw: productionResult.bw, + std_bw: productionResult.std_bw, + uniformity: productionResult.uniformity, + std_uniformity: productionResult.std_uniformity, + dep_kum: productionResult.dep_kum, + dep_std: productionResult.dep_std, + butiran_utuh: productionResult.butiran_utuh, + butiran_putih: productionResult.butiran_putih, + butiran_retak: productionResult.butiran_retak, + butiran_pecah: productionResult.butiran_pecah, + butiran_jumlah: productionResult.butiran_jumlah, + total_butir: productionResult.total_butir, + kg_utuh: productionResult.kg_utuh, + kg_putih: productionResult.kg_putih, + kg_retak: productionResult.kg_retak, + kg_pecah: productionResult.kg_pecah, + kg_jumlah: productionResult.kg_jumlah, + total_kg: productionResult.total_kg, + persen_utuh: productionResult.persen_utuh, + persen_putih: productionResult.persen_putih, + persen_retak: productionResult.persen_retak, + persen_pecah: productionResult.persen_pecah, + hd: productionResult.hd, + hd_std: productionResult.hd_std, + fi: productionResult.fi, + fi_std: productionResult.fi_std, + em: productionResult.em, + em_std: productionResult.em_std, + ew: productionResult.ew, + ew_std: productionResult.ew_std, + fcr: productionResult.fcr, + fcr_std: productionResult.fcr_std, + hh: productionResult.hh, + hh_std: productionResult.hh_std, + project_flock_name: productionResult.project_flock.name, + project_flock_category: productionResult.project_flock.category, + kandang_name: productionResult.project_flock.kandang.name, + created_at: formatDate(productionResult.created_at, 'YYYY-MM-DD'), + updated_at: formatDate(productionResult.updated_at, 'YYYY-MM-DD'), + }); + }); + }); + + const wb = XLSX.utils.book_new(); + + Object.keys(groupedData).forEach((sheetName) => { + const ws = XLSX.utils.json_to_sheet(groupedData[sheetName]); + // Sheet names cannot exceed 31 chars + const safeSheetName = sheetName.substring(0, 31); + XLSX.utils.book_append_sheet(wb, ws, safeSheetName); + }); + + const productionResultExcelFileName = `laporan-hasil-produksi-${formatDate(Date.now(), 'YYYY-MM-DD')}-${rows[0].projectFlockKandang.project_flock.flock_name}.xlsx`; + + XLSX.writeFile(wb, productionResultExcelFileName); + } catch (error) { + console.error(error); + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } } } -export const ProductionResultReportApi = new ProductionResultReportApiService(); +export const ProductionResultReportApi = new ProductionResultReportApiService( + '/reports/production-result' +); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index f96f1149..95d1526d 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -112,34 +112,42 @@ export type ClosingProductionData = { final_population: number; feed_in: number; feed_used: number; - feed_used_per_head: number; }; - sales: { chicken: { sales_population: number; sales_weight: number; - average_weight: number; - chicken_average_selling_price: number; + avg_weight: number; + avg_selling_price: number; }; egg?: { egg_pieces: number; - egg_mass_kg: number; - average_egg_weight_kg: number; - egg_average_selling_price: number; + egg_mass: number; + avg_egg_weight: number; + avg_selling_price: number; }; }; - performance: { depletion: number; age_day: number; - mortality_std: number; - mortality_act: number; - deff_mortality: number; - fcr_std: number; + mor_std: number; + mor_act: number; + mor_diff: number; + awg_act: number; + awg_std: number; + feed_intake: number; + feed_intake_std: number; fcr_act: number; - deff_fcr: number; - awg: number; + fcr_std: number; + fcr_diff: number; + hen_day_act?: number; + hen_day_std?: number; + egg_mass?: number; + egg_mass_std?: number; + egg_weight?: number; + egg_weight_std?: number; + hen_housed_act?: number; + hen_housed_std?: number; }; }; diff --git a/src/types/api/inventory/movement.d.ts b/src/types/api/inventory/movement.d.ts index 53dfa61d..2f6caceb 100644 --- a/src/types/api/inventory/movement.d.ts +++ b/src/types/api/inventory/movement.d.ts @@ -14,6 +14,14 @@ type MovementWarehouse = { }; }; +export type MovementDocument = { + id: number; + path: string; + name: string; + ext: string; + size: number; +}; + export type BaseMovement = { id: number; transfer_reason: string; @@ -39,6 +47,7 @@ export type BaseMovement = { document_path: string; shipping_cost_item: number; shipping_cost_total: number; + document?: MovementDocument; items: { id: number; stock_transfer_detail_id: number; @@ -49,7 +58,7 @@ export type BaseMovement = { export type Movement = BaseMetadata & BaseMovement; -export type CreateMovementPayload = { +export type CreateMovementPayloadData = { transfer_reason: string; transfer_date: string; source_warehouse_id: number; @@ -71,3 +80,8 @@ export type CreateMovementPayload = { }[]; }[]; }; + +export type CreateMovementPayload = { + data: CreateMovementPayloadData; + documents?: File[]; +};