feat(FE): add master data production standard, slicing form and index table

This commit is contained in:
randy-ar
2025-12-27 03:23:03 +07:00
parent 4ddd1dc8e3
commit 663c1dea14
16 changed files with 1684 additions and 0 deletions
@@ -0,0 +1,13 @@
'use client';
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
const AddProductionStandardPage = () => {
return (
<>
<ProductionStandardForm formType='add' />
</>
);
};
export default AddProductionStandardPage;
@@ -0,0 +1,11 @@
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
const EditProductionStandardPage = () => {
return (
<>
<ProductionStandardForm formType='edit' />
</>
);
};
export default EditProductionStandardPage;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,11 @@
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
const DetailProductionStandardPage = () => {
return (
<>
<ProductionStandardForm formType='detail' />
</>
);
};
export default DetailProductionStandardPage;
@@ -0,0 +1,11 @@
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
const ProductionStandardPage = () => {
return (
<div className='w-full'>
<ProductionStandardTable />
</div>
);
};
export default ProductionStandardPage;
@@ -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<ProductionStandard, unknown>;
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RowOptionsMenuWrapper>
);
};
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 (
<>
<div className='flex flex-col gap-6 p-6'>
<div className='flex flex-row gap-6 justify-end'>
<Button href='/master-data/production-standard/add'>
<Icon icon='mdi:plus' /> Tambah
</Button>
</div>
<Table<ProductionStandard>
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 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
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'
),
}}
/>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Production Standard ini (${selectedProductionStandard?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</>
);
};
export default ProductionStandardTable;
@@ -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
>;
@@ -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<number | null>(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<ProductionStandardFormValues>({
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<ProductionStandardRepeaterFormSchemaValues>({
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<ColumnDef<TableRowsType>[]>(() => {
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 (
<div className='flex gap-2'>
<Button
variant='outline'
color='warning'
className='p-2'
onClick={() => handleEditClick(row.row.index)}
>
<Icon icon='mdi:pencil' />
</Button>
<Button
variant='outline'
color='error'
className='p-2'
onClick={() => handleRemoveRow(row.row.index)}
>
<Icon icon='mdi:delete' />
</Button>
</div>
);
},
},
];
}, []);
// ===== 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<HTMLInputElement>) => {
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 (
<div className='p-6 gap-6 flex flex-col'>
<FormHeader
title='Tambah Data Standard Produksi'
backUrl='/master-data/production-standard'
/>
<div className='grid sm:grid-cols-2 gap-6'>
<TextInput
label='Nama'
name='name'
placeholder='Nama Standard Produksi'
value={formik.values.name}
onChange={handleNameChange}
onBlur={formik.handleBlur}
errorMessage={formik.errors.name as string}
isError={Boolean(formik.errors.name)}
/>
<SelectInput
label='Kategori Proyek'
value={FLOCK_CATEGORY_OPTIONS.find(
(option) => 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)}
/>
</div>
<Table<TableRowsType>
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 (
<tr key={`placeholder-${row.index}`}>
<td colSpan={11} className='p-6 text-center text-gray-400'>
{row.original.placeHolder}
</td>
</tr>
);
}
return (
<tr
key={`row-${row.index}`}
className='sticky bottom-0 bg-base-100 shadow-lg'
>
<td colSpan={11} className='p-6'>
<form
className='h-full w-full flex flex-col justify-end'
onSubmit={repeaterFormik.handleSubmit}
onReset={repeaterFormik.handleReset}
>
<div className='grid grid-cols-9 gap-4 items-start'>
<NumberInput
name='week'
label='Week'
placeholder='1'
value={repeaterFormik.values.week}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
errorMessage={repeaterFormik.errors.week as string}
isError={
Boolean(repeaterFormik.errors.week) &&
Boolean(repeaterFormik.touched.week)
}
disabled={formType === 'detail'}
/>
<NumberInput
name='production_standard_details.target_hen_day_production'
label='Hen Day'
placeholder='1'
value={
repeaterFormik.values.production_standard_details
.target_hen_day_production
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />}
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'}
/>
<NumberInput
name='production_standard_details.target_hen_house_production'
label='Hen House'
placeholder='1'
value={
repeaterFormik.values.production_standard_details
.target_hen_house_production
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
Butir
</div>
}
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'}
/>
<NumberInput
name='production_standard_details.target_egg_weight'
label='Egg Weight'
placeholder='1'
value={
repeaterFormik.values.production_standard_details
.target_egg_weight
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
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'}
/>
<NumberInput
name='production_standard_details.target_egg_mass'
label='Egg Mass'
placeholder='1'
value={
repeaterFormik.values.production_standard_details
.target_egg_mass
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
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'}
/>
<NumberInput
name='standard_growth_details.target_mean_bw'
label='Mean BW'
placeholder='1'
value={
repeaterFormik.values.standard_growth_details
.target_mean_bw
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
gr
</div>
}
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'}
/>
<NumberInput
name='standard_growth_details.max_depletion'
label='Max Depletion'
placeholder='1'
value={
repeaterFormik.values.standard_growth_details
.max_depletion
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />}
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'}
/>
<NumberInput
name='standard_growth_details.min_uniformity'
label='Min Uniformity'
placeholder='1'
value={
repeaterFormik.values.standard_growth_details
.min_uniformity
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={<Icon icon='mdi:percent' />}
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'}
/>
<NumberInput
name='standard_growth_details.feed_intake'
label='Feed Intake'
placeholder='1'
value={
repeaterFormik.values.standard_growth_details
.feed_intake
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
endAdornment={
<div className='w-full h-full flex items-center justify-center'>
gr/ekor
</div>
}
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'}
/>
</div>
<div className='flex justify-center mt-6 gap-2'>
{editMode && (
<Button
type='button'
color='error'
variant='outline'
className='min-w-24'
onClick={handleCancelEdit}
>
<Icon icon='mdi:close' /> Batal
</Button>
)}
{formType !== 'detail' && (
<Button
type='submit'
color={editMode ? 'warning' : 'success'}
className='min-w-24'
>
<Icon icon={editMode ? 'mdi:pencil' : 'mdi:plus'} />{' '}
{editMode ? 'Edit Data' : 'Tambah Data'}
</Button>
)}
{/* Should not be absolute */}
<Button
type='button'
variant='outline'
color='primary'
onClick={toggleTableHeight}
className='absolute bottom-6 right-6'
>
<Icon
icon={
isTableExpanded
? 'mdi:chevron-up'
: 'mdi:chevron-down'
}
/>
{isTableExpanded ? 'Tutup Tabel' : 'Lihat Semua'}
</Button>
</div>
</form>
</td>
</tr>
);
}
return null;
}}
/>
<form
className='flex justify-between mt-6 gap-2'
onSubmit={formik.handleSubmit}
>
<div>Simpan Total {formik.values.details?.length || 0} Data</div>
<div className='flex gap-2'>
<Button
type='button'
className='min-w-24'
color='warning'
onClick={handleReset}
>
Reset
</Button>
<Button type='submit' className='min-w-24'>
Submit
</Button>
</div>
</form>
</div>
);
};
export default ProductionStandardForm;
+4
View File
@@ -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;
@@ -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
}
}
]
}
]
@@ -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<ProductionStandard[]>
> {
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<BaseApiResponse<ProductionStandard>> {
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);
});
}
+27
View File
@@ -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'
);
+23
View File
@@ -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<FormStore>()(
devtools(
persist(
(...args) => ({
...createProductionStandardFormSlice(...args),
}),
{
name: 'production-standard-form-cache',
}
),
{
name: 'FormStore',
}
)
);
@@ -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,
})),
});
+33
View File
@@ -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;
}
+27
View File
@@ -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;