mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 21:41:57 +00:00
feat(FE): add master data production standard, slicing form and index table
This commit is contained in:
@@ -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;
|
||||
+62
@@ -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;
|
||||
Reference in New Issue
Block a user