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