mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-26 08:15:44 +00:00
Merge branch 'hotfix/adjustment-recording-fifo-stock' into 'development'
[HOTFIX/FE] Adjustment FIFO Stock V2 (Stock Related Usage) See merge request mbugroup/lti-web-client!351
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -26,6 +26,10 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import toast from 'react-hot-toast';
|
||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||
@@ -38,6 +42,62 @@ import {
|
||||
AdjustmentFilterType,
|
||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
popoverPosition: 'bottom' | 'top';
|
||||
props: CellContext<InventoryAdjustment, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const popoverId = `adjustment#${props.row.original.id}`;
|
||||
const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`;
|
||||
|
||||
const closePopover = () => {
|
||||
document.getElementById(popoverId)?.hidePopover();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
position={popoverPosition === 'bottom' ? 'bottom-start' : 'left'}
|
||||
className='w-full max-w-40 rounded-xl border border-base-content/5 shadow-sm'
|
||||
>
|
||||
<div className='flex flex-col bg-base-100 rounded-xl'>
|
||||
<RequirePermission permissions='lti.inventory.delete'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteClickHandler();
|
||||
closePopover();
|
||||
}}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InventoryAdjustmentTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
@@ -182,12 +242,39 @@ const InventoryAdjustmentTable = () => {
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
||||
const {
|
||||
data: inventoryAdjustments,
|
||||
isLoading,
|
||||
mutate: refreshAdjustments,
|
||||
} = useSWR(
|
||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||
InventoryAdjustmentApi.getAllFetcher
|
||||
);
|
||||
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const response = await InventoryAdjustmentApi.delete(
|
||||
selectedAdjustment?.id as number
|
||||
);
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Adjustment!');
|
||||
refreshAdjustments();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Adjustment');
|
||||
}
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [selectedAdjustment, setSelectedAdjustment] = useState<
|
||||
InventoryAdjustment | undefined
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const singleDeleteModal = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
@@ -302,8 +389,39 @@ const InventoryAdjustmentTable = () => {
|
||||
header: 'Oleh',
|
||||
accessorFn: (row) => row.created_user?.name ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Aksi',
|
||||
cell: (props: CellContext<InventoryAdjustment, 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 = () => {
|
||||
setSelectedAdjustment(props.row.original);
|
||||
singleDeleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<RowOptionsMenu
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[tableFilterState.pageSize, tableFilterState.page]
|
||||
[
|
||||
tableFilterState.pageSize,
|
||||
tableFilterState.page,
|
||||
singleDeleteModal,
|
||||
setSelectedAdjustment,
|
||||
]
|
||||
);
|
||||
|
||||
const updateSortingFilter = useCallback(
|
||||
@@ -532,6 +650,21 @@ const InventoryAdjustmentTable = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={singleDeleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Adjustment ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: singleDeleteHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
@@ -21,6 +21,8 @@ import { cn } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from '@/components/Button';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
@@ -41,9 +43,11 @@ import {
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: {
|
||||
popoverPosition: 'bottom' | 'top';
|
||||
props: CellContext<Movement, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const popoverId = `movement#${props.row.original.id}`;
|
||||
const popoverAnchorName = `--anchor-movement#${props.row.original.id}`;
|
||||
@@ -83,6 +87,20 @@ const RowOptionsMenu = ({
|
||||
Detail
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
<RequirePermission permissions='lti.inventory.transfer.delete'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
deleteClickHandler();
|
||||
closePopover();
|
||||
}}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='p-3 justify-start text-sm font-semibold w-full focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon icon='mdi:delete-outline' width={20} height={20} />
|
||||
Delete
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</div>
|
||||
@@ -206,12 +224,37 @@ const MovementTable = () => {
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [selectedMovement, setSelectedMovement] = useState<
|
||||
Movement | undefined
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const singleDeleteModal = useModal();
|
||||
|
||||
const { data: movements, isLoading } = useSWR(
|
||||
const {
|
||||
data: movements,
|
||||
isLoading,
|
||||
mutate: refreshMovements,
|
||||
} = useSWR(
|
||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||
MovementApi.getAllFetcher
|
||||
);
|
||||
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
const response = await MovementApi.delete(selectedMovement?.id as number);
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Movement!');
|
||||
refreshMovements();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Movement');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
@@ -275,16 +318,27 @@ const MovementTable = () => {
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedMovement(props.row.original);
|
||||
singleDeleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<RowOptionsMenu
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[tableFilterState.pageSize, tableFilterState.page]
|
||||
[
|
||||
tableFilterState.pageSize,
|
||||
tableFilterState.page,
|
||||
singleDeleteModal,
|
||||
setSelectedMovement,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -455,6 +509,21 @@ const MovementTable = () => {
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
ref={singleDeleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data Movement ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: singleDeleteHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -82,6 +82,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: number;
|
||||
warehouse_name: string;
|
||||
quantity: number;
|
||||
transfer_available_qty?: number;
|
||||
}
|
||||
|
||||
// ===== USE SELECT HOOKS =====
|
||||
@@ -379,6 +380,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: formik.values.source_warehouse_id
|
||||
? formik.values.source_warehouse_id.toString()
|
||||
: '',
|
||||
transfer_context: 'inventory_transfer',
|
||||
stock_mode: 'exclude_chickin',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -391,6 +394,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
warehouse_id: pw.warehouse.id,
|
||||
warehouse_name: pw.warehouse.name,
|
||||
quantity: pw.quantity,
|
||||
transfer_available_qty: pw.transfer_available_qty,
|
||||
}))
|
||||
: [];
|
||||
}, [productWarehouses]);
|
||||
@@ -834,6 +838,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}, [formik.values.products, formik.values.deliveries]);
|
||||
|
||||
const getAvailableStock = useCallback(
|
||||
(productId: number) => {
|
||||
if (type === 'detail') return 0;
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
(pw) => pw.product_id === productId
|
||||
);
|
||||
|
||||
return (
|
||||
productWarehouse?.transfer_available_qty ??
|
||||
productWarehouse?.quantity ??
|
||||
0
|
||||
);
|
||||
},
|
||||
[productWarehouseOptions, type]
|
||||
);
|
||||
|
||||
const getTotalStock = useCallback(
|
||||
(productId: number) => {
|
||||
if (type === 'detail') return 0;
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
@@ -844,6 +864,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
[productWarehouseOptions, type]
|
||||
);
|
||||
|
||||
const hasAvailableQty = useCallback(
|
||||
(productId: number) => {
|
||||
const productWarehouse = productWarehouseOptions.find(
|
||||
(pw) => pw.product_id === productId
|
||||
);
|
||||
return productWarehouse?.transfer_available_qty !== undefined;
|
||||
},
|
||||
[productWarehouseOptions]
|
||||
);
|
||||
|
||||
const getProductQtyBottomLabel = useCallback(
|
||||
(productIdx: number) => {
|
||||
if (type === 'detail') return undefined;
|
||||
@@ -851,16 +881,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
if (!product || !product.product_id) return undefined;
|
||||
|
||||
const availableStock = getAvailableStock(product.product_id);
|
||||
const totalStock = getTotalStock(product.product_id);
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
const remainingStock = availableStock - requestedQty;
|
||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||
|
||||
if (requestedQty > 0) {
|
||||
if (isAyamProduct) {
|
||||
return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`;
|
||||
}
|
||||
return `Sisa: ${formatNumber(remainingStock)}`;
|
||||
}
|
||||
|
||||
if (isAyamProduct) {
|
||||
return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`;
|
||||
}
|
||||
|
||||
return `Tersedia: ${formatNumber(availableStock)}`;
|
||||
},
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
[
|
||||
formik.values.products,
|
||||
getAvailableStock,
|
||||
getTotalStock,
|
||||
hasAvailableQty,
|
||||
type,
|
||||
]
|
||||
);
|
||||
|
||||
const getDeliveryProductQtyBottomLabel = useCallback(
|
||||
@@ -922,15 +967,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
if (!product || !product.product_id) return null;
|
||||
|
||||
const availableStock = getAvailableStock(product.product_id);
|
||||
const totalStock = getTotalStock(product.product_id);
|
||||
const requestedQty = Number(product.product_qty) || 0;
|
||||
const isAyamProduct = hasAvailableQty(product.product_id);
|
||||
|
||||
if (requestedQty > availableStock) {
|
||||
if (isAyamProduct) {
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`;
|
||||
}
|
||||
return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[formik.values.products, getAvailableStock, type]
|
||||
[
|
||||
formik.values.products,
|
||||
getAvailableStock,
|
||||
getTotalStock,
|
||||
hasAvailableQty,
|
||||
type,
|
||||
]
|
||||
);
|
||||
|
||||
const validateDeliveryQty = useCallback(
|
||||
|
||||
@@ -21,6 +21,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { useFormik } from 'formik';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton';
|
||||
import Table from '@/components/Table';
|
||||
import { type Recording } from '@/types/api/production/recording';
|
||||
import { getRecordingRestriction } from './recording-utils';
|
||||
import { RecordingApi } from '@/services/api/production';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -105,30 +107,75 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const isRecordingEditable = (recording: Recording) => {
|
||||
if (
|
||||
recording.executed_at &&
|
||||
recording.project_flock?.project_flock_category === 'GROWING'
|
||||
) {
|
||||
const isGrowingCategory =
|
||||
recording.project_flock?.project_flock_category === 'GROWING';
|
||||
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||
if (isGrowingLockedByLaying) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentIsLaying =
|
||||
recording.project_flock?.project_flock_category === 'LAYING';
|
||||
|
||||
const restriction = getRecordingRestriction(
|
||||
recording.is_laying,
|
||||
recording.is_transition,
|
||||
currentIsLaying
|
||||
);
|
||||
|
||||
if (restriction.isLocked) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRecordingRestrictionInfo = (recording: Recording) => {
|
||||
const isGrowingCategory =
|
||||
recording.project_flock?.project_flock_category === 'GROWING';
|
||||
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||
if (isGrowingLockedByLaying) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason:
|
||||
'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying',
|
||||
};
|
||||
}
|
||||
|
||||
const currentIsLaying =
|
||||
recording.project_flock?.project_flock_category === 'LAYING';
|
||||
|
||||
return getRecordingRestriction(
|
||||
recording.is_laying,
|
||||
recording.is_transition,
|
||||
currentIsLaying
|
||||
);
|
||||
};
|
||||
|
||||
const isApproved = isRecordingApproved(props.row.original);
|
||||
const isRejected = isRecordingRejected(props.row.original);
|
||||
const isEditable = isRecordingEditable(props.row.original);
|
||||
const restrictionInfo = getRecordingRestrictionInfo(props.row.original);
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Tooltip
|
||||
content={restrictionInfo.isLocked ? restrictionInfo.lockReason : ''}
|
||||
position='top'
|
||||
>
|
||||
<PopoverButton
|
||||
tabIndex={0}
|
||||
variant='ghost'
|
||||
color='none'
|
||||
popoverTarget={popoverId}
|
||||
anchorName={popoverAnchorName}
|
||||
className={restrictionInfo.isLocked ? 'text-error' : ''}
|
||||
>
|
||||
<Icon icon='material-symbols:more-vert' width={16} height={16} />
|
||||
</PopoverButton>
|
||||
</Tooltip>
|
||||
|
||||
<PopoverContent
|
||||
id={popoverId}
|
||||
@@ -560,12 +607,17 @@ const RecordingTable = () => {
|
||||
const singleDeleteHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await RecordingApi.delete(selectedRecording?.id as number);
|
||||
refreshRecordings();
|
||||
const response = await RecordingApi.delete(selectedRecording?.id as number);
|
||||
|
||||
singleDeleteModal.closeModal();
|
||||
toast.success('Successfully delete Recording!');
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Recording!');
|
||||
refreshRecordings();
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Recording');
|
||||
}
|
||||
};
|
||||
|
||||
const approveHandler = async (notes: string) => {
|
||||
@@ -761,11 +813,30 @@ const RecordingTable = () => {
|
||||
{
|
||||
header: 'Kategori',
|
||||
cell: (props) => {
|
||||
const isTransition = props.row.original.is_transition;
|
||||
const category =
|
||||
props.row.original.project_flock?.project_flock_category;
|
||||
if (!category) return '-';
|
||||
props.row.original.project_flock?.project_flock_category ||
|
||||
'GROWING';
|
||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||
return <StatusBadge color={color} text={formatTitleCase(category)} />;
|
||||
|
||||
const isGrowingLocked =
|
||||
category === 'GROWING' && props.row.original.is_laying;
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<StatusBadge color={color} text={formatTitleCase(category)} />
|
||||
{isTransition && (
|
||||
<span className='text-xs text-warning font-medium'>
|
||||
(Transisi)
|
||||
</span>
|
||||
)}
|
||||
{isGrowingLocked && (
|
||||
<span className='text-xs text-error font-medium'>
|
||||
(Penguncian)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
} from '@/components/pages/production/recording/form/RecordingForm.schema';
|
||||
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import { formatDate, formatNumber, cn } from '@/lib/helper';
|
||||
import toast from 'react-hot-toast';
|
||||
import ApprovalSteps, {
|
||||
useApprovalSteps,
|
||||
@@ -79,7 +79,9 @@ import {
|
||||
GROWING_RECORDING_APPROVAL_LINE,
|
||||
LAYING_RECORDING_APPROVAL_LINE,
|
||||
} from '@/config/approval-line';
|
||||
import { PROJECT_FLOCK_STATUS } from '@/config/constant';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import { getRecordingRestriction } from '../recording-utils';
|
||||
|
||||
interface RecordingFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -242,6 +244,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const calculateWeek = useCallback(
|
||||
(day: number): number => {
|
||||
if (
|
||||
productionStandards?.details &&
|
||||
productionStandards.details.length > 0
|
||||
) {
|
||||
const firstWeek = productionStandards.details[0].week;
|
||||
|
||||
const weekOffset = Math.ceil(day / 7) - 1;
|
||||
return firstWeek + weekOffset;
|
||||
}
|
||||
|
||||
return Math.ceil(day / 7);
|
||||
},
|
||||
[productionStandards]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const checkProductionStandardModalOpen = () => {
|
||||
const isOpen = productionStandardModal.ref.current?.open || false;
|
||||
@@ -272,73 +291,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return recording?.approval?.action === 'REJECTED';
|
||||
}, []);
|
||||
|
||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||
if (
|
||||
recording?.executed_at &&
|
||||
recording?.project_flock?.project_flock_category === 'GROWING'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
// ===== PAYLOAD CREATION HELPERS =====
|
||||
const createGrowingPayload = useCallback(
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
const depletions = values.depletions
|
||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||
.map((depletion) => ({
|
||||
product_warehouse_id: depletion.product_warehouse_id!,
|
||||
qty: Number(depletion.qty) || 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
record_date: values.record_date,
|
||||
stocks: (values.stocks ?? []).map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
})),
|
||||
...(depletions && depletions.length > 0 && { depletions }),
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const createLayingPayload = useCallback(
|
||||
(values: RecordingLayingFormValues) => {
|
||||
const depletions = values.depletions
|
||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||
.map((depletion) => ({
|
||||
product_warehouse_id: depletion.product_warehouse_id!,
|
||||
qty: Number(depletion.qty) || 0,
|
||||
}));
|
||||
|
||||
const eggs = values.eggs
|
||||
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
||||
.map((egg) => ({
|
||||
product_warehouse_id: egg.product_warehouse_id!,
|
||||
qty: Number(egg.qty) || 0,
|
||||
weight:
|
||||
typeof egg.weight === 'number'
|
||||
? egg.weight
|
||||
: parseFloat(String(egg.weight)) || 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
record_date: values.record_date,
|
||||
stocks: values.stocks.map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
})),
|
||||
...(depletions && depletions.length > 0 && { depletions }),
|
||||
...(eggs && eggs.length > 0 && { eggs }),
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FORM HANDLERS =====
|
||||
const createRecordingHandler = useCallback(
|
||||
async (
|
||||
@@ -380,11 +332,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
if (!initialValues?.id) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
await RecordingApi.delete(initialValues.id);
|
||||
const response = await RecordingApi.delete(initialValues.id);
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete Recording!');
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
if (isResponseSuccess(response)) {
|
||||
toast.success(response?.message || 'Successfully delete Recording!');
|
||||
router.push('/production/recording');
|
||||
} else {
|
||||
toast.error(response?.message || 'Failed to delete Recording');
|
||||
}
|
||||
}, [deleteModal, initialValues?.id, router]);
|
||||
|
||||
// ===== API DATA FETCHING =====
|
||||
@@ -403,16 +361,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
loadMore: loadMoreProjectFlocks,
|
||||
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||
location_id: selectedProjectFlockLocationId,
|
||||
status: PROJECT_FLOCK_STATUS.AKTIF,
|
||||
});
|
||||
|
||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||
if (!selectedProjectFlock || !selectedKandang) return null;
|
||||
if (!selectedProjectFlock || !selectedKandang || !selectedRecordDate)
|
||||
return null;
|
||||
const params = new URLSearchParams({
|
||||
project_flock_id: selectedProjectFlock.value.toString(),
|
||||
kandang_id: selectedKandang.value.toString(),
|
||||
record_date: selectedRecordDate,
|
||||
});
|
||||
return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`;
|
||||
}, [selectedProjectFlock, selectedKandang]);
|
||||
}, [selectedProjectFlock, selectedKandang, selectedRecordDate]);
|
||||
|
||||
const { data: projectFlockKandangLookupData } = useSWR(
|
||||
projectFlockKandangLookupUrl,
|
||||
@@ -444,13 +405,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
() => ProductionStandardApi.getSingle(productionStandardId!)
|
||||
);
|
||||
|
||||
const { data: productionStandardForAdd } = useSWR(
|
||||
type === 'add' && productionStandardId
|
||||
? `production-standard-add-${productionStandardId}`
|
||||
: null,
|
||||
() => ProductionStandardApi.getSingle(productionStandardId!)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (productionStandard?.status === 'success') {
|
||||
setProductionStandards(
|
||||
productionStandard.data as ProductionStandard | null
|
||||
);
|
||||
} else if (productionStandardForAdd?.status === 'success') {
|
||||
setProductionStandards(
|
||||
productionStandardForAdd.data as ProductionStandard | null
|
||||
);
|
||||
}
|
||||
}, [productionStandard]);
|
||||
}, [productionStandard, productionStandardForAdd]);
|
||||
|
||||
const projectFlockKandangDetailUrl = useMemo(() => {
|
||||
if (
|
||||
@@ -476,6 +448,159 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
? projectFlockKandangDetailData.data
|
||||
: undefined;
|
||||
|
||||
const selectedProjectFlockKandangId = useMemo(() => {
|
||||
if (type === 'add') {
|
||||
return projectFlockKandangLookup?.project_flock_kandang_id ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
projectFlockKandangDetail?.id ??
|
||||
initialValues?.project_flock?.project_flock_kandang_id ??
|
||||
null
|
||||
);
|
||||
}, [
|
||||
type,
|
||||
projectFlockKandangLookup,
|
||||
projectFlockKandangDetail,
|
||||
initialValues,
|
||||
]);
|
||||
|
||||
// ===== TRANSITION RESTRICTION LOGIC =====
|
||||
const isTransitionPeriod = useMemo(() => {
|
||||
return (
|
||||
initialValues?.is_transition ??
|
||||
projectFlockKandangLookup?.is_transition ??
|
||||
false
|
||||
);
|
||||
}, [initialValues, projectFlockKandangLookup]);
|
||||
|
||||
const recordingRestriction = useMemo(() => {
|
||||
let isLaying: boolean;
|
||||
if (initialValues?.is_laying !== undefined) {
|
||||
isLaying = initialValues.is_laying;
|
||||
} else if (projectFlockKandangLookup?.is_laying !== undefined) {
|
||||
isLaying = projectFlockKandangLookup.is_laying;
|
||||
} else {
|
||||
isLaying =
|
||||
projectFlockKandangDetail?.project_flock?.category === 'LAYING' ||
|
||||
false;
|
||||
}
|
||||
|
||||
const isTransition =
|
||||
initialValues?.is_transition ??
|
||||
projectFlockKandangLookup?.is_transition ??
|
||||
false;
|
||||
|
||||
const currentIsLaying =
|
||||
type === 'edit'
|
||||
? projectFlockKandangDetail?.project_flock?.category === 'LAYING'
|
||||
: projectFlockKandangLookup?.project_flock?.category === 'LAYING';
|
||||
|
||||
return getRecordingRestriction(isLaying, isTransition, currentIsLaying);
|
||||
}, [
|
||||
initialValues,
|
||||
projectFlockKandangLookup,
|
||||
projectFlockKandangDetail,
|
||||
type,
|
||||
]);
|
||||
|
||||
// ===== PAYLOAD CREATION HELPERS =====
|
||||
const createGrowingPayload = useCallback(
|
||||
(values: RecordingGrowingFormValues) => {
|
||||
const depletions = recordingRestriction.canEditDepletion
|
||||
? values.depletions
|
||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||
.map((depletion) => ({
|
||||
product_warehouse_id: depletion.product_warehouse_id!,
|
||||
qty: Number(depletion.qty) || 0,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const stocks = recordingRestriction.canEditStock
|
||||
? (values.stocks ?? [])
|
||||
.filter((s) => s.product_warehouse_id && s.qty)
|
||||
.map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
record_date: values.record_date,
|
||||
...(stocks.length > 0 && { stocks }),
|
||||
...(depletions.length > 0 && { depletions }),
|
||||
};
|
||||
},
|
||||
[recordingRestriction.canEditStock, recordingRestriction.canEditDepletion]
|
||||
);
|
||||
|
||||
const createLayingPayload = useCallback(
|
||||
(values: RecordingLayingFormValues) => {
|
||||
const depletions = values.depletions
|
||||
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||
.map((depletion) => ({
|
||||
product_warehouse_id: depletion.product_warehouse_id!,
|
||||
qty: Number(depletion.qty) || 0,
|
||||
}));
|
||||
|
||||
const eggs = values.eggs
|
||||
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
|
||||
.map((egg) => ({
|
||||
product_warehouse_id: egg.product_warehouse_id!,
|
||||
qty: Number(egg.qty) || 0,
|
||||
weight:
|
||||
typeof egg.weight === 'number'
|
||||
? egg.weight
|
||||
: parseFloat(String(egg.weight)) || 0,
|
||||
}));
|
||||
|
||||
const stocks = recordingRestriction.canEditStock
|
||||
? values.stocks
|
||||
.filter((s) => s.product_warehouse_id && s.qty)
|
||||
.map((stock) => ({
|
||||
product_warehouse_id: stock.product_warehouse_id,
|
||||
qty: Number(stock.qty) || 0,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return {
|
||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||
record_date: values.record_date,
|
||||
...(stocks.length > 0 && { stocks }),
|
||||
...(depletions && depletions.length > 0 && { depletions }),
|
||||
...(eggs && eggs.length > 0 && { eggs }),
|
||||
};
|
||||
},
|
||||
[recordingRestriction.canEditStock]
|
||||
);
|
||||
|
||||
const isRecordingEditable = useCallback((recording?: Recording) => {
|
||||
if (!recording) return true;
|
||||
|
||||
const isGrowingCategory =
|
||||
recording.project_flock?.project_flock_category === 'GROWING';
|
||||
const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying;
|
||||
if (isGrowingLockedByLaying) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentIsLaying =
|
||||
recording.project_flock?.project_flock_category === 'LAYING';
|
||||
|
||||
const restriction = getRecordingRestriction(
|
||||
recording.is_laying,
|
||||
recording.is_transition,
|
||||
currentIsLaying
|
||||
);
|
||||
|
||||
if (restriction.isLocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const {
|
||||
options: stockProductOptions,
|
||||
rawData: stockProducts,
|
||||
@@ -581,15 +706,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return approvedProjectFlockKandangsData.data;
|
||||
}, [approvedProjectFlockKandangsData]);
|
||||
|
||||
const isLayingCategory =
|
||||
initialValues?.project_flock?.project_flock_category === 'LAYING' ||
|
||||
projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
|
||||
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
|
||||
const isLayingCategory = useMemo(() => {
|
||||
// Priority 1: initialValues category (for edit/detail mode)
|
||||
if (initialValues?.project_flock?.project_flock_category !== undefined) {
|
||||
return initialValues.project_flock.project_flock_category === 'LAYING';
|
||||
}
|
||||
|
||||
const isGrowingCategory =
|
||||
initialValues?.project_flock?.project_flock_category === 'GROWING' ||
|
||||
projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
|
||||
projectFlockKandangDetail?.project_flock?.category === 'GROWING';
|
||||
// Priority 2: projectFlockKandangLookup category (for add mode)
|
||||
if (projectFlockKandangLookup?.project_flock?.category !== undefined) {
|
||||
return projectFlockKandangLookup.project_flock.category === 'LAYING';
|
||||
}
|
||||
|
||||
// Priority 3: projectFlockKandangDetail (fallback for edit/detail mode)
|
||||
return (
|
||||
projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false
|
||||
);
|
||||
}, [
|
||||
initialValues?.project_flock?.project_flock_category,
|
||||
projectFlockKandangLookup,
|
||||
projectFlockKandangDetail,
|
||||
]);
|
||||
|
||||
const isGrowingCategory = !isLayingCategory;
|
||||
|
||||
const recordingApprovalLines = useMemo(() => {
|
||||
if (isLayingCategory) {
|
||||
@@ -637,8 +775,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
return options;
|
||||
}, [locationOptions, projectFlockKandangDetail, type]);
|
||||
|
||||
const isProjectFlockActive = useCallback((projectFlock: ProjectFlock) => {
|
||||
const approvalStepName = projectFlock.approval?.step_name
|
||||
?.trim()
|
||||
.toLowerCase();
|
||||
if (approvalStepName) {
|
||||
return approvalStepName === PROJECT_FLOCK_STATUS.AKTIF.toLowerCase();
|
||||
}
|
||||
|
||||
return (
|
||||
projectFlock.status?.trim().toLowerCase() ===
|
||||
PROJECT_FLOCK_STATUS.AKTIF.toLowerCase()
|
||||
);
|
||||
}, []);
|
||||
|
||||
const activeProjectFlockIDs = useMemo(() => {
|
||||
if (!isResponseSuccess(projectFlocksRawData)) return new Set<number>();
|
||||
|
||||
const data = projectFlocksRawData.data as ProjectFlock[];
|
||||
return new Set(
|
||||
data
|
||||
.filter((projectFlock) => isProjectFlockActive(projectFlock))
|
||||
.map((projectFlock) => projectFlock.id)
|
||||
);
|
||||
}, [projectFlocksRawData, isProjectFlockActive]);
|
||||
|
||||
const enhancedProjectFlockOptions = useMemo(() => {
|
||||
const options = [...projectFlockOptions];
|
||||
const options = projectFlockOptions.filter((option) => {
|
||||
if (type !== 'add') return true;
|
||||
return activeProjectFlockIDs.has(Number(option.value));
|
||||
});
|
||||
|
||||
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
|
||||
const currentProjectFlock = projectFlockKandangDetail.project_flock;
|
||||
@@ -654,7 +820,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [projectFlockOptions, projectFlockKandangDetail, type]);
|
||||
}, [
|
||||
projectFlockOptions,
|
||||
projectFlockKandangDetail,
|
||||
type,
|
||||
activeProjectFlockIDs,
|
||||
]);
|
||||
|
||||
const kandangOptions = useMemo(() => {
|
||||
let options: OptionType[] = [];
|
||||
@@ -762,8 +933,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
projectFlockKandangDetail,
|
||||
]);
|
||||
|
||||
const isProductWarehouseBelongsToSelectedProjectFlockKandang = useCallback(
|
||||
(productWarehouse: ProductWarehouse) => {
|
||||
if (!selectedProjectFlockKandangId) return false;
|
||||
|
||||
return (
|
||||
productWarehouse.project_flock_kandang?.id ===
|
||||
selectedProjectFlockKandangId
|
||||
);
|
||||
},
|
||||
[selectedProjectFlockKandangId]
|
||||
);
|
||||
|
||||
const scopedStockProductIds = useMemo(() => {
|
||||
if (!isResponseSuccess(stockProducts) || !selectedProjectFlockKandangId) {
|
||||
return new Set<number>();
|
||||
}
|
||||
|
||||
const data = stockProducts.data as unknown as ProductWarehouse[];
|
||||
return new Set(
|
||||
data
|
||||
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
|
||||
.map((product) => product.id)
|
||||
);
|
||||
}, [
|
||||
stockProducts,
|
||||
selectedProjectFlockKandangId,
|
||||
isProductWarehouseBelongsToSelectedProjectFlockKandang,
|
||||
]);
|
||||
|
||||
const unifiedStockProducts = useMemo(() => {
|
||||
const options = [...stockProductOptions];
|
||||
const options = selectedProjectFlockKandangId
|
||||
? stockProductOptions.filter((option) =>
|
||||
scopedStockProductIds.has(Number(option.value))
|
||||
)
|
||||
: [];
|
||||
|
||||
if (
|
||||
initialValues &&
|
||||
@@ -787,14 +991,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [stockProductOptions, initialValues, type]);
|
||||
}, [
|
||||
stockProductOptions,
|
||||
initialValues,
|
||||
type,
|
||||
selectedProjectFlockKandangId,
|
||||
scopedStockProductIds,
|
||||
]);
|
||||
|
||||
const depletionProducts = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
|
||||
if (isResponseSuccess(depletionProductsData) && selectedKandang) {
|
||||
if (
|
||||
isResponseSuccess(depletionProductsData) &&
|
||||
selectedProjectFlockKandangId
|
||||
) {
|
||||
const data = depletionProductsData.data as unknown as ProductWarehouse[];
|
||||
data.forEach((product) => {
|
||||
data
|
||||
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
|
||||
.forEach((product) => {
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: product.product.name,
|
||||
@@ -822,14 +1037,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [depletionProductsData, initialValues, type, selectedKandang]);
|
||||
}, [
|
||||
depletionProductsData,
|
||||
initialValues,
|
||||
type,
|
||||
selectedProjectFlockKandangId,
|
||||
isProductWarehouseBelongsToSelectedProjectFlockKandang,
|
||||
]);
|
||||
|
||||
const eggProducts = useMemo(() => {
|
||||
const options: OptionType[] = [];
|
||||
|
||||
if (isResponseSuccess(eggProductsData) && selectedKandang) {
|
||||
if (isResponseSuccess(eggProductsData) && selectedProjectFlockKandangId) {
|
||||
const data = eggProductsData.data as unknown as ProductWarehouse[];
|
||||
data.forEach((product) => {
|
||||
data
|
||||
.filter(isProductWarehouseBelongsToSelectedProjectFlockKandang)
|
||||
.forEach((product) => {
|
||||
options.push({
|
||||
value: product.id,
|
||||
label: product.product.name,
|
||||
@@ -854,7 +1077,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [eggProductsData, initialValues, type, selectedKandang]);
|
||||
}, [
|
||||
eggProductsData,
|
||||
initialValues,
|
||||
type,
|
||||
selectedProjectFlockKandangId,
|
||||
isProductWarehouseBelongsToSelectedProjectFlockKandang,
|
||||
]);
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formikInitialValues = useMemo(() => {
|
||||
@@ -924,6 +1153,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!recordingRestriction.canEditStock) {
|
||||
baseValues.stocks = [];
|
||||
}
|
||||
|
||||
if (!recordingRestriction.canEditDepletion) {
|
||||
baseValues.depletions = [];
|
||||
}
|
||||
|
||||
return baseValues;
|
||||
}, [
|
||||
initialValues,
|
||||
@@ -934,6 +1171,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
selectedLocation,
|
||||
selectedProjectFlock,
|
||||
selectedKandang,
|
||||
recordingRestriction.canEditStock,
|
||||
recordingRestriction.canEditDepletion,
|
||||
]);
|
||||
|
||||
const formik = useFormik<
|
||||
@@ -954,7 +1193,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
? UpdateRecordingGrowingFormSchema
|
||||
: RecordingGrowingFormSchema;
|
||||
}
|
||||
return schema.clone().concat(
|
||||
|
||||
if (!recordingRestriction.canEditStock) {
|
||||
schema = schema.shape({
|
||||
stocks: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
product_warehouse_id: Yup.number().optional(),
|
||||
qty: Yup.number().optional(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.default([]) as unknown as Yup.Schema<unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
if (!recordingRestriction.canEditDepletion) {
|
||||
schema = schema.shape({
|
||||
depletions: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
product_warehouse_id: Yup.number().optional(),
|
||||
qty: Yup.number().optional(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.default([]) as unknown as Yup.Schema<unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
return schema.concat(
|
||||
Yup.object().shape({
|
||||
project_flock_kandang_id: Yup.number().test(
|
||||
'not-already-recorded',
|
||||
@@ -1299,6 +1567,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setSelectedLocation(location);
|
||||
setSelectedProjectFlock(null);
|
||||
setSelectedKandang(null);
|
||||
setProductionStandards(null);
|
||||
setNextDayRecording(null);
|
||||
if (duplicateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
@@ -1323,6 +1593,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
|
||||
setSelectedProjectFlock(projectFlock);
|
||||
setSelectedKandang(null);
|
||||
setProductionStandards(null);
|
||||
setNextDayRecording(null);
|
||||
if (duplicateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
@@ -1343,6 +1615,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
formik.setFieldValue('kandang_id', kandangId);
|
||||
|
||||
setSelectedKandang(kandang);
|
||||
setProductionStandards(null);
|
||||
setNextDayRecording(null);
|
||||
if (duplicateErrorShown) {
|
||||
toast.dismiss();
|
||||
setDuplicateErrorShown(false);
|
||||
@@ -1497,7 +1771,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
}, [
|
||||
projectFlockKandangDetail,
|
||||
type,
|
||||
enhancedProjectFlockOptions,
|
||||
formik.values.project_flock_kandang_id,
|
||||
setFieldValue,
|
||||
]);
|
||||
@@ -1879,10 +2152,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
<p className='font-semibold'>
|
||||
{type === 'add'
|
||||
? nextDayRecording
|
||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})`
|
||||
? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${calculateWeek(nextDayRecording.next_day)})`
|
||||
: '-'
|
||||
: initialValues?.day
|
||||
? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})`
|
||||
? `Hari ke-${initialValues.day} (Minggu ke-${calculateWeek(initialValues.day)})`
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1956,18 +2229,18 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
<div>
|
||||
<span className='text-sm text-gray-600'>Kategori</span>
|
||||
<p className='font-semibold'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={
|
||||
initialValues.project_flock
|
||||
?.project_flock_category === 'LAYING'
|
||||
? 'info'
|
||||
: 'warning'
|
||||
}
|
||||
size='sm'
|
||||
>
|
||||
{initialValues.project_flock?.project_flock_category}
|
||||
{(() => {
|
||||
const category =
|
||||
initialValues.project_flock?.project_flock_category ||
|
||||
'GROWING';
|
||||
const color =
|
||||
category === 'LAYING' ? 'info' : 'warning';
|
||||
return (
|
||||
<Badge variant='soft' color={color} size='sm'>
|
||||
{category}
|
||||
</Badge>
|
||||
);
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2103,9 +2376,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
{type === 'detail' && initialValues && (
|
||||
<div
|
||||
className={`grid gap-6 mb-6 grid-cols-1 ${
|
||||
initialValues.project_flock?.project_flock_category === 'LAYING'
|
||||
? 'xl:grid-cols-3'
|
||||
: 'xl:grid-cols-2'
|
||||
initialValues.is_laying ? 'xl:grid-cols-3' : 'xl:grid-cols-2'
|
||||
}`}
|
||||
>
|
||||
{/* FCR Section */}
|
||||
@@ -2196,8 +2467,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
{/* Egg Production Section - Only for LAYING category */}
|
||||
{type === 'detail' &&
|
||||
initialValues &&
|
||||
initialValues.project_flock?.project_flock_category ===
|
||||
'LAYING' && (
|
||||
initialValues.is_laying && (
|
||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||
<div className='px-4 py-3 border-b border-gray-200'>
|
||||
<span className='card-title font-bold text-xl'>
|
||||
@@ -2292,7 +2562,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stocks Table */}
|
||||
{/* Stocks Table - Only show if can edit stock or has data */}
|
||||
{(recordingRestriction.canEditStock ||
|
||||
(type === 'detail' && formik.values.stocks?.length > 0)) && (
|
||||
<Card
|
||||
title='Stok Persediaan'
|
||||
className={{
|
||||
@@ -2318,12 +2590,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedStocks(
|
||||
formik.values.stocks?.map((_, idx) => idx) ?? []
|
||||
formik.values.stocks?.map((_, idx) => idx) ??
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
setSelectedStocks([]);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2373,6 +2647,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2391,7 +2666,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
) || null
|
||||
}
|
||||
onChange={(selectedOption) => {
|
||||
const option = selectedOption as OptionType | null;
|
||||
const option =
|
||||
selectedOption as OptionType | null;
|
||||
formik.setFieldValue(
|
||||
`stocks.${idx}.product_warehouse_id`,
|
||||
option?.value || 0
|
||||
@@ -2425,7 +2701,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
isSearchable
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
!formik.values.project_flock_kandang_id
|
||||
!formik.values.project_flock_kandang_id ||
|
||||
!recordingRestriction.canEditStock
|
||||
}
|
||||
isClearable={type !== 'detail'}
|
||||
inputPrefix={
|
||||
@@ -2472,7 +2749,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
disabled={
|
||||
type === 'detail' ||
|
||||
!recordingRestriction.canEditStock
|
||||
}
|
||||
/>
|
||||
{getStockUsageAdornment(idx)}
|
||||
</div>
|
||||
@@ -2484,6 +2764,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeStock(idx)}
|
||||
disabled={!recordingRestriction.canEditStock}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
@@ -2501,7 +2782,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedStocks.length > 0 && (
|
||||
{selectedStocks.length > 0 &&
|
||||
recordingRestriction.canEditStock && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
@@ -2513,18 +2795,60 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
Hapus Terpilih ({selectedStocks.length})
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
!recordingRestriction.canEditStock
|
||||
? 'Stock tidak dapat ditambahkan pada masa transisi Laying'
|
||||
: ''
|
||||
}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addStock}
|
||||
className='w-fit'
|
||||
disabled={
|
||||
!formik.values.project_flock_kandang_id ||
|
||||
!recordingRestriction.canEditStock
|
||||
}
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Stok
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Transition Warning Banner -- MOVED UP -- */}
|
||||
{isTransitionPeriod && (
|
||||
<div className='alert alert-warning mb-4'>
|
||||
<Icon
|
||||
icon='material-symbols:warning-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>
|
||||
{isLayingCategory
|
||||
? 'Masa Transisi Laying: Hanya Deplesi yang dapat diisi. Stock (Pakan/OVK) tidak dapat diinput.'
|
||||
: 'Masa Transisi Growing: Hanya Stock (Pakan/OVK) yang dapat diisi. Deplesi tidak dapat diinput.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Locked Recording Warning */}
|
||||
{recordingRestriction.isLocked && (
|
||||
<div className='alert alert-error mb-4'>
|
||||
<Icon
|
||||
icon='material-symbols:lock-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{recordingRestriction.lockReason}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Depletions Table */}
|
||||
{((type as 'add' | 'edit' | 'detail') !== 'detail' ||
|
||||
@@ -2532,7 +2856,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
<Card
|
||||
title='Deplesi'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
wrapper: cn('w-full mb-4 shadow', {
|
||||
'opacity-60':
|
||||
!recordingRestriction.canEditDepletion &&
|
||||
(type as 'add' | 'edit' | 'detail') !== 'detail',
|
||||
}),
|
||||
title: 'mb-4',
|
||||
}}
|
||||
>
|
||||
@@ -2562,6 +2890,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
setSelectedDepletions([]);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditDepletion}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2598,6 +2927,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={!recordingRestriction.canEditDepletion}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -2623,7 +2953,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}}
|
||||
options={getAvailableDepletionProductOptions(idx)}
|
||||
placeholder='Pilih Kondisi'
|
||||
placeholder={
|
||||
!formik.values.project_flock_kandang_id
|
||||
? 'Pilih kandang terlebih dahulu'
|
||||
: 'Pilih Kondisi'
|
||||
}
|
||||
isLoading={isLoadingDepletionProducts}
|
||||
onMenuScrollToBottom={loadMoreDepletionProducts}
|
||||
isError={
|
||||
@@ -2640,7 +2974,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
idx
|
||||
).errorMessage
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
!formik.values.project_flock_kandang_id ||
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-48',
|
||||
}}
|
||||
@@ -2679,7 +3017,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
)
|
||||
: null
|
||||
}
|
||||
disabled={type === 'detail'}
|
||||
disabled={
|
||||
type === 'detail' ||
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
@@ -2689,6 +3030,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removeDepletion(idx)}
|
||||
disabled={
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
@@ -2706,7 +3050,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
</div>
|
||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedDepletions.length > 0 && (
|
||||
{selectedDepletions.length > 0 &&
|
||||
recordingRestriction.canEditDepletion && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
@@ -2718,15 +3063,28 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
Hapus Terpilih ({selectedDepletions.length})
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
!recordingRestriction.canEditDepletion
|
||||
? 'Deplesi tidak dapat ditambahkan pada masa transisi Growing'
|
||||
: ''
|
||||
}
|
||||
position='top'
|
||||
>
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addDepletion}
|
||||
className='w-fit'
|
||||
disabled={
|
||||
!formik.values.project_flock_kandang_id ||
|
||||
!recordingRestriction.canEditDepletion
|
||||
}
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Depletion
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -2847,7 +3205,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
);
|
||||
}}
|
||||
options={getAvailableEggProductOptions(idx)}
|
||||
placeholder='Pilih Kondisi Telur'
|
||||
placeholder={
|
||||
!formik.values.project_flock_kandang_id
|
||||
? 'Pilih kandang terlebih dahulu'
|
||||
: 'Pilih Kondisi Telur'
|
||||
}
|
||||
isLoading={isLoadingEggProducts}
|
||||
onMenuScrollToBottom={loadMoreEggProducts}
|
||||
isError={
|
||||
@@ -2864,7 +3226,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
idx
|
||||
).errorMessage
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
!formik.values.project_flock_kandang_id
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-48',
|
||||
}}
|
||||
@@ -2969,6 +3334,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
||||
color='success'
|
||||
onClick={addEgg}
|
||||
className='w-fit'
|
||||
disabled={!formik.values.project_flock_kandang_id}
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Telur
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
export type RecordingRestriction = {
|
||||
canEditStock: boolean;
|
||||
canEditDepletion: boolean;
|
||||
canEditEgg: boolean;
|
||||
isLocked: boolean;
|
||||
lockReason?: string;
|
||||
};
|
||||
|
||||
export const getRecordingRestriction = (
|
||||
isLaying: boolean,
|
||||
isTransition: boolean,
|
||||
currentIsLaying?: boolean
|
||||
): RecordingRestriction => {
|
||||
if (isTransition && !isLaying) {
|
||||
const isLayingKandangInTransition = currentIsLaying === true;
|
||||
|
||||
if (isLayingKandangInTransition) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: true,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLaying && !isTransition && currentIsLaying) {
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason:
|
||||
'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying',
|
||||
};
|
||||
}
|
||||
|
||||
if (!isLaying && !isTransition) {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: false,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
if (isLaying && !isTransition) {
|
||||
return {
|
||||
canEditStock: true,
|
||||
canEditDepletion: true,
|
||||
canEditEgg: true,
|
||||
isLocked: false,
|
||||
lockReason: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
canEditStock: false,
|
||||
canEditDepletion: false,
|
||||
canEditEgg: false,
|
||||
isLocked: true,
|
||||
lockReason: 'Kondisi transisi tidak valid',
|
||||
};
|
||||
};
|
||||
@@ -555,6 +555,12 @@ export const APPROVAL_WORKFLOWS = {
|
||||
],
|
||||
};
|
||||
|
||||
export const PROJECT_FLOCK_STATUS = {
|
||||
PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name,
|
||||
AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name,
|
||||
SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name,
|
||||
} as const;
|
||||
|
||||
export const ACCEPTED_FILE_TYPE = {
|
||||
PDF: {
|
||||
'application/pdf': ['.pdf'],
|
||||
|
||||
+11
@@ -9,8 +9,19 @@ export type BaseProductWarehouse = {
|
||||
warehouse_id: number;
|
||||
uom: Uom;
|
||||
quantity: number;
|
||||
transfer_available_qty?: number;
|
||||
product: Product;
|
||||
warehouse: Warehouse;
|
||||
project_flock_kandang?: {
|
||||
id: number;
|
||||
project_flock_id: number;
|
||||
kandang_id: number;
|
||||
period: number;
|
||||
project_flock?: {
|
||||
id: number;
|
||||
flock_name: string;
|
||||
};
|
||||
};
|
||||
week?: number | null;
|
||||
};
|
||||
|
||||
|
||||
+2
@@ -74,6 +74,8 @@ export type ProjectFlockKandangLookup = {
|
||||
available_quantity?: number;
|
||||
population: number;
|
||||
chick_in_date: string;
|
||||
is_transition: boolean;
|
||||
is_laying: boolean;
|
||||
};
|
||||
|
||||
export type ProjectFlockAvailableQuantity = {
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@ export type BaseRecording = {
|
||||
project_flock: ProjectFlock;
|
||||
record_datetime: string;
|
||||
day: number;
|
||||
executed_at: string;
|
||||
is_transition: boolean;
|
||||
is_laying: boolean;
|
||||
} & ProductionMetrics;
|
||||
|
||||
export type RecordingDepletion = {
|
||||
|
||||
Reference in New Issue
Block a user