diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx new file mode 100644 index 00000000..691ea734 --- /dev/null +++ b/src/app/report/production-result/page.tsx @@ -0,0 +1,11 @@ +import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent'; + +const ProductionResultReportPage = () => { + return ( +
+ +
+ ); +}; + +export default ProductionResultReportPage; diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/ClosingGeneralInformationTable.tsx index af21497a..6d5b52b2 100644 --- a/src/components/pages/closing/ClosingGeneralInformationTable.tsx +++ b/src/components/pages/closing/ClosingGeneralInformationTable.tsx @@ -25,9 +25,9 @@ const ClosingGeneralInformationTable = ({ {initialValue?.period} - Kategori + Project Flock : - {initialValue?.project_category} + {initialValue?.project_flock?.name} Populasi diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index e6574d4f..0cacb549 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -126,28 +126,6 @@ const ClosingsTable = () => { accessorKey: 'shed_label', header: 'Jumlah Kandang', }, - { - accessorKey: 'sales_paid_amount', - header: 'Jumlah Sudah Bayar', - cell: (props) => ( - - {formatCurrency(props.row.original.sales_paid_amount)} - - ), - }, - { - accessorKey: 'sales_remaining_amount', - header: 'Jumlah Sisa Bayar', - cell: (props) => ( - - {formatCurrency(props.row.original.sales_remaining_amount)} - - ), - }, - { - accessorKey: 'sales_payment_status', - header: 'Status Pembayaran', - }, { accessorKey: 'project_status', header: 'Status', diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 30013001..4901b349 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -12,11 +12,6 @@ type RecordingGrowingFormSchemaType = { label: string; } | null; project_flock_kandang_id: number; - body_weights: { - weight: number | string; - avg_weight: number | string; - qty: number | string; - }[]; stocks: { product_warehouse_id: number; qty: number | string; @@ -35,12 +30,6 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & { }[]; }; -export type BodyWeightSchema = { - weight: number | string; - avg_weight: number | string; - qty: number | string; -}; - export type StockSchema = { product_warehouse_id: number; qty: number | string; @@ -57,20 +46,6 @@ export type EggSchema = { weight: number | string; }; -const BodyWeightObjectSchema: Yup.ObjectSchema = Yup.object({ - weight: Yup.number() - .required('Berat ayam total wajib diisi!') - .min(1, 'Berat ayam total minimal 1 gram!') - .typeError('Berat ayam total harus berupa angka!'), - avg_weight: Yup.number() - .required('Berat ayam rata-rata wajib diisi!') - .typeError('Berat ayam rata-rata harus berupa angka!'), - qty: Yup.number() - .required('Jumlah ayam wajib diisi!') - .min(1, 'Jumlah ayam minimal 1 ekor!') - .typeError('Jumlah ayam harus berupa angka!'), -}); - const StockObjectSchema: Yup.ObjectSchema = Yup.object({ product_warehouse_id: Yup.number() .required('Produk wajib diisi!') @@ -140,10 +115,6 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema; type RecordingFormData = Partial & { - body_weights?: CreateGrowingRecordingPayload['body_weights']; stocks?: CreateGrowingRecordingPayload['stocks'] | Recording['stocks']; depletions?: | CreateGrowingRecordingPayload['depletions'] @@ -216,19 +186,6 @@ export const getRecordingGrowingFormInitialValues = ( } : null, project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, - body_weights: initialValues?.body_weights?.map( - (bw: NonNullable[0]) => ({ - weight: bw.avg_weight * bw.qty, - avg_weight: bw.avg_weight, - qty: bw.qty, - }) - ) ?? [ - { - weight: '', - avg_weight: '', - qty: '', - }, - ], stocks: initialValues?.stocks?.map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index c8fa3ca0..3a6f8071 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -60,6 +60,7 @@ import { GROWING_RECORDING_APPROVAL_LINE, LAYING_RECORDING_APPROVAL_LINE, } from '@/config/approval-line'; +import Table from '@/components/Table'; interface RecordingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -71,16 +72,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const router = useRouter(); // ===== STATE MANAGEMENT ===== - const [selectedBodyWeights, setSelectedBodyWeights] = useState([]); const [selectedStocks, setSelectedStocks] = useState([]); const [selectedDepletions, setSelectedDepletions] = useState([]); const [selectedEggs, setSelectedEggs] = useState([]); - const [editingAverageIndex] = useState(null); - const [manuallyEditedRows, setManuallyEditedRows] = useState>( - new Set() - ); - const [locationSearchValue, setLocationSearchValue] = useState(''); const [selectedLocation, setSelectedLocation] = useState( null @@ -122,19 +117,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { (values: RecordingGrowingFormValues) => { return { project_flock_kandang_id: values.project_flock_kandang_id, - body_weights: (values.body_weights ?? []).map((bw) => { - const qty = Number(bw.qty) || 0; - const weight = Number(bw.weight) || 0; - const totalWeight = qty * weight; - return { - avg_weight: - typeof bw.avg_weight === 'number' - ? bw.avg_weight - : parseFloat(String(bw.avg_weight)) || 0, - qty: qty, - total_weight: parseFloat(String(totalWeight)) || 0, - }; - }), stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -152,15 +134,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { (values: RecordingLayingFormValues) => { return { project_flock_kandang_id: values.project_flock_kandang_id, - body_weights: (values.body_weights ?? []).map((bw) => { - return { - avg_weight: - typeof bw.avg_weight === 'number' - ? bw.avg_weight - : parseFloat(String(bw.avg_weight)) || 0, - qty: Number(bw.qty) || 0, - }; - }), stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -587,28 +560,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return recordedIds; }, [existingRecordings, today]); - const getLatestTotalChickQty = useCallback( - (projectFlockKandangId: number) => { - if (!isResponseSuccess(existingRecordings)) return null; - - const projectFlockRecordings = existingRecordings.data.filter( - (recording) => - recording.project_flock_kandang_id === projectFlockKandangId - ); - - if (projectFlockRecordings.length === 0) return null; - - projectFlockRecordings.sort( - (a, b) => - new Date(b.record_datetime).getTime() - - new Date(a.record_datetime).getTime() - ); - - return projectFlockRecordings[0].total_chick_qty; - }, - [existingRecordings] - ); - const unifiedStockProducts = useMemo(() => { const options: OptionType[] = []; if (isResponseSuccess(stockProducts) && selectedKandang) { @@ -808,25 +759,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); // ===== HELPER FUNCTIONS ===== - const getTotalChickQtyError = useCallback( - (qty: number) => { - if (type === 'detail') return null; - if (!formik.values.project_flock_kandang_id) return null; - - const totalChickQty = getLatestTotalChickQty( - formik.values.project_flock_kandang_id - ); - if (!totalChickQty) return null; - - if (qty > totalChickQty) { - return `Jumlah ayam tidak boleh melebihi total ayam tersedia! Maksimal: ${formatNumber(totalChickQty)}`; - } - - return null; - }, - [formik.values.project_flock_kandang_id, getLatestTotalChickQty, type] - ); - useCallback((): OptionType | null => { if ( !formik.values.project_flock_kandang || @@ -1193,124 +1125,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setIsRejectLoading(false); }; - const addBodyWeight = () => { - const newBodyWeights = [ - ...(formik.values.body_weights || []), - { - weight: '', - avg_weight: '', - qty: '', - }, - ]; - formik.setFieldValue('body_weights', newBodyWeights); - }; - - const handleWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.weight`, value); - - setManuallyEditedRows((prev) => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = Number(currentWeight.qty) || 0; - const totalWeight = qty * value; - - if (qty > 0 && value > 0) { - const avgWeight = parseFloat((value / qty).toFixed(2)); - formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.avg_weight`, ''); - } - - formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); - } - }; - - const handleAvgWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.avg_weight`, value); - - setManuallyEditedRows((prev) => { - const newSet = new Set(prev); - newSet.add(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = Number(currentWeight.qty) || 0; - if (qty > 0 && value > 0) { - const totalWeight = value * qty; - formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); - formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.weight`, 0); - formik.setFieldValue(`body_weights.${idx}.total_weight`, 0); - } - } - }; - - const handleQtyChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.qty`, value); - - setManuallyEditedRows((prev) => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const weight = Number(currentWeight.weight) || 0; - const totalWeight = value * weight; - - if (value > 0 && weight > 0) { - const avgWeight = parseFloat((weight / value).toFixed(2)); - formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.avg_weight`, ''); - } - - formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); - } - }; - - const handleWeightChangeWrapper = - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleWeightChange(idx, value); - }; - - const handleAvgWeightChangeWrapper = - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleAvgWeightChange(idx, value); - }; - - const handleQtyChangeWrapper = - (idx: number) => (e: React.ChangeEvent) => { - const value = parseFloat(e.target.value) || 0; - handleQtyChange(idx, value); - }; - - const removeBodyWeight = (idx: number) => { - const updatedBodyWeights = formik.values.body_weights?.filter( - (_, i) => i !== idx - ); - formik.setFieldValue('body_weights', updatedBodyWeights); - }; - - const removeSelectedBodyWeights = () => { - const updatedBodyWeights = formik.values.body_weights?.filter( - (_, idx) => !selectedBodyWeights.includes(idx) - ); - formik.setFieldValue('body_weights', updatedBodyWeights); - setSelectedBodyWeights([]); - }; - const addStock = () => { const newStocks = [ ...(formik.values.stocks || []), @@ -1435,45 +1249,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } }, [isLayingCategory, type]); - const bodyWeightValues = useMemo(() => { - if (!formik.values.body_weights) return []; - return formik.values.body_weights.map((w) => ({ - weight: w.weight, - qty: w.qty, - })); - }, [formik.values.body_weights]); - - useEffect(() => { - if (formik.values.body_weights && editingAverageIndex === null) { - const updatedBodyWeights = formik.values.body_weights.map( - (weight, idx) => { - if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { - return weight; - } - const qty = Number(weight.qty) || 0; - const weightValue = Number(weight.weight) || 0; - return { - ...weight, - avg_weight: - qty > 0 && weightValue > 0 - ? parseFloat((weightValue / qty).toFixed(2)) - : 0, - }; - } - ); - const hasChanges = updatedBodyWeights.some( - (updated, idx) => - idx !== editingAverageIndex && - !manuallyEditedRows.has(idx) && - updated.avg_weight !== - (formik.values.body_weights[idx]?.avg_weight || 0) - ); - if (hasChanges) { - formik.setFieldValue('body_weights', updatedBodyWeights, false); - } - } - }, [bodyWeightValues, editingAverageIndex, manuallyEditedRows]); - return ( <>
@@ -1763,266 +1538,241 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - {/* Body Weights Table */} - -
- - - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - )} - - - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - )} - - - - {formik.values.body_weights?.map((bw, idx) => ( - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + + + + + + +
- 0 - } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedBodyWeights( - formik.values.body_weights?.map( - (_, idx) => idx - ) ?? [] - ); - } else { - setSelectedBodyWeights([]); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - Berat Ayam (gram) - - * - - - Jumlah Ayam - - * - - - Rata-rata Berat Ayam (gram) - - * - - Action
- - ) => { - if (e.target.checked) { - setSelectedBodyWeights([ - ...selectedBodyWeights, - idx, - ]); - } else { - setSelectedBodyWeights( - selectedBodyWeights.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> + {/* FCR & Mortality Metrics - Detail View Only */} + {type === 'detail' && initialValues && ( +
+ {/* FCR Section */} +
+
+ FCR +
+
+ + + + + + + + + + + + - )} - - - - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - )} - - ))} - -
+ AKTUAL + + STANDAR +
FCR + + {initialValues.fcr_value && + initialValues.fcr_value > 0 + ? formatNumber(initialValues.fcr_value) + : '-'} + - - - - - - -
- -
+
+ {initialValues.fcr_std && initialValues.fcr_std > 0 + ? formatNumber(initialValues.fcr_std) + : '-'}
-
- {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( -
- {selectedBodyWeights.length > 0 && ( - - )} - +
Feed Intake (KG) + + {initialValues.feed_intake && + initialValues.feed_intake > 0 + ? formatNumber(initialValues.feed_intake) + : '-'} + + + {initialValues.feed_intake_std && + initialValues.feed_intake_std > 0 + ? formatNumber(initialValues.feed_intake_std) + : '-'} +
+
- )} -
+ + {/* Mortality Section */} +
+
+ + Mortalitas + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ DEPLESI KUMULATIF +
+ Total + + (%) +
+ + {initialValues.total_depletion_qty && + initialValues.total_depletion_qty > 0 + ? formatNumber(initialValues.total_depletion_qty) + : '-'} + + + {initialValues.cum_depletion_rate && + initialValues.cum_depletion_rate > 0 + ? initialValues.cum_depletion_rate.toFixed(2) + : '-'} +
+ Total Ayam +
+ {initialValues.total_chick_qty && + initialValues.total_chick_qty > 0 + ? formatNumber(initialValues.total_chick_qty) + : '-'} +
+
+
+ + {/* Egg Production Section - Only for LAYING category */} + {type === 'detail' && + initialValues && + initialValues.project_flock_category === 'LAYING' && ( +
+
+ + Produksi + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ AKTUAL + + STANDAR +
Egg Mass + + {initialValues.egg_mesh && + initialValues.egg_mesh > 0 + ? formatNumber(initialValues.egg_mesh) + : '-'} + + + {initialValues.egg_mesh_std && + initialValues.egg_mesh_std > 0 + ? formatNumber(initialValues.egg_mesh_std) + : '-'} +
+ Egg Weight (KG) + + + {initialValues.egg_weight && + initialValues.egg_weight > 0 + ? formatNumber(initialValues.egg_weight) + : '-'} + + + {initialValues.egg_weight_std && + initialValues.egg_weight_std > 0 + ? formatNumber(initialValues.egg_weight_std) + : '-'} +
Hen Day + + {initialValues.hand_day && + initialValues.hand_day > 0 + ? formatNumber(initialValues.hand_day) + : '-'} + + + {initialValues.hand_day_std !== undefined && + initialValues.hand_day_std > 0 + ? `${initialValues.hand_day_std}%` + : '-'} +
Hen House + + {initialValues.hand_house && + initialValues.hand_house > 0 + ? formatNumber(initialValues.hand_house) + : '-'} + + + {initialValues.hand_house_std !== undefined && + initialValues.hand_house_std > 0 + ? `${initialValues.hand_house_std}%` + : '-'} +
+
+
+ )} + + )} {/* Stocks Table */} { + 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 [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, + } = 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, + } = 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, + } = 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, + } = 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); + // TODO: Implement export functionality in API service first if needed + toast.error('Fitur export belum tersedia'); + setIsLoadingExportingToExcel(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(projectFlockKandangResponse.meta); + 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/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx new file mode 100644 index 00000000..80ee3e9d --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx @@ -0,0 +1,364 @@ +'use client'; + +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 { isResponseSuccess } from '@/lib/api-helper'; +import { ProductionResult } from '@/types/api/report/production-result'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ProductionResultReportApi } from '@/services/api/report/production-result'; + +interface ProductionResultProjectFlockKandangTableProps { + projectFlockKandangId?: number; + kandangName?: string; +} + +const ProductionResultProjectFlockKandangTable = ({ + projectFlockKandangId, + kandangName, +}: ProductionResultProjectFlockKandangTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter({ + initial: { + filter_by: '', + sort_by: '', + }, + paramMap: { + pageSize: 'limit', + }, + }); + + const { data: productionResults, isLoading: isLoadingProductionResults } = + useSWR( + projectFlockKandangId + ? `/reports/production-result/${projectFlockKandangId}${getTableFilterQueryString()}` + : null, + ProductionResultReportApi.getAllProductionResultFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(false); + + const [sorting, setSorting] = useState([]); + + 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), + }, + // Butiran + { + 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), + }, + ], + }, + // Kg + { + 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), + }, + ], + }, + // Persen + { + 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), + }, + ], + }, + // Produksi + { + header: 'Produksi', + columns: [ + { + accessorKey: 'hd', + header: 'HD', + cell: (props) => formatNumber(props.row.original.hd), + }, + { + accessorKey: 'hd_std', + header: 'HD STD', + cell: (props) => formatNumber(props.row.original.hd_std), + }, + { + accessorKey: 'fi', + header: 'FI', + cell: (props) => formatNumber(props.row.original.fi), + }, + { + accessorKey: 'fi_std', + header: 'FI STD', + cell: (props) => formatNumber(props.row.original.fi_std), + }, + { + accessorKey: 'em', + header: 'EM', + cell: (props) => formatNumber(props.row.original.em), + }, + { + accessorKey: 'em_std', + header: 'EM STD', + cell: (props) => formatNumber(props.row.original.em_std), + }, + { + accessorKey: 'ew', + header: 'EW', + cell: (props) => formatNumber(props.row.original.ew), + }, + { + accessorKey: 'ew_std', + header: 'EW STD', + cell: (props) => formatNumber(props.row.original.ew_std), + }, + { + accessorKey: 'fcr', + header: 'FCR', + cell: (props) => formatNumber(props.row.original.fcr), + }, + { + accessorKey: 'fcr_std', + header: 'FCR STD', + cell: (props) => formatNumber(props.row.original.fcr_std), + }, + { + accessorKey: 'hh', + header: 'HH', + cell: (props) => formatNumber(props.row.original.hh), + }, + { + accessorKey: 'hh_std', + header: 'HH STD', + cell: (props) => formatNumber(props.row.original.hh_std), + }, + ], + }, + ]; + + useEffect(() => { + if (sorting.length === 1) { + updateFilter('filter_by', sorting[0].id); + updateFilter('sort_by', sorting[0].desc ? 'desc' : 'asc'); + } else { + updateFilter('filter_by', ''); + updateFilter('sort_by', ''); + } + }, [sorting]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(productionResults) + ? productionResults.data.length > 0 + : false + ); + } + }, [productionResults, isResponseSuccess]); + + return ( + + +
{kandangName}
+ + + + } + 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-base-content/10 text-base-content/50', + }} + /> +
+
+
+ ); +}; + +export default ProductionResultProjectFlockKandangTable; diff --git a/src/config/constant.ts b/src/config/constant.ts index a2f5349d..f177b394 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -82,6 +82,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Penjualan', link: '/report/marketing', }, + { + text: 'Hasil Produksi', + link: '/report/production-result', + }, ], }, { diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 80bddb8a..47013aba 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -3,6 +3,7 @@ export const ROUTE_PERMISSIONS: Record = { // Dashboard '/dashboard/': ['lti.dashboard.list'], + '/dashboard': ['lti.dashboard.list'], // Production // Production - Project Flock @@ -75,12 +76,19 @@ 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/add/': ['lti.finance.payments.create'], - '/finance/detail/edit/': ['lti.finance.payments.update'], - '/finance/add/initial-balance/': ['lti.finance.initial_balances.create'], + '/finance/': ['lti.dashboard.list', 'lti.finance.transaction.list'], + '/finance/detail/': ['lti.dashboard.list', 'lti.finance.transaction.detail'], + '/finance/add/': ['lti.dashboard.list', 'lti.finance.payments.create'], + '/finance/detail/edit/': [ + 'lti.dashboard.list', + 'lti.finance.payments.update', + ], + '/finance/add/initial-balance/': [ + 'lti.dashboard.list', + 'lti.finance.initial_balances.create', + ], '/finance/detail/edit/initial-balance/': [ + 'lti.dashboard.list', 'lti.finance.initial_balances.update', ], '/finance/add/injection/': ['lti.finance.injections.create'], @@ -95,6 +103,10 @@ export const ROUTE_PERMISSIONS: Record = { '/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'], + // Inventory '/inventory/adjustment/': ['lti.inventory.list'], '/inventory/adjustment/add/': ['lti.inventory.create'], @@ -178,14 +190,20 @@ export const ROUTE_PERMISSIONS: Record = { '/master-data/flock/detail/': ['lti.master.flocks.detail'], '/master-data/flock/detail/edit/': ['lti.master.flocks.update'], - '/master-data/production-standard/': ['lti.master.production_standards.list'], + '/master-data/production-standard/': [ + 'lti.dashboard.list', + 'lti.master.production_standards.list', + ], '/master-data/production-standard/add/': [ + 'lti.dashboard.list', 'lti.master.production_standards.create', ], '/master-data/production-standard/detail/': [ + 'lti.dashboard.list', 'lti.master.production_standards.detail', ], '/master-data/production-standard/detail/edit/': [ + 'lti.dashboard.list', 'lti.master.production_standards.update', ], }; diff --git a/src/services/api/base.ts b/src/services/api/base.ts index 2fcf3712..6a3fdfa4 100644 --- a/src/services/api/base.ts +++ b/src/services/api/base.ts @@ -15,6 +15,22 @@ export class BaseApiService { return await httpClientFetcher>(endpoint); } + async getAll(query?: Record) { + try { + const getAllPath = this.basePath; + const getAllRes = await httpClient>(getAllPath, { + query, + }); + + return getAllRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + async getSingle(id: number) { try { const getSinglePath = `${this.basePath}/${id}`; diff --git a/src/services/api/report/production-result.ts b/src/services/api/report/production-result.ts new file mode 100644 index 00000000..98997e9b --- /dev/null +++ b/src/services/api/report/production-result.ts @@ -0,0 +1,166 @@ +import { sleep } from '@/lib/helper'; +import { BaseApiService } from '@/services/api/base'; +import { 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, + }, + ], +}; + +export class ProductionResultReportApiService extends BaseApiService< + ProductionResult, + unknown, + unknown +> { + constructor(basePath: string = '/reports/production-result') { + super(basePath); + } + + async getAllProductionResultFetcher( + endpoint: string + ): Promise> { + // return await httpClientFetcher>( + // endpoint + // ); + + await sleep(1000); + + return PRODUCTION_RESULT_DUMMY_DATA; + } +} + +export const ProductionResultReportApi = new ProductionResultReportApiService(); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index c23354f8..ecdaebb9 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -6,6 +6,11 @@ import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; import { BaseMetadata } from '@/types/api/api-general'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; +import { ProjectFlock } from '@/types/api/production/project-flock'; export type BaseSales = { id: number; @@ -29,10 +34,6 @@ export type BaseClosingSales = { period: number; sales: BaseSales[]; }; -import { Kandang } from '@/types/api/master-data/kandang'; -import { Product } from '@type/api/master-data/product'; -import { Customer } from '@type/api/master-data/customer'; -import { BaseMetadata } from '@/types/api/api-general'; export type BaseSales = { id: number; @@ -66,9 +67,6 @@ export type BaseClosing = { closing_date?: string; shed_label: string; shed_count: number; - sales_paid_amount: number; - sales_remaining_amount: number; - sales_payment_status: string; project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; }; @@ -83,6 +81,7 @@ export type BaseClosingGeneralInformation = BaseClosing & { sales_payment_status: string; project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; closing_status: string; + project_flock: ProjectFlock; }; export type ClosingGeneralInformation = BaseMetadata & diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 9bed7685..9cf9a625 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -4,12 +4,23 @@ import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; export type ProductionMetrics = { total_depletion_qty: number; cum_depletion_rate: number; - daily_gain: number; - avg_daily_gain: number; cum_intake: number; fcr_value: number; + fcr_std?: number; total_chick_qty: number; - cum_depletion: number; + hand_day?: number; + hand_house?: number; + feed_intake?: number; + egg_mesh?: number; + egg_weight?: number; + hand_day_std?: number; + hand_house_std?: number; + feed_intake_std?: number; + egg_mesh_std?: number; + egg_weight_std?: number; + daily_gain?: number; + avg_daily_gain?: number; + cum_depletion?: number; }; export type BaseRecording = { @@ -20,12 +31,6 @@ export type BaseRecording = { project_flock_category?: 'GROWING' | 'LAYING'; } & ProductionMetrics; -export type RecordingBW = { - avg_weight: number; - qty: number; - total_weight: number; -}; - export type RecordingDepletion = { product_warehouse_id: number; qty: number; @@ -63,7 +68,6 @@ export type Recording = BaseMetadata & BaseRecording & { approval?: BaseApproval; created_user: User; - body_weights?: RecordingBW[]; depletions?: RecordingDepletion[]; stocks?: RecordingStock[]; eggs?: RecordingEgg[]; @@ -77,10 +81,6 @@ export type NextDayRecording = { export type CreateGrowingRecordingPayload = { project_flock_kandang_id: number; - body_weights: { - avg_weight: number; - qty: number; - }[]; stocks?: { product_warehouse_id: number; qty: number; diff --git a/src/types/api/report/production-result.d.ts b/src/types/api/report/production-result.d.ts new file mode 100644 index 00000000..3fa8fdb8 --- /dev/null +++ b/src/types/api/report/production-result.d.ts @@ -0,0 +1,59 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { BaseKandang } from '@/types/api/master-data/kandang'; + +export type BaseProductionResult = { + project_flock: Pick & { + kandang: Pick; + }; + + woa: number; + + // BW + bw: number; + std_bw: number; + uniformity: number; + std_uniformity: string; // "90% up" - keeping as string based on "up" suffix potential + + // Dep + dep_kum: number; + dep_std: number; + + // Butiran + butiran_utuh: number; + butiran_putih: number; + butiran_retak: number; + butiran_pecah: number; + butiran_jumlah: number; + total_butir: number; + + // Kg + kg_utuh: number; + kg_putih: number; + kg_retak: number; + kg_pecah: number; + kg_jumlah: number; + total_kg: number; + + // % + persen_utuh: number; + persen_putih: number; + persen_retak: number; + persen_pecah: number; + + // Produksi + hd: number; + hd_std: number; + fi: number; + fi_std: number; + em: number; + em_std: number; + ew: number; + ew_std: number; + fcr: number; + fcr_std: number; + hh: number; + hh_std: number; +}; + +export type ProductionResult = BaseMetadata & BaseProductionResult;