diff --git a/src/app/master-data/production-standard/add/page.tsx b/src/app/master-data/production-standard/add/page.tsx new file mode 100644 index 00000000..f25338d6 --- /dev/null +++ b/src/app/master-data/production-standard/add/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm'; + +const AddProductionStandardPage = () => { + return ( + <> + + + ); +}; + +export default AddProductionStandardPage; diff --git a/src/app/master-data/production-standard/detail/edit/page.tsx b/src/app/master-data/production-standard/detail/edit/page.tsx new file mode 100644 index 00000000..8c72053f --- /dev/null +++ b/src/app/master-data/production-standard/detail/edit/page.tsx @@ -0,0 +1,11 @@ +import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm'; + +const EditProductionStandardPage = () => { + return ( + <> + + + ); +}; + +export default EditProductionStandardPage; diff --git a/src/app/master-data/production-standard/detail/layout.tsx b/src/app/master-data/production-standard/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/production-standard/detail/layout.tsx @@ -0,0 +1,11 @@ +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/production-standard/detail/page.tsx b/src/app/master-data/production-standard/detail/page.tsx new file mode 100644 index 00000000..ce6a2412 --- /dev/null +++ b/src/app/master-data/production-standard/detail/page.tsx @@ -0,0 +1,11 @@ +import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm'; + +const DetailProductionStandardPage = () => { + return ( + <> + + + ); +}; + +export default DetailProductionStandardPage; diff --git a/src/app/master-data/production-standard/page.tsx b/src/app/master-data/production-standard/page.tsx new file mode 100644 index 00000000..ed1107cd --- /dev/null +++ b/src/app/master-data/production-standard/page.tsx @@ -0,0 +1,11 @@ +import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable'; + +const ProductionStandardPage = () => { + return ( +
+ +
+ ); +}; + +export default ProductionStandardPage; diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx new file mode 100644 index 00000000..c473f4c7 --- /dev/null +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -0,0 +1,201 @@ +'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'; +import useSWR from 'swr'; +import { productionStandardApi } from '@/services/api/master-data'; +import { isResponseSuccess } from '@/lib/api-helper'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { CellContext } from '@tanstack/react-table'; +import { useModal } from '@/components/Modal'; +import { useState } from 'react'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import toast from 'react-hot-toast'; +import { cn } from '@/lib/helper'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( + + + + + + + + ); +}; + +const ProductionStandardTable = () => { + const deleteModal = useModal(); + + const [selectedProductionStandard, setSelectedProductionStandard] = useState< + ProductionStandard | undefined + >(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const { + data: productionStandards, + isLoading: productionStandardsLoading, + mutate: refreshProductionStandards, + } = useSWR( + `${productionStandardApi.basePath}`, + productionStandardApi.getAllFetcher + ); + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await productionStandardApi.delete( + selectedProductionStandard?.id as number + ); + refreshProductionStandards(); + + deleteModal.closeModal(); + toast.success('Successfully delete Production Standard!'); + setIsDeleteLoading(false); + }; + + return ( + <> +
+
+ +
+ + data={ + isResponseSuccess(productionStandards) + ? productionStandards.data + : [] + } + columns={[ + { + header: 'No', + accessorFn: (row, index) => index + 1, + }, + { + header: 'Nama', + accessorKey: 'name', + }, + { + header: 'Jumlah Week', + accessorFn: (row) => row.details.length, + }, + { + 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 = () => { + setSelectedProductionStandard(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]} + className={{ + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'last:flex last:flex-row last:justify-end' + ), + bodyColumnClassName: cn( + TABLE_DEFAULT_STYLING.bodyColumnClassName, + 'last:flex last:flex-row last:justify-end' + ), + }} + /> +
+ + + ); +}; + +export default ProductionStandardTable; diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts new file mode 100644 index 00000000..a2d18c71 --- /dev/null +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts @@ -0,0 +1,62 @@ +import * as Yup from 'yup'; + +export const ProductionStandardFormSchema = Yup.object({ + name: Yup.string().required('Nama wajib diisi!'), + project_category: Yup.string().required('Kategori proyek wajib diisi!'), + details: Yup.array().of( + Yup.object({ + week: Yup.number().required('Minggu wajib diisi!'), + production_standard_details: Yup.object({ + target_hen_day_production: Yup.number().required( + 'Produksi telur per hari wajib diisi!' + ), + target_hen_house_production: Yup.number().required( + 'Produksi telur per kandang wajib diisi!' + ), + target_egg_weight: Yup.number().required('Berat telur wajib diisi!'), + target_egg_mass: Yup.number().required('Massa telur wajib diisi!'), + }), + standard_growth_details: Yup.object({ + target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), + max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), + min_uniformity: Yup.number().required( + 'Minimal uniformitas wajib diisi!' + ), + feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + }), + }) + ), +}); + +export const UpdateProductionStandardFormSchema = ProductionStandardFormSchema; + +export type ProductionStandardFormValues = Yup.InferType< + typeof ProductionStandardFormSchema +>; + +export const ProductionStandardRepeaterFormSchema = Yup.object({ + week: Yup.number().required('Minggu wajib diisi!'), + production_standard_details: Yup.object({ + target_hen_day_production: Yup.number().required( + 'Produksi telur per hari wajib diisi!' + ), + target_hen_house_production: Yup.number().required( + 'Produksi telur per kandang wajib diisi!' + ), + target_egg_weight: Yup.number().required('Berat telur wajib diisi!'), + target_egg_mass: Yup.number().required('Massa telur wajib diisi!'), + }), + standard_growth_details: Yup.object({ + target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), + max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), + min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), + feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + }), +}); + +export const UpdateProductionStandardRepeaterFormSchema = + ProductionStandardRepeaterFormSchema; + +export type ProductionStandardRepeaterFormSchemaValues = Yup.InferType< + typeof ProductionStandardRepeaterFormSchema +>; diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx new file mode 100644 index 00000000..69667b31 --- /dev/null +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx @@ -0,0 +1,843 @@ +'use client'; + +import Button from '@/components/Button'; +import Card from '@/components/Card'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import NumberInput from '@/components/input/NumberInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import TextInput from '@/components/input/TextInput'; +import { + ProductionStandardRepeaterFormSchemaValues, + ProductionStandardFormValues, + ProductionStandardRepeaterFormSchema, +} from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import { FLOCK_CATEGORY_OPTIONS } from '@/config/constant'; +import { cn } from '@/lib/helper'; +import { ProductionStandard } from '@/types/api/master-data/production-standard'; +import { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { useEffect, useMemo, useState } from 'react'; +import { useFormStore } from '@/stores/form/form.store'; +import { ColumnDef } from '@tanstack/react-table'; + +type TableRowsType = { + customRow: boolean; + placeHolder: string; +} & ProductionStandardRepeaterFormSchemaValues; + +const ProductionStandardForm = ({ + formType = 'add', + initialValue, + afterSubmit, +}: { + formType: 'add' | 'edit' | 'detail'; + initialValue?: ProductionStandard; + afterSubmit?: () => void; +}) => { + // ===== State ===== + const [editMode, setEditMode] = useState(false); + const [editIndex, setEditIndex] = useState(null); + const [isTableExpanded, setIsTableExpanded] = useState(false); + + // ===== Store ===== + const { + formData, + setFormData, + addDetail, + updateDetail, + deleteDetail, + clearCache, + } = useFormStore(); + + // ===== Formik ===== + const formikInitialValues = useMemo(() => { + // For add mode, merge cached data with initial values + if (formType === 'add' && formData) { + return { + name: formData.name || '', + project_category: formData.project_category || '', + details: formData.details || [], + } as ProductionStandardFormValues; + } + + return { + name: initialValue?.name || '', + project_category: initialValue?.project_category || '', + details: initialValue?.details || [], + } as ProductionStandardFormValues; + }, [initialValue, formData, formType]); + const formik = useFormik({ + initialValues: + formikInitialValues as unknown as ProductionStandardFormValues, + onSubmit: (values) => { + switch (formType) { + case 'add': + handleSubmit(values); + break; + case 'edit': + handleUpdate(values); + break; + default: + break; + } + afterSubmit?.(); + }, + }); + const { setValues: formikSetValues } = formik; + + // ===== Formik Repeater ===== + const repeaterFormikInitialValues = useMemo(() => { + return { + week: '' as unknown as number, + production_standard_details: { + target_hen_day_production: '' as unknown as number, + target_hen_house_production: '' as unknown as number, + target_egg_weight: '' as unknown as number, + target_egg_mass: '' as unknown as number, + }, + standard_growth_details: { + target_mean_bw: '' as unknown as number, + max_depletion: '' as unknown as number, + min_uniformity: '' as unknown as number, + feed_intake: '' as unknown as number, + }, + }; + }, []); + const repeaterFormik = useFormik({ + initialValues: + repeaterFormikInitialValues as unknown as ProductionStandardRepeaterFormSchemaValues, + validationSchema: ProductionStandardRepeaterFormSchema, + onSubmit: (values) => { + if (editMode && editIndex !== null) { + handleUpdateRow(editIndex, values); + } else { + handleAddRow(values); + } + }, + }); + const { setValues: repeaterFormikSetValues } = repeaterFormik; + + // ===== Effect ===== + useEffect(() => { + formikSetValues( + formikInitialValues as unknown as ProductionStandardFormValues + ); + }, [formikSetValues, formikInitialValues]); + + // ===== Data Table ===== + const tableRows = useMemo(() => { + const details = formik.values.details || []; + const rows: TableRowsType[] = []; + + // Show placeholder if no details + if (details.length === 0) { + rows.push({ + customRow: true, + placeHolder: 'Masukkan data standard produksi', + } as TableRowsType); + } else { + // Show actual data rows + details.forEach((detail) => { + rows.push(detail as TableRowsType); + }); + } + + // Always add repeater form row at the end + rows.push({ + customRow: true, + placeHolder: '', + } as TableRowsType); + + return rows; + }, [formik.values.details]); + const columns = useMemo[]>(() => { + return [ + { + header: 'No', + accessorFn: (row, index) => index + 1, + enableSorting: false, + }, + { + header: 'Minggu', + accessorKey: 'week', + enableSorting: false, + }, + { + header: 'Hen Day', + accessorFn: (row) => + row.production_standard_details.target_hen_day_production, + enableSorting: false, + }, + { + header: 'Hen House', + accessorFn: (row) => + row.production_standard_details.target_hen_house_production, + enableSorting: false, + }, + { + header: 'Egg Weight', + accessorFn: (row) => row.production_standard_details.target_egg_weight, + enableSorting: false, + }, + { + header: 'Egg Mass', + accessorFn: (row) => row.production_standard_details.target_egg_mass, + enableSorting: false, + }, + { + header: 'Mean BW', + accessorFn: (row) => row.standard_growth_details.target_mean_bw, + enableSorting: false, + }, + { + header: 'Max Depletion', + accessorFn: (row) => row.standard_growth_details.max_depletion, + enableSorting: false, + }, + { + header: 'Min Uniformity', + accessorFn: (row) => row.standard_growth_details.min_uniformity, + enableSorting: false, + }, + { + header: 'Feed Intake', + accessorFn: (row) => row.standard_growth_details.feed_intake, + enableSorting: false, + }, + { + header: 'Aksi', + cell: (row) => { + // Don't show action buttons for custom rows or in detail mode + if (row.row.original.customRow || formType === 'detail') return null; + + return ( +
+ + +
+ ); + }, + }, + ]; + }, []); + + // ===== Handler ===== + const handleAddRow = (values: ProductionStandardRepeaterFormSchemaValues) => { + // Check for duplicate week + const existingWeeks = (formik.values.details || []).map((d) => d.week); + if (existingWeeks.includes(values.week)) { + repeaterFormik.setFieldError( + 'week', + 'Minggu sudah ada, pilih minggu lain!' + ); + return; + } + + const newValues = [ + ...(formik.values.details || []), + ] as ProductionStandardRepeaterFormSchemaValues[]; + newValues.push(values); + + const updatedFormValues = { + ...formik.values, + details: newValues, + }; + + formikSetValues(updatedFormValues); + + // Save to store (only in add mode) + if (formType === 'add') { + setFormData({ + name: updatedFormValues.name, + project_category: updatedFormValues.project_category, + details: updatedFormValues.details || [], + }); + } + + // Reset repeater form + // repeaterFormik.resetForm(); + repeaterFormikSetValues({ + ...repeaterFormik.values, + week: Number(values.week) + 1, + }); + + // Scroll to bottom after adding + setTimeout(() => scrollToBottom(), 100); + }; + + const handleRemoveRow = (index: number) => { + const newValues = [...(formik.values.details || [])]; + newValues.splice(index, 1); + + const updatedFormValues = { + ...formik.values, + details: newValues, + }; + + formikSetValues(updatedFormValues); + + // Save to store (only in add mode) + if (formType === 'add') { + setFormData({ + name: updatedFormValues.name, + project_category: updatedFormValues.project_category, + details: updatedFormValues.details || [], + }); + } + }; + + const handleUpdateRow = ( + index: number, + values: ProductionStandardRepeaterFormSchemaValues + ) => { + // Check for duplicate week (excluding current row) + const existingWeeks = (formik.values.details || []) + .map((d, i) => (i !== index ? d.week : null)) + .filter((w) => w !== null); + if (existingWeeks.includes(values.week)) { + repeaterFormik.setFieldError( + 'week', + 'Minggu sudah ada, pilih minggu lain!' + ); + return; + } + + const newValues = [...(formik.values.details || [])]; + newValues[index] = values; + + const updatedFormValues = { + ...formik.values, + details: newValues, + }; + + formikSetValues(updatedFormValues); + + // Save to store (only in add mode) + if (formType === 'add') { + setFormData({ + name: updatedFormValues.name, + project_category: updatedFormValues.project_category, + details: updatedFormValues.details || [], + }); + } + + // Exit edit mode and reset form + setEditMode(false); + setEditIndex(null); + repeaterFormik.resetForm(); + + // Scroll to bottom after updating + setTimeout(() => scrollToBottom(), 100); + }; + + const handleEditClick = (index: number) => { + const row = formik.values.details?.[ + index + ] as ProductionStandardRepeaterFormSchemaValues; + if (row) { + setEditMode(true); + setEditIndex(index); + repeaterFormikSetValues(row); + } + }; + + const handleCancelEdit = () => { + setEditMode(false); + setEditIndex(null); + repeaterFormik.resetForm(); + }; + + const handleSubmit = (values: ProductionStandardFormValues) => { + console.log('Submitting:', values); + // TODO: Call API to submit data + // After successful submission: + clearCache(); + formik.resetForm(); + afterSubmit?.(); + }; + + const handleUpdate = (values: ProductionStandardFormValues) => { + console.log('Updating:', values); + // TODO: Call API to update data + // After successful update: + clearCache(); + afterSubmit?.(); + }; + + const handleReset = () => { + // Clear cache and reset form + clearCache(); + formik.resetForm(); + repeaterFormik.resetForm(); + setEditMode(false); + setEditIndex(null); + }; + + // Scroll to bottom of table + const scrollToBottom = () => { + // Find the table wrapper element by its class + const tableWrapper = document.querySelector( + '.overflow-x-auto.max-h-128, .overflow-x-auto' + ); + if (tableWrapper) { + tableWrapper.scrollTo({ + top: tableWrapper.scrollHeight, + behavior: 'smooth', + }); + } + }; + + // Toggle table height + const toggleTableHeight = () => { + setIsTableExpanded(!isTableExpanded); + }; + + // Wrapper handlers for saving to store + const handleNameChange = (e: React.ChangeEvent) => { + formik.handleChange(e); + + // Save to store (only in add mode) + if (formType === 'add') { + setFormData({ + name: e.target.value, + project_category: formik.values.project_category, + details: formik.values.details || [], + }); + } + }; + + const handleProjectCategoryChange = (value: unknown) => { + const newValue = ((value as OptionType)?.value as string) || ''; + + formikSetValues({ + ...formik.values, + project_category: newValue, + }); + + // Save to store (only in add mode) + if (formType === 'add') { + setFormData({ + name: formik.values.name, + project_category: newValue, + details: formik.values.details || [], + }); + } + }; + + // ===== Function ===== + + return ( +
+ +
+ + option.value === formik.values.project_category + )} + options={FLOCK_CATEGORY_OPTIONS} + onChange={handleProjectCategoryChange} + errorMessage={formik.errors.project_category as string} + isError={Boolean(formik.errors.project_category)} + /> +
+ + + data={tableRows} + columns={columns} + pageSize={tableRows.length} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'last:flex last:flex-row last:justify-end' + ), + bodyColumnClassName: cn( + TABLE_DEFAULT_STYLING.bodyColumnClassName, + 'last:flex last:flex-row last:justify-end' + ), + tableWrapperClassName: cn( + TABLE_DEFAULT_STYLING.tableWrapperClassName, + 'overflow-x-auto', + !isTableExpanded && 'max-h-128' + ), + tableClassName: cn( + 'font-inter w-full text-sm font-medium', + 'table table-pin-rows table-pin-cols' + ), + }} + renderCustomRow={(row) => { + if (row.original.customRow) { + if (row.original.placeHolder) { + return ( + + + {row.original.placeHolder} + + + ); + } + return ( + + +
+
+ + } + errorMessage={ + repeaterFormik.errors.production_standard_details + ?.target_hen_day_production as string + } + isError={ + Boolean( + repeaterFormik.errors.production_standard_details + ?.target_hen_day_production + ) && + Boolean( + repeaterFormik.touched.production_standard_details + ?.target_hen_day_production + ) + } + disabled={formType === 'detail'} + /> + + Butir +
+ } + errorMessage={ + repeaterFormik.errors.production_standard_details + ?.target_hen_house_production as string + } + isError={ + Boolean( + repeaterFormik.errors.production_standard_details + ?.target_hen_house_production + ) && + Boolean( + repeaterFormik.touched.production_standard_details + ?.target_hen_house_production + ) + } + disabled={formType === 'detail'} + /> + + gr +
+ } + errorMessage={ + repeaterFormik.errors.production_standard_details + ?.target_egg_weight as string + } + isError={ + Boolean( + repeaterFormik.errors.production_standard_details + ?.target_egg_weight + ) && + Boolean( + repeaterFormik.touched.production_standard_details + ?.target_egg_weight + ) + } + disabled={formType === 'detail'} + /> + + gr + + } + errorMessage={ + repeaterFormik.errors.production_standard_details + ?.target_egg_mass as string + } + isError={ + Boolean( + repeaterFormik.errors.production_standard_details + ?.target_egg_mass + ) && + Boolean( + repeaterFormik.touched.production_standard_details + ?.target_egg_mass + ) + } + disabled={formType === 'detail'} + /> + + gr + + } + errorMessage={ + repeaterFormik.errors.standard_growth_details + ?.target_mean_bw as string + } + isError={ + Boolean( + repeaterFormik.errors.standard_growth_details + ?.target_mean_bw + ) && + Boolean( + repeaterFormik.touched.standard_growth_details + ?.target_mean_bw + ) + } + disabled={formType === 'detail'} + /> + } + errorMessage={ + repeaterFormik.errors.standard_growth_details + ?.max_depletion as string + } + isError={ + Boolean( + repeaterFormik.errors.standard_growth_details + ?.max_depletion + ) && + Boolean( + repeaterFormik.touched.standard_growth_details + ?.max_depletion + ) + } + disabled={formType === 'detail'} + /> + } + errorMessage={ + repeaterFormik.errors.standard_growth_details + ?.min_uniformity as string + } + isError={ + Boolean( + repeaterFormik.errors.standard_growth_details + ?.min_uniformity + ) && + Boolean( + repeaterFormik.touched.standard_growth_details + ?.min_uniformity + ) + } + disabled={formType === 'detail'} + /> + + gr/ekor + + } + errorMessage={ + repeaterFormik.errors.standard_growth_details + ?.feed_intake as string + } + isError={ + Boolean( + repeaterFormik.errors.standard_growth_details + ?.feed_intake + ) && + Boolean( + repeaterFormik.touched.standard_growth_details + ?.feed_intake + ) + } + disabled={formType === 'detail'} + /> + +
+ {editMode && ( + + )} + {formType !== 'detail' && ( + + )} + {/* Should not be absolute */} + +
+ + + + ); + } + return null; + }} + /> +
+
Simpan Total {formik.values.details?.length || 0} Data
+
+ + +
+
+ + ); +}; + +export default ProductionStandardForm; diff --git a/src/config/constant.ts b/src/config/constant.ts index e510691f..70308901 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -141,6 +141,10 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Flock', link: '/master-data/flock', }, + { + text: 'Standar Produksi', + link: '/master-data/production-standard', + }, ], }, ] as const; diff --git a/src/dummy/master-data/production-standard.dummy.json b/src/dummy/master-data/production-standard.dummy.json new file mode 100644 index 00000000..380a552b --- /dev/null +++ b/src/dummy/master-data/production-standard.dummy.json @@ -0,0 +1,294 @@ +[ + { + "id": 3, + "name": "Standard Growing 2024", + "project_category": "GROWING", + "created_at": "Fri May 10 2024 19:22:41 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 13, + "production_standard_uniformity_details": { + "target_mean_bw": 1608, + "max_depletion": 2.217125905431294, + "min_uniformity": 82.53307938674605, + "max_cv": 10.755001233777175, + "week": 10, + "feed_intake": 106 + } + } + ] + }, + { + "id": 1, + "name": "Standard Laying 2024", + "project_category": "LAYING", + "created_at": "Thu Jan 18 2024 05:55:37 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 7, + "production_standard_details": { + "target_hen_day_production": 88.75664879714013, + "target_hen_house_production": 88.14547241912292, + "target_egg_weight": 56.500738261325466, + "target_egg_mass": 51.3608296108157 + }, + "standard_growth_details": { + "target_mean_bw": 1630, + "max_depletion": 1.4984809075731345, + "min_uniformity": 89.58032440497733, + "max_cv": 10.088686692512729, + "week": 2, + "feed_intake": 109 + } + } + ] + }, + { + "id": 18, + "name": "Standard Laying 2024", + "project_category": "LAYING", + "created_at": "Mon Mar 04 2024 08:29:01 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 5, + "production_standard_details": { + "target_hen_day_production": 96.61629851755295, + "target_hen_house_production": 92.28797293699245, + "target_egg_weight": 56.58098085770421, + "target_egg_mass": 52.43691607207049 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1879, + "max_depletion": 2.627489091697176, + "min_uniformity": 82.66289615405532, + "max_cv": 10.820852039399298, + "week": 20, + "feed_intake": 102 + } + } + ] + }, + { + "id": 13, + "name": "Standard Laying 2024", + "project_category": "LAYING", + "created_at": "Wed Aug 21 2024 16:53:00 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 19, + "production_standard_details": { + "target_hen_day_production": 90.64987149673148, + "target_hen_house_production": 84.72381158749832, + "target_egg_weight": 52.66407930502588, + "target_egg_mass": 48.67508874158 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1640, + "max_depletion": 1.0327075188137618, + "min_uniformity": 81.06885977450052, + "max_cv": 10.554487690853291, + "week": 17, + "feed_intake": 103 + } + } + ] + }, + { + "id": 15, + "name": "Standard Laying 2025", + "project_category": "LAYING", + "created_at": "Wed Jun 19 2024 22:53:30 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 18, + "production_standard_details": { + "target_hen_day_production": 93.92688146007806, + "target_hen_house_production": 88.99021279347687, + "target_egg_weight": 52.34548967695446, + "target_egg_mass": 47.022424468842786 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1613, + "max_depletion": 1.4131114163932998, + "min_uniformity": 87.70472314168066, + "max_cv": 10.854404697694157, + "week": 19, + "feed_intake": 113 + } + } + ] + }, + { + "id": 4, + "name": "Standard Growing 2025", + "project_category": "GROWING", + "created_at": "Fri Aug 02 2024 20:01:03 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 10, + "production_standard_uniformity_details": { + "target_mean_bw": 1679, + "max_depletion": 1.6915361117048733, + "min_uniformity": 86.90679412785661, + "max_cv": 9.332617207000094, + "week": 17, + "feed_intake": 103 + } + } + ] + }, + { + "id": 4, + "name": "Standard Laying 2024", + "project_category": "LAYING", + "created_at": "Wed Jun 12 2024 11:17:31 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 17, + "production_standard_details": { + "target_hen_day_production": 80.64302567936814, + "target_hen_house_production": 89.82086172466285, + "target_egg_weight": 55.226688911717915, + "target_egg_mass": 53.11072600271201 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1874, + "max_depletion": 2.438323895989795, + "min_uniformity": 84.30289784580617, + "max_cv": 8.222592209557122, + "week": 12, + "feed_intake": 105 + } + } + ] + }, + { + "id": 6, + "name": "Standard Laying 2025", + "project_category": "LAYING", + "created_at": "Mon Mar 18 2024 10:48:49 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 13, + "production_standard_details": { + "target_hen_day_production": 82.2346989800578, + "target_hen_house_production": 90.75391628121226, + "target_egg_weight": 57.499497168597166, + "target_egg_mass": 47.20514521984387 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1831, + "max_depletion": 1.074492532699157, + "min_uniformity": 85.74444671505677, + "max_cv": 10.858199896316137, + "week": 7, + "feed_intake": 112 + } + } + ] + }, + { + "id": 7, + "name": "Standard Laying 2024", + "project_category": "LAYING", + "created_at": "Sat Mar 30 2024 14:29:22 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 2, + "production_standard_details": { + "target_hen_day_production": 90.49925722287992, + "target_hen_house_production": 89.55923007437376, + "target_egg_weight": 58.22187327861563, + "target_egg_mass": 54.45919757347778 + }, + "production_standard_uniformity_details": { + "target_mean_bw": 1809, + "max_depletion": 2.2870196905499673, + "min_uniformity": 83.61968975899043, + "max_cv": 10.012889742382296, + "week": 6, + "feed_intake": 115 + } + } + ] + }, + { + "id": 11, + "name": "Standard Growing 2024", + "project_category": "GROWING", + "created_at": "Fri Jan 19 2024 06:05:53 GMT+0700 (Western Indonesia Time)", + "updated_at": "2025-12-26T23:13:21Z", + "created_user": { + "id": 1, + "name": "Super Admin", + "email": "superadmin@mbugroup.id" + }, + "details": [ + { + "week": 17, + "production_standard_uniformity_details": { + "target_mean_bw": 1803, + "max_depletion": 2.3862272943774725, + "min_uniformity": 88.37012562585544, + "max_cv": 9.948053043223062, + "week": 16, + "feed_intake": 114 + } + } + ] + } +] diff --git a/src/dummy/master-data/production-standard.dummy.ts b/src/dummy/master-data/production-standard.dummy.ts new file mode 100644 index 00000000..451f2b46 --- /dev/null +++ b/src/dummy/master-data/production-standard.dummy.ts @@ -0,0 +1,46 @@ +/** + * Dummy data for ProductionStandard[] + * Generated from: master-data-standar-produksi.json + * + * This file is auto-generated. Do not edit manually. + */ + +import { ProductionStandard } from '../../types/api/master-data/production-standard'; +import { BaseApiResponse } from '@/types/api/api-general'; +import dummyData from './production-standard.dummy.json'; + +/** + * Get dummy ProductionStandard[] data + * @returns Promise with BaseApiResponse containing ProductionStandard[] + */ +export async function getDummyAllFetcher(): Promise< + BaseApiResponse +> { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + status: 'success', + message: 'Data retrieved successfully', + data: dummyData as unknown as ProductionStandard[], + }); + }, 500); + }); +} + +export async function getDummySingleFetcher( + id: number +): Promise> { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + code: 200, + status: 'success', + message: 'Data retrieved successfully', + data: dummyData.find( + (item) => item.id === id + ) as unknown as ProductionStandard, + }); + }, 500); + }); +} diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 66df717a..0540277b 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -64,6 +64,11 @@ import { Flock, UpdateFlockPayload, } from '@/types/api/master-data/flock'; +import { ProductionStandard } from '@/types/api/master-data/production-standard'; +import { + getDummyAllFetcher, + getDummySingleFetcher, +} from '@/dummy/master-data/production-standard.dummy'; export const UomApi = new BaseApiService< Uom, @@ -141,3 +146,25 @@ export const FlockApi = new BaseApiService< CreateFlockPayload, UpdateFlockPayload >('/master-data/flocks'); + +export class ProductionStandardApi extends BaseApiService< + ProductionStandard, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getAllFetcher() { + return await getDummyAllFetcher(); + } + + async getSingleFetcher(id: number) { + return await getDummySingleFetcher(id); + } +} + +export const productionStandardApi = new ProductionStandardApi( + '/master-data/production-standard' +); diff --git a/src/stores/form/form.store.ts b/src/stores/form/form.store.ts new file mode 100644 index 00000000..c3ea0dba --- /dev/null +++ b/src/stores/form/form.store.ts @@ -0,0 +1,23 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; + +import { FormStore } from '@/types/stores'; +import { createProductionStandardFormSlice } from '@/stores/form/slices/production-standard-form.slice'; + +export const useFormStore = create()( + devtools( + persist( + (...args) => ({ + ...createProductionStandardFormSlice(...args), + }), + { + name: 'production-standard-form-cache', + } + ), + { + name: 'FormStore', + } + ) +); diff --git a/src/stores/form/slices/production-standard-form.slice.ts b/src/stores/form/slices/production-standard-form.slice.ts new file mode 100644 index 00000000..1029ecba --- /dev/null +++ b/src/stores/form/slices/production-standard-form.slice.ts @@ -0,0 +1,67 @@ +import { StateCreator } from 'zustand'; +import { FormStore } from '@/types/stores'; + +export const createProductionStandardFormSlice: StateCreator< + FormStore, + [], + [], + FormStore +> = (set): FormStore => ({ + formData: null, + editMode: false, + editIndex: null, + + setFormData: (data) => + set(() => ({ + formData: data, + })), + + setEditMode: (mode, index) => + set(() => ({ + editMode: mode, + editIndex: index, + })), + + addDetail: (detail) => + set((state) => ({ + formData: state.formData + ? { + ...state.formData, + details: [...state.formData.details, detail], + } + : null, + })), + + updateDetail: (index, detail) => + set((state) => { + if (!state.formData) return state; + const newDetails = [...state.formData.details]; + newDetails[index] = detail; + return { + formData: { + ...state.formData, + details: newDetails, + }, + }; + }), + + deleteDetail: (index) => + set((state) => { + if (!state.formData) return state; + const newDetails = [...state.formData.details]; + newDetails.splice(index, 1); + return { + formData: { + ...state.formData, + details: newDetails, + }, + }; + }), + + clearCache: () => + set(() => ({ + formData: null, + editMode: false, + editIndex: null, + })), +}); diff --git a/src/types/api/master-data/production-standard.d.ts b/src/types/api/master-data/production-standard.d.ts new file mode 100644 index 00000000..44611bae --- /dev/null +++ b/src/types/api/master-data/production-standard.d.ts @@ -0,0 +1,33 @@ +import { CreatedUser } from '@/types/api/api-general'; + +export interface ProductionStandard { + id: number; + name: string; + project_category: string; + created_at: string; + updated_at: string; + created_user: CreatedUser; + details: StandardDetails[]; +} + +export interface StandardDetails { + week: number; + standard_growth_details: StandardGrowthDetails; + production_standard_details: ProductionStandardDetails; +} + +export interface ProductionStandardDetails { + target_hen_day_production: number; + target_hen_house_production: number; + target_egg_weight: number; + target_egg_mass: number; +} + +export interface StandardGrowthDetails { + target_mean_bw: number; + max_depletion: number; + min_uniformity: number; + max_cv: number; + week: number; + feed_intake: number; +} diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 37b252fe..96b681c0 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -1,3 +1,5 @@ +import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema'; + type MainUiSlice = { mainDrawerOpen: boolean; setMainDrawerOpen: (open: boolean) => void; @@ -13,3 +15,28 @@ type DrawerUISlice = { }; export type UIStore = MainUiSlice & DrawerUISlice; + +type ProductionStandardFormSlice = { + formData: { + name: string; + project_category: string; + details: ProductionStandardRepeaterFormSchemaValues[]; + } | null; + editMode: boolean; + editIndex: number | null; + setFormData: (data: { + name: string; + project_category: string; + details: ProductionStandardRepeaterFormSchemaValues[]; + }) => void; + setEditMode: (mode: boolean, index: number | null) => void; + addDetail: (detail: ProductionStandardRepeaterFormSchemaValues) => void; + updateDetail: ( + index: number, + detail: ProductionStandardRepeaterFormSchemaValues + ) => void; + deleteDetail: (index: number) => void; + clearCache: () => void; +}; + +export type FormStore = ProductionStandardFormSlice;