Merge branch 'development' into feat/FE/US-77/transfer-to-laying

This commit is contained in:
ValdiANS
2025-10-27 13:07:09 +07:00
35 changed files with 4772 additions and 181 deletions
@@ -61,32 +61,24 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
.min(1, 'Biaya minimal 1!')
.typeError('Biaya harus berupa angka!')
.test(
'one-of-cost-fields',
'Biaya pengiriman atau biaya per item wajib diisi!',
function (value) {
const { delivery_cost_per_item } = this.parent;
return (
(value !== undefined && value > 0) ||
(delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
);
}
),
.test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
const { delivery_cost_per_item } = this.parent;
return (
(value !== undefined && value > 0) ||
(delivery_cost_per_item !== undefined && delivery_cost_per_item > 0)
);
}),
delivery_cost_per_item: Yup.number()
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
.min(1, 'Biaya per item minimal 1!')
.typeError('Biaya per item harus berupa angka!')
.test(
'one-of-cost-fields',
'Biaya pengiriman atau biaya per item wajib diisi!',
function (value) {
const { delivery_cost } = this.parent;
return (
(value !== undefined && value > 0) ||
(delivery_cost !== undefined && delivery_cost > 0)
);
}
),
.test('one-of-cost-fields', 'Wajib diisi salah satu!', function (value) {
const { delivery_cost } = this.parent;
return (
(value !== undefined && value > 0) ||
(delivery_cost !== undefined && delivery_cost > 0)
);
}),
document_path: Yup.string().optional(),
document_index: Yup.number().optional(),
document: Yup.mixed<File | string>()
@@ -7,8 +7,8 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { FormHeader } from '@/components/helper/form/FormHeader';
import { FormActions } from '@/components/helper/form/FormActions';
import {
@@ -29,6 +29,7 @@ import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouseApi } from '@/services/api/inventory';
import { toast } from 'react-hot-toast';
import FileInput from '@/components/input/FileInput';
import CheckboxInput from '@/components/input/CheckboxInput';
interface MovementFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -217,7 +218,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) {
return {
isError: false,
errorMessage: undefined,
errorMessage: '',
};
}
@@ -229,7 +230,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return {
isError: touchedField && Boolean(errorField?.[column as string]),
errorMessage: touchedField ? errorField?.[column as string] : undefined,
errorMessage:
touchedField && errorField?.[column as string]
? errorField[column as string]
: '',
};
};
@@ -246,7 +250,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
if (!touchedDelivery?.products || !errorDelivery?.products) {
return {
isError: false,
errorMessage: undefined,
errorMessage: '',
};
}
@@ -255,7 +259,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
return {
isError: Boolean(touchedField && errorField),
errorMessage: touchedField ? errorField : undefined,
errorMessage: touchedField && errorField ? errorField : '',
};
};
@@ -706,7 +710,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
value={formik.values.source_warehouse}
onChange={(val) => {
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue(
'source_warehouse_id',
(val as WarehouseOptionType)?.value
@@ -764,7 +770,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang'
value={formik.values.destination_warehouse}
onChange={(val) => {
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
(val as WarehouseOptionType)?.value
@@ -831,30 +839,53 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<tr>
{type !== 'detail' && (
<th>
<input
type='checkbox'
className='checkbox'
checked={
formik.values.products?.length ===
selectedProducts.length &&
formik.values.products?.length > 0
}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts(
formik.values.products?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedProducts([]);
<div className='flex justify-center'>
<CheckboxInput
name='select-all-products'
checked={
formik.values.products?.length ===
selectedProducts.length &&
formik.values.products?.length > 0
}
}}
/>
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts(
formik.values.products?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedProducts([]);
}
}}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</div>
</th>
)}
<th>Produk</th>
<th>Qty</th>
<th>
Produk
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Qty
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{type !== 'detail' && <th>Aksi</th>}
</tr>
</thead>
@@ -863,23 +894,30 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<tr key={`product-row-${idx}-${product.product_id}`}>
{type !== 'detail' && (
<td>
<input
type='checkbox'
className='checkbox'
checked={selectedProducts.includes(idx)}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts([
...selectedProducts,
idx,
]);
} else {
setSelectedProducts(
selectedProducts.filter((i) => i !== idx)
);
}
}}
/>
<div className='flex justify-center'>
<CheckboxInput
name={`product-${idx}`}
checked={selectedProducts.includes(idx)}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts([
...selectedProducts,
idx,
]);
} else {
setSelectedProducts(
selectedProducts.filter((i) => i !== idx)
);
}
}}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</div>
</td>
)}
<td>
@@ -887,10 +925,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={product.product ?? undefined}
onChange={(val) => {
formik.setFieldTouched(
`products.${idx}.product`,
true
);
formik.setFieldValue(
`products.${idx}.product`,
val
);
formik.setFieldTouched(
`products.${idx}.product_id`,
true
);
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
@@ -911,7 +957,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isClearable
{...isRepeaterInputError(
'products',
'product',
'product_id',
idx
)}
className={{
@@ -954,17 +1000,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => removeProduct(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
<div className='flex flex-col items-start gap-2'>
<Button
type='button'
color='error'
onClick={() => removeProduct(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</div>
</td>
)}
</tr>
@@ -1006,43 +1054,106 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
{/* Deliveries table */}
<div className='card bg-base-100 shadow mb-4'>
<div className='card-body'>
<h2 className='card-title mb-4'>Pengiriman</h2>
<h2 className='card-title mb-8'>Pengiriman</h2>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
{type !== 'detail' && (
<th>
<input
type='checkbox'
className='checkbox'
checked={
formik.values.deliveries?.length ===
selectedDeliveries.length &&
formik.values.deliveries?.length > 0
}
onChange={(e) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedDeliveries([]);
<div className='flex justify-center'>
<CheckboxInput
name='select-all-deliveries'
checked={
formik.values.deliveries?.length ===
selectedDeliveries.length &&
formik.values.deliveries?.length > 0
}
}}
/>
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedDeliveries([]);
}
}}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</div>
</th>
)}
<th>Produk</th>
<th>Qty</th>
<th>Supplier</th>
<th>Plat Nomor</th>
<th>
Produk
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Qty
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Supplier
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Plat Nomor
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>Dokumen</th>
<th>Biaya Pengiriman (Rp.)</th>
<th>Biaya Per Item (Rp.)</th>
<th>Nama Sopir</th>
<th>
Biaya Pengiriman (Rp.)
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Biaya Per Item (Rp.)
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Nama Sopir
<span
className='tooltip tooltip-error tooltip-bottom z-[9999]'
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
{type !== 'detail' && <th>Aksi</th>}
</tr>
</thead>
@@ -1051,23 +1162,32 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<tr key={`delivery-row-${idx}`}>
{type !== 'detail' && (
<td>
<input
type='checkbox'
className='checkbox'
checked={selectedDeliveries.includes(idx)}
onChange={(e) => {
if (e.target.checked) {
setSelectedDeliveries([
...selectedDeliveries,
idx,
]);
} else {
setSelectedDeliveries(
selectedDeliveries.filter((i) => i !== idx)
);
}
}}
/>
<div className='flex justify-center'>
<CheckboxInput
name={`delivery-${idx}`}
checked={selectedDeliveries.includes(idx)}
onChange={(
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries([
...selectedDeliveries,
idx,
]);
} else {
setSelectedDeliveries(
selectedDeliveries.filter(
(i) => i !== idx
)
);
}
}}
classNames={{
wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm',
}}
/>
</div>
</td>
)}
<td>
@@ -1075,10 +1195,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={delivery.products[0]?.product ?? undefined}
onChange={(val) => {
formik.setFieldTouched(
`deliveries.${idx}.products.0.product`,
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product_id`,
(val as OptionType)?.value
@@ -1087,6 +1215,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
options={getFilteredProductWarehouseOptions()}
isDisabled={type === 'detail'}
isClearable
isError={
isDeliveryProductInputError(idx, 0, 'product_id')
.isError
}
errorMessage={
isDeliveryProductInputError(idx, 0, 'product_id')
.errorMessage
}
className={{
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
@@ -1122,10 +1258,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required
value={delivery.supplier}
onChange={(val) => {
formik.setFieldTouched(
`deliveries.${idx}.supplier`,
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.supplier_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier_id`,
(val as OptionType)?.value
@@ -1136,6 +1280,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isLoading={isLoadingSuppliers}
isDisabled={type === 'detail'}
isClearable
{...isRepeaterInputError(
'deliveries',
'supplier_id',
idx
)}
className={{
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
@@ -1163,27 +1312,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</td>
<td>
{type === 'detail' ? (
<Button
color='primary'
className='w-full min-w-52 flex items-center justify-center gap-2'
disabled={!delivery.document_path}
href={delivery.document_path ?? undefined}
target='_blank'
rel='noopener noreferrer'
>
{delivery.document_path ? (
<>
<Icon
icon='material-symbols:file-open-outline'
width={20}
height={20}
/>
Lihat Dokumen
</>
) : (
'-'
)}
</Button>
<>
<div className='flex flex-col items-start gap-2'>
<Button
color='primary'
className='w-full min-w-52 flex items-center justify-center gap-2'
disabled={!delivery.document_path}
href={delivery.document_path ?? undefined}
target='_blank'
rel='noopener noreferrer'
>
{delivery.document_path ? (
<>
<Icon
icon='material-symbols:file-open-outline'
width={20}
height={20}
/>
Lihat Dokumen
</>
) : (
'-'
)}
</Button>
</div>
</>
) : (
<FileInput
name={`deliveries.${idx}.document`}
@@ -1194,6 +1347,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
toast.error(
'Ukuran dokumen maksimal 2 MB!'
);
e.target.value = '';
return;
}
formik.setFieldValue(
@@ -1215,15 +1369,17 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
</td>
<td>
<TextInput
<NumberInput
required
type='number'
name={`deliveries.${idx}.delivery_cost`}
value={delivery.delivery_cost || ''}
onChange={(e) =>
handleDeliveryCostChange(idx, e.target.value)
}
onBlur={formik.handleBlur}
maskType='currency'
decimals={0}
min={0}
{...isRepeaterInputError(
'deliveries',
'delivery_cost',
@@ -1231,14 +1387,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-48',
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
}}
/>
</td>
<td>
<TextInput
<NumberInput
required
type='number'
name={`deliveries.${idx}.delivery_cost_per_item`}
value={delivery.delivery_cost_per_item || ''}
onChange={(e) =>
@@ -1248,6 +1404,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)
}
onBlur={formik.handleBlur}
maskType='currency'
decimals={0}
min={0}
{...isRepeaterInputError(
'deliveries',
'delivery_cost_per_item',
@@ -1255,7 +1414,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
)}
readOnly={type === 'detail'}
className={{
wrapper: 'w-full min-w-48',
wrapper:
'w-full min-w-52 md:min-w-72 lg:min-w-80',
}}
/>
</td>
@@ -1280,17 +1440,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => removeDelivery(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
<div className='flex flex-col items-start gap-2'>
<Button
type='button'
color='error'
onClick={() => removeDelivery(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</div>
</td>
)}
</tr>
@@ -0,0 +1,325 @@
'use client';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
import { ChickinApi, ProjectFlockApi } from '@/services/api/production';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Chickin } from '@/types/api/production/chickin';
import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
import { useState } from 'react';
import useSWR from 'swr';
import ChickinForm from './form/ChickinForm';
const ChickinTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedChickin, setSelectedChickin] = useState<Chickin | undefined>(
undefined
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
const chickinModal = useModal();
// Data Fetching
const {
data: chickins,
isLoading,
mutate: refreshChickins,
} = useSWR(
`${ChickinApi.basePath}${getTableFilterQueryString()}`,
ChickinApi.getAllFetcher
);
const {
data: projectFlocks,
isLoading: isLoadingProjectFlocks,
} = useSWR(
`${ProjectFlockApi.basePath}${getTableFilterQueryString()}`,
ProjectFlockApi.getAllFetcher
);
const searchChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', event.target.value);
setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
try {
await ChickinApi.delete(selectedChickin?.id as number);
refreshChickins();
deleteModal.closeModal();
} finally {
setIsDeleteLoading(false);
}
};
return (
<>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<Button
href='/production/chickin/add?projectFlockId=1'
color='primary'
>
<Icon icon='uil:plus' width={24} height={24} />
Tambah
</Button>
<DebouncedTextInput
name='search'
placeholder='Cari Chickin'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<TableRowSizeSelector
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
</div>
</div>
<Table<Chickin>
data={isResponseSuccess(chickins) ? chickins?.data : []}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.project_flock_kandang?.kandang.name,
header: 'Kandang',
},
{
accessorFn: (row) => row.quantity,
header: 'Jumlah Chickin',
},
{
accessorFn: (row) => row.chick_in_date,
header: 'Tanggal Chickin',
cell: (props) => {
if (props.row.original.chick_in_date) {
return new Date(props.row.original.chick_in_date).toLocaleDateString(
'id-ID'
);
} else {
return '-';
}
}
},
{
accessorFn: (row) => row.note,
header: 'Catatan',
},
{
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 = () => {
setSelectedChickin(props.row.original);
deleteModal.openModal();
};
const editClickHandler = () => {
setSelectedChickin(props.row.original);
chickinModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
editClickHandler={editClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
editClickHandler={editClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(chickins) ? chickins?.meta?.page : 0}
totalItems={
isResponseSuccess(chickins) ? chickins?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(chickins) && chickins?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Chickin ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
onClick: confirmationModalDeleteClickHandler,
isLoading: isDeleteLoading,
color: 'error',
}}
/>
<Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'>
<h1 className='text-xl font-semibold text-center mb-6'>
Chickin Kandang - { selectedChickin?.project_flock_kandang && selectedChickin?.project_flock_kandang.kandang?.name}
</h1>
<Button
color='error'
variant='link'
onClick={chickinModal.closeModal}
>
<Icon
className='text-black'
icon='uil:times'
width={24}
height={24}
/>
</Button>
</div>
<ChickinForm initialValues={selectedChickin} formType='edit' afterSubmit={() => {
refreshChickins()
chickinModal.closeModal()
}}/>
</Modal>
</>
);
};
const RowOptionsMenu = ({
type = 'dropdown',
props,
editClickHandler,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Chickin, unknown>;
editClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type == 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<Button
href={`/production/chickin/detail?projectFlockId=${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
variant='ghost'
color='warning'
className='justify-start text-sm'
onClick={editClickHandler}
>
<Icon icon='mdi:pencil-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>
</div>
);
};
export default ChickinTable;
@@ -0,0 +1,11 @@
import * as Yup from 'yup';
export const ChickinFormSchema = Yup.object({
chick_in_date: Yup.string().required('Tanggal masuk wajib diisi!'),
note: Yup.string().required('Catatan wajib diisi!'),
quantity: Yup.number().min(1, 'Jumlah wajib diisi!').required('Jumlah wajib diisi!'),
})
export type ChickinFormValues = Yup.InferType<typeof ChickinFormSchema>;
export const UpdateChickinFormSchema = ChickinFormSchema;
@@ -0,0 +1,206 @@
'use client';
import Button from '@/components/Button';
import {
Chickin,
CreateChickinPayload,
UpdateChickinPayload,
} from '@/types/api/production/chickin';
import {
ChickinFormSchema,
ChickinFormValues,
UpdateChickinFormSchema,
} from '@/components/pages/production/chickin/form/ChickinForm.schema';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFormik } from 'formik';
import { ChickinApi } from '@/services/api/production';
import DateInput from '@/components/input/DateInput';
import { isResponseError } from '@/lib/api-helper';
import toast from 'react-hot-toast';
import { Icon } from '@iconify/react';
import TextArea from '@/components/input/TextArea';
import TextInput from '@/components/input/TextInput';
interface ChickinFormProps {
formType?: 'add' | 'detail' | 'edit';
initialValues?: Chickin;
afterSubmit?: () => void;
}
const ChickinForm = ({
formType = 'add',
initialValues,
afterSubmit,
}: ChickinFormProps) => {
// Helper Function
const formatDateForInput = (dateString?: string): string => {
if (!dateString) return '';
return new Date(dateString).toISOString().split('T')[0];
};
// State
const [chickinFormErrorMessage, setChickinFormErrorMessage] = useState('');
// Initial Value
const formikInitialValue = useMemo<ChickinFormValues>(() => {
return {
chick_in_date: formatDateForInput(initialValues?.chick_in_date) ?? '',
note: initialValues?.note ?? `Catatan Chickin ${initialValues?.project_flock_kandang?.project_flock.flock.name}`,
quantity: initialValues?.quantity ?? 1,
};
}, [initialValues]);
// Handle Submit Function
const handleCreate = useCallback(
async (
payload: CreateChickinPayload,
afterSubmit: (() => void) | undefined
) => {
const res = await ChickinApi.create(payload);
if (isResponseError(res)) {
setChickinFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
afterSubmit?.();
},
[]
);
const handleUpdate = useCallback(
async (
payload: UpdateChickinPayload,
afterSubmit: (() => void) | undefined
) => {
const res = await ChickinApi.update(
payload.project_flock_kandang_id as number,
payload
);
if (isResponseError(res)) {
setChickinFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
afterSubmit?.();
},
[]
);
// Formik
const formik = useFormik<ChickinFormValues>({
initialValues: formikInitialValue,
enableReinitialize: true,
validationSchema:
formType === 'edit' ? UpdateChickinFormSchema : ChickinFormSchema,
onSubmit: async (values) => {
// reset error message
setChickinFormErrorMessage('');
if (initialValues?.project_flock_kandang?.id == undefined) {
return;
}
// create payload
const payload = {
chick_in_date: values.chick_in_date,
project_flock_kandang_id: initialValues?.project_flock_kandang?.id,
};
// cek type form yang disubmit
switch (formType) {
case 'add':
handleCreate(payload, afterSubmit);
break;
case 'edit':
handleUpdate(payload, afterSubmit);
break;
default:
break;
}
},
});
// Initialize Formik
const { setValues: formikSetValues } = formik;
useEffect(() => {
formikSetValues(formikInitialValue);
}, [formikSetValues, formikInitialValue]);
return (
<>
<form
className='min-h-48 flex flex-col gap-4'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
<DateInput
value={formik.values.chick_in_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
name='chick_in_date'
label='Tanggal Chickin'
required
isError={
formik.touched.chick_in_date && Boolean(formik.errors.chick_in_date)
}
errorMessage={formik.errors.chick_in_date}
/>
<TextInput
value={formik.values.quantity}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
name='quantity'
label='Jumlah Chickin'
required
isError={formik.touched.quantity && Boolean(formik.errors.quantity)}
errorMessage={formik.errors.quantity}
type='number'
disabled
/>
<TextArea
required
label='Catatan'
name='note'
value={formik.values.note}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={formik.touched.note && Boolean(formik.errors.note)}
errorMessage={formik.errors.note}
/>
{initialValues?.project_flock_kandang?.id == undefined && (
<p className='text-error'>Project Flock Kandang tidak ditemukan.</p>
)}
{chickinFormErrorMessage && (
<div
role='alert'
className='alert alert-error'
onClick={() => {
setChickinFormErrorMessage('');
}}
>
<Icon icon='mdi:times' />
<span>{chickinFormErrorMessage}</span>
</div>
)}
<div className='flex justify-center mt-auto gap-2'>
<Button color='warning' type='reset'>
Reset
</Button>
<Button
type='submit'
isLoading={formik.isSubmitting}
disabled={
!formik.isValid ||
formik.isSubmitting ||
!initialValues?.project_flock_kandang?.id
}
>
Submit
</Button>
</div>
</form>
</>
);
};
export default ChickinForm;
@@ -56,6 +56,15 @@ const RowOptionsMenu = ({
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/production/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
</Button>
{/* <Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost'
@@ -0,0 +1,495 @@
'use client';
import { useCallback, useMemo, useState } from 'react';
import { Icon } from '@iconify/react';
import { SortingState } from '@tanstack/react-table';
import { cn } from '@/lib/helper';
import { useModal } from '@/components/Modal';
import Button from '@/components/Button';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { OptionType } from '@/components/input/SelectInput';
import { ROWS_OPTIONS } from '@/config/constant';
import { TableToolbar } from '@/components/table/TableToolbar';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import Table from '@/components/Table';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import { type CellContext } from '@tanstack/react-table';
import { type Recording } from '@/types/api/production/recording';
const dummyRecordings: Recording[] = [
{
id: 1,
flock: {
id: 1,
name: 'Flock Recording 1',
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
recording_date: '2024-01-01',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
coop: {
id: 1,
name: 'Coop 1',
status: 'ACTIVE',
location: {
id: 1,
name: 'Location 1',
address: 'Jl. Contoh No. 1',
area: {
id: 1,
name: 'Area 1',
},
},
pic: {
id: 1,
id_user: 1,
email: 'pic@example.com',
name: 'PIC User',
},
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
feed_data: [
{
feed_name: 'Feed 1',
feed_qty: 100,
feed_stock: 500,
},
],
body_weight: [
{
chicken_weight: 2.5,
chicken_count: 1000,
average_chicken_weight: 2.5,
},
],
vaccination: [
{
vaccine_name: 'Vaccine 1',
total_stock: 200,
used_stock: 150,
},
],
mortality: [
{
condition: 'NORMAL',
count: 5,
},
],
created_at: '2024-01-01',
updated_at: '2024-01-01',
created_user: {
id: 1,
id_user: 1,
email: 'admin@example.com',
name: 'Admin',
},
},
];
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Recording, unknown>;
deleteClickHandler: () => void;
}) => {
return (
<div
tabIndex={type === 'dropdown' ? 0 : undefined}
className={cn(
{
'dropdown-content': type === 'dropdown',
'mt-2': type === 'collapse',
},
'p-2.5 mr-2 flex flex-col gap-1 bg-base-100 rounded-box z-10 border border-black/10 shadow'
)}
>
<Button
href={`recording/detail/?recordingId=${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={`recording/detail/edit/?recordingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</div>
);
};
const RecordingTable = () => {
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedRecordings, setSelectedRecordings] = useState<number[]>([]);
const [, setSelectedRecording] = useState<Recording | undefined>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false);
const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false);
const singleDeleteModal = useModal();
const bulkApproveModal = useModal();
const bulkRejectModal = useModal();
const searchChangeHandler = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
setPage(1);
},
[]
);
const pageSizeChangeHandler = useCallback(
(val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
},
[]
);
const paginatedData = useMemo(() => {
const filteredData = dummyRecordings.filter(
(recording) =>
recording.flock.name.toLowerCase().includes(search.toLowerCase()) ||
recording.location.name.toLowerCase().includes(search.toLowerCase()) ||
recording.coop.name.toLowerCase().includes(search.toLowerCase())
);
const start = (page - 1) * pageSize;
return filteredData.slice(start, start + pageSize);
}, [page, pageSize, search]);
const bulkApproveHandler = async () => {
setIsBulkApproveLoading(true);
console.log(
'Approved recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => {
setIsBulkApproveLoading(false);
setSelectedRecordings([]);
bulkApproveModal.closeModal();
}, 1000);
};
const bulkRejectHandler = async () => {
setIsBulkRejectLoading(true);
console.log(
'Rejected recordings:',
paginatedData.filter((_, idx) => selectedRecordings.includes(idx))
);
setTimeout(() => {
setIsBulkRejectLoading(false);
setSelectedRecordings([]);
bulkRejectModal.closeModal();
}, 1000);
};
const singleDeleteHandler = async () => {
setIsDeleteLoading(true);
setTimeout(() => {
setIsDeleteLoading(false);
singleDeleteModal.closeModal();
}, 1000);
};
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: 'recording/add',
label: 'Tambah Recording',
}}
search={{
value: search,
onChange: searchChangeHandler,
placeholder: 'Cari Recording',
}}
/>
<TableRowSizeSelector
value={pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
</div>
{/* Bulk action buttons */}
<div className={'flex justify-end items-center'}>
{selectedRecordings.length > 0 && (
<div className='flex gap-2 mb-4'>
<Button
type='button'
color='success'
onClick={() => bulkApproveModal.openModal()}
className='flex items-center gap-2'
>
<Icon
icon='material-symbols:check-circle-outline'
width={20}
height={20}
/>
Approve ({selectedRecordings.length})
</Button>
<Button
type='button'
color='error'
onClick={() => bulkRejectModal.openModal()}
className='flex items-center gap-2'
>
<Icon
icon='material-symbols:cancel-outline'
width={20}
height={20}
/>
Reject ({selectedRecordings.length})
</Button>
</div>
)}
<ConfirmationModal
ref={bulkApproveModal.ref}
type='success'
text={`Apakah anda yakin ingin menyetujui ${selectedRecordings.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'success',
isLoading: isBulkApproveLoading,
onClick: bulkApproveHandler,
}}
/>
<ConfirmationModal
ref={bulkRejectModal.ref}
type='error'
text={`Apakah anda yakin ingin menolak ${selectedRecordings.length} data Recording yang dipilih?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isBulkRejectLoading,
onClick: bulkRejectHandler,
}}
/>
</div>
<Table
data={paginatedData}
columns={[
{
id: 'select',
accessorKey: 'id',
header: ({ table }) => (
<input
type='checkbox'
className='checkbox'
checked={
table.getRowModel().rows.length > 0 &&
table
.getRowModel()
.rows.every((row) => selectedRecordings.includes(row.index))
}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings(
table.getRowModel().rows.map((row) => row.index)
);
} else {
setSelectedRecordings([]);
}
}}
/>
),
cell: ({ row }) => (
<input
type='checkbox'
className='checkbox'
checked={selectedRecordings.includes(row.index)}
onChange={(e) => {
if (e.target.checked) {
setSelectedRecordings([...selectedRecordings, row.index]);
} else {
setSelectedRecordings(
selectedRecordings.filter((i) => i !== row.index)
);
}
}}
/>
),
},
{
header: '#',
cell: (props) => pageSize * (page - 1) + props.row.index + 1,
},
{
accessorKey: 'flock.name',
header: 'Flock',
},
{
accessorKey: 'recording_date',
header: 'Tanggal Recording',
cell: (props) =>
new Date(props.row.original.recording_date).toLocaleDateString(),
},
{
accessorKey: 'location.name',
header: 'Lokasi',
},
{
accessorKey: 'coop.name',
header: 'Kandang',
},
{
accessorKey: 'mortality',
header: 'Total Mortality',
cell: (props) =>
props.row.original.mortality.reduce(
(acc, curr) => acc + curr.count,
0
),
},
{
header: 'Aksi',
cell: (props: CellContext<Recording, unknown>) => {
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 = () => {
setSelectedRecording(props.row.original);
singleDeleteModal.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>
)}
</>
);
},
},
]}
pageSize={pageSize}
page={page}
totalItems={dummyRecordings.length}
onPageChange={setPage}
isLoading={false}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20': paginatedData.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
<ConfirmationModal
ref={singleDeleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Recording ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: singleDeleteHandler,
}}
/>
</div>
);
};
export default RecordingTable;
@@ -0,0 +1,212 @@
import * as Yup from 'yup';
import { RECORDING_FLAG_OPTIONS } from '@/config/constant';
import { Recording } from '@/types/api/production/recording';
export const RecordingFormSchema = Yup.object({
flock: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
flock_id: Yup.number()
.default(0)
.typeError('Flock wajib diisi!')
.test(
'is-valid-flock',
'Flock wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Flock wajib diisi!'),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
location_id: Yup.number()
.default(0)
.typeError('Lokasi wajib diisi!')
.test(
'is-valid-location',
'Lokasi wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Lokasi wajib diisi!'),
coop: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
coop_id: Yup.number()
.default(0)
.typeError('Kandang wajib diisi!')
.test(
'is-valid-coop',
'Kandang wajib diisi!',
(value) => value !== undefined && value !== null && value > 0
)
.required('Kandang wajib diisi!'),
recording_date: Yup.date()
.required('Tanggal recording wajib diisi')
.typeError('Format tanggal tidak valid'),
feed_data: Yup.array()
.of(
Yup.object({
feed_id: Yup.string().required('Nama pakan wajib diisi!'),
feed_qty: Yup.mixed<number | ''>().notRequired(),
feed_stock: Yup.number()
.required('Jumlah pakan yang digunakan wajib diisi!')
.min(1, 'Jumlah pakan minimal 1!')
.typeError('Jumlah pakan yang digunakan harus berupa angka!')
.test(
'is-not-exceed-qty',
'Jumlah pakan yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { feed_qty } = this.parent;
if (value === undefined) return true;
if (
feed_qty === undefined ||
feed_qty === '' ||
typeof feed_qty !== 'number'
)
return true;
return value <= feed_qty;
}
),
})
)
.min(1, 'Minimal harus ada 1 data pakan!')
.required('Data pakan wajib diisi!'),
body_weight: Yup.array()
.of(
Yup.object({
chicken_weight: Yup.number()
.required('Berat ayam wajib diisi!')
.min(1, 'Berat ayam minimal 1 gram!')
.typeError('Berat ayam harus berupa angka!'),
chicken_count: Yup.number()
.required('Jumlah ayam wajib diisi!')
.min(1, 'Jumlah ayam minimal 1 ekor!')
.typeError('Jumlah ayam harus berupa angka!'),
average_chicken_weight: Yup.number()
.required('Rata-rata berat ayam wajib diisi!')
.min(1, 'Rata-rata berat ayam minimal 1 gram!')
.typeError('Rata-rata berat ayam harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data bobot badan!')
.required('Data bobot badan wajib diisi!'),
vaccination: Yup.array()
.of(
Yup.object({
vaccine_id: Yup.string().required('Nama vaksin wajib diisi!'),
total_stock: Yup.mixed<number | ''>().notRequired(),
used_stock: Yup.number()
.required('Jumlah vaksin yang digunakan wajib diisi!')
.min(1, 'Jumlah vaksin minimal 1!')
.typeError('Jumlah vaksin yang digunakan harus berupa angka!')
.test(
'is-not-exceed-total',
'Jumlah vaksin yang digunakan tidak boleh melebihi stok tersedia!',
function (value) {
const { total_stock } = this.parent;
if (value === undefined) return true;
if (
total_stock === undefined ||
total_stock === '' ||
typeof total_stock !== 'number'
)
return true;
return value <= total_stock;
}
),
})
)
.min(1, 'Minimal harus ada 1 data vaksinasi!')
.required('Data vaksinasi wajib diisi!'),
mortality: Yup.array()
.of(
Yup.object({
condition: Yup.mixed<string>()
.oneOf(
RECORDING_FLAG_OPTIONS.map((opt) => opt.value),
'Kondisi tidak valid!'
)
.required('Kondisi wajib diisi!'),
count: Yup.number()
.required('Jumlah mortalitas wajib diisi!')
.min(1, 'Jumlah mortalitas minimal 1 ekor!')
.typeError('Jumlah mortalitas harus berupa angka!'),
})
)
.min(1, 'Minimal harus ada 1 data mortalitas!')
.required('Data mortalitas wajib diisi!'),
});
export const UpdateRecordingFormSchema = RecordingFormSchema;
export type RecordingFormValues = Yup.InferType<typeof RecordingFormSchema>;
export const getRecordingFormInitialValues = (
initialValues?: Recording
): RecordingFormValues => ({
flock: initialValues?.flock
? {
value: initialValues.flock.id,
label: initialValues.flock.name,
}
: null,
flock_id: initialValues?.flock?.id ?? 0,
location: initialValues?.location
? {
value: initialValues.location.id,
label: initialValues.location.name,
}
: null,
location_id: initialValues?.location?.id ?? 0,
coop: initialValues?.coop
? {
value: initialValues.coop.id,
label: initialValues.coop.name,
}
: null,
coop_id: initialValues?.coop?.id ?? 0,
recording_date: initialValues?.recording_date
? new Date(initialValues.recording_date)
: new Date(),
feed_data: initialValues?.feed_data
? initialValues.feed_data.map((feed) => ({
feed_id: feed.feed_name,
feed_qty: feed.feed_qty,
feed_stock: feed.feed_stock,
}))
: [
{
feed_id: '',
feed_qty: '',
feed_stock: 0,
},
],
body_weight: initialValues?.body_weight ?? [
{
chicken_weight: 0,
chicken_count: 0,
average_chicken_weight: 0,
},
],
vaccination: initialValues?.vaccination
? initialValues.vaccination.map((vaccine) => ({
vaccine_id: vaccine.vaccine_name,
total_stock: vaccine.total_stock,
used_stock: vaccine.used_stock,
}))
: [
{
vaccine_id: '',
total_stock: '',
used_stock: 0,
},
],
mortality: initialValues?.mortality ?? [
{
condition: '',
count: 0,
},
],
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { useModal } from '@/components/Modal';
import { RecordingApi } from '@/services/api/production';
import {
CreateRecordingPayload,
UpdateRecordingPayload,
} from '@/types/api/production/recording';
import { isResponseError } from '@/lib/api-helper';
export const useRecordingFormHandlers = (initialValuesId?: number) => {
const router = useRouter();
const deleteModal = useModal();
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const createRecordingHandler = useCallback(
async (payload: CreateRecordingPayload) => {
const res = await RecordingApi.create(payload);
if (isResponseError(res)) {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.push('/flock/recording');
},
[router]
);
const updateRecordingHandler = useCallback(
async (recordingId: number, payload: UpdateRecordingPayload) => {
const res = await RecordingApi.update(recordingId, payload);
if (res?.status === 'error') {
setRecordingFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
router.refresh();
router.push('/flock/recording');
},
[router]
);
const deleteRecordingClickHandler = useCallback(() => {
deleteModal.openModal();
}, [deleteModal]);
const confirmationModalDeleteClickHandler = useCallback(async () => {
if (!initialValuesId) return;
setIsDeleteLoading(true);
await RecordingApi.delete(initialValuesId);
deleteModal.closeModal();
toast.success('Successfully delete Recording!');
setIsDeleteLoading(false);
router.push('/flock/recording');
}, [deleteModal, initialValuesId, router]);
return {
deleteModal,
recordingFormErrorMessage,
isDeleteLoading,
createRecordingHandler,
updateRecordingHandler,
deleteRecordingClickHandler,
confirmationModalDeleteClickHandler,
};
};