diff --git a/src/app/production/transfer-to-laying/add/page.tsx b/src/app/production/transfer-to-laying/add/page.tsx
new file mode 100644
index 00000000..6f29085f
--- /dev/null
+++ b/src/app/production/transfer-to-laying/add/page.tsx
@@ -0,0 +1,11 @@
+import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
+
+const AddTransferToLaying = () => {
+ return (
+
+
+
+ );
+};
+
+export default AddTransferToLaying;
diff --git a/src/app/production/transfer-to-laying/detail/edit/page.tsx b/src/app/production/transfer-to-laying/detail/edit/page.tsx
new file mode 100644
index 00000000..9003dbba
--- /dev/null
+++ b/src/app/production/transfer-to-laying/detail/edit/page.tsx
@@ -0,0 +1,148 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
+
+import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+
+import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
+
+// TODO: delete dummy data
+const DUMMY_TRANSFER_TO_LAYING_EDIT: TransferToLaying = {
+ id: 1,
+ transfer_date: '2025-10-14',
+ flock_source: {
+ id: 1,
+ name: 'Flock asal test',
+ },
+ flock_destination: {
+ id: 2,
+ name: 'Flock tujuan destination',
+ },
+ quantity: 10,
+ kandangs: [
+ {
+ kandang: {
+ id: 1,
+ name: 'Kandang test',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'test location',
+ address: 'test address 1',
+ area: { id: 1, name: 'test area 1' },
+ },
+ pic: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+ },
+ quantity: 8,
+ },
+ {
+ kandang: {
+ id: 1,
+ name: 'Kandang test 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'test location',
+ address: 'test address 1',
+ area: { id: 1, name: 'test area 1' },
+ },
+ pic: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+ },
+ quantity: 2,
+ },
+ ],
+ reason: 'Test alasan',
+
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+};
+
+const TransferToLayingEdit = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const transferToLayingId = searchParams.get('transferToLayingId');
+
+ const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
+ useSWR(transferToLayingId, (id: number) =>
+ TransferToLayingApi.getSingle(id)
+ );
+
+ if (!transferToLayingId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ // TODO: remove dummy data and integrate with real API
+ if (
+ !isLoadingTransferToLaying &&
+ (!transferToLaying ||
+ (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_EDIT))
+ ) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingTransferToLaying && (
+
+ )}
+ {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
+
+ )} */}
+
+ {/* TODO: remove this dummy data and integrate to real API */}
+
+
+ );
+};
+
+export default TransferToLayingEdit;
diff --git a/src/app/production/transfer-to-laying/detail/layout.tsx b/src/app/production/transfer-to-laying/detail/layout.tsx
new file mode 100644
index 00000000..7220dfa1
--- /dev/null
+++ b/src/app/production/transfer-to-laying/detail/layout.tsx
@@ -0,0 +1,11 @@
+import SuspenseHelper from '@/components/helper/SuspenseHelper';
+
+const Layout = ({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) => {
+ return {children} ;
+};
+
+export default Layout;
diff --git a/src/app/production/transfer-to-laying/detail/page.tsx b/src/app/production/transfer-to-laying/detail/page.tsx
new file mode 100644
index 00000000..de5426c8
--- /dev/null
+++ b/src/app/production/transfer-to-laying/detail/page.tsx
@@ -0,0 +1,148 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import useSWR from 'swr';
+
+import TransferToLayingForm from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm';
+
+import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+
+import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
+
+// TODO: delete dummy data
+const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
+ id: 1,
+ transfer_date: '2025-10-14',
+ flock_source: {
+ id: 1,
+ name: 'Flock asal test',
+ },
+ flock_destination: {
+ id: 2,
+ name: 'Flock tujuan destination',
+ },
+ quantity: 10,
+ kandangs: [
+ {
+ kandang: {
+ id: 1,
+ name: 'Kandang test',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'test location',
+ address: 'test address 1',
+ area: { id: 1, name: 'test area 1' },
+ },
+ pic: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+ },
+ quantity: 8,
+ },
+ {
+ kandang: {
+ id: 1,
+ name: 'Kandang test 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'test location',
+ address: 'test address 1',
+ area: { id: 1, name: 'test area 1' },
+ },
+ pic: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+ },
+ quantity: 2,
+ },
+ ],
+ reason: 'Test alasan',
+
+ created_user: {
+ id: 1,
+ id_user: 2,
+ email: 'test@gmail.com',
+ name: 'test',
+ },
+ created_at: '14-10-2025',
+ updated_at: '14-10-2025',
+};
+
+const TransferToLayingDetail = () => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const transferToLayingId = searchParams.get('transferToLayingId');
+
+ const { data: transferToLaying, isLoading: isLoadingTransferToLaying } =
+ useSWR(transferToLayingId, (id: number) =>
+ TransferToLayingApi.getSingle(id)
+ );
+
+ if (!transferToLayingId) {
+ router.back();
+
+ return (
+
+
+
+ );
+ }
+
+ // TODO: remove dummy data and integrate with real API
+ if (
+ !isLoadingTransferToLaying &&
+ (!transferToLaying ||
+ (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
+ ) {
+ router.replace('/404');
+ return;
+ }
+
+ return (
+
+ {isLoadingTransferToLaying && (
+
+ )}
+ {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
+
+ )} */}
+
+ {/* TODO: remove this dummy data and integrate to real API */}
+
+
+ );
+};
+
+export default TransferToLayingDetail;
diff --git a/src/app/production/transfer-to-laying/page.tsx b/src/app/production/transfer-to-laying/page.tsx
new file mode 100644
index 00000000..84513542
--- /dev/null
+++ b/src/app/production/transfer-to-laying/page.tsx
@@ -0,0 +1,11 @@
+import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
+
+const TransferToLaying = () => {
+ return (
+
+ );
+};
+
+export default TransferToLaying;
diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx
index be87f069..4a3b44b0 100644
--- a/src/components/MainDrawer.tsx
+++ b/src/components/MainDrawer.tsx
@@ -10,6 +10,7 @@ import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Navbar from '@/components/Navbar';
import Collapse from '@/components/Collapse';
+import Button from '@/components/Button';
import { useUiStore } from '@/stores/ui/ui.store';
import { MAIN_DRAWER_LINKS } from '@/config/constant';
@@ -155,9 +156,15 @@ const MainDrawerMenu = () => {
};
const MainDrawerContent = () => {
+ const { setMainDrawerOpen } = useUiStore();
+
+ const closeMainDrawerHandler = () => {
+ setMainDrawerOpen(false);
+ };
+
return (
-
+
diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx
index 1d9d86b4..dbd4b6bc 100644
--- a/src/components/helper/RequireAuth.tsx
+++ b/src/components/helper/RequireAuth.tsx
@@ -158,7 +158,7 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
const { data: userResponse, isLoading: isLoadingUserResponse } =
useSWRImmutable
(
- '/auth/get-me',
+ '/auth/sso/userinfo',
httpClientFetcher,
{
shouldRetryOnError: false,
@@ -194,4 +194,4 @@ const RequireAuth = ({ children }: RequireAuthProps) => {
return <>{children}>;
};
-export default RequireAuth;
\ No newline at end of file
+export default RequireAuth;
diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx
index 9b57cabc..6a8d0ac8 100644
--- a/src/components/input/SelectInput.tsx
+++ b/src/components/input/SelectInput.tsx
@@ -1,6 +1,8 @@
'use client';
import { ComponentType, ReactNode, useEffect, useMemo, useState } from 'react';
+import useSWR from 'swr';
+
import Select, {
OptionProps,
GroupBase,
@@ -11,7 +13,10 @@ import Select, {
import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce';
-import { cn } from '@/lib/helper';
+import { cn, getByPath } from '@/lib/helper';
+import { httpClientFetcher } from '@/services/http/client';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { BaseApiResponse } from '@/types/api/api-general';
export interface OptionType {
value: string | number;
@@ -182,7 +187,7 @@ const SelectInput = (props: SelectInputProps) => {
'text-error!': isError,
}),
menu: () =>
- cn('border border-gray-200 rounded-lg bg-white shadow-lg!'),
+ cn('border border-gray-200 rounded! bg-base-100 shadow-lg!'),
menuList: () => cn('p-2! max-h-60 overflow-auto'),
option: ({ isFocused, isSelected }) =>
cn('mt-1 px-3 py-2 rounded cursor-pointer!', {
@@ -222,4 +227,45 @@ const SelectInput = (props: SelectInputProps) => {
);
};
+const useSelect = (
+ basePath: string,
+ valueKey: keyof T,
+ labelKey: keyof T,
+ searchKey: string = 'search',
+ params?: { [key: string]: string }
+) => {
+ const [inputValue, setInputValue] = useState('');
+
+ const optionsUrlParams = useMemo(() => {
+ return new URLSearchParams({
+ [searchKey]: inputValue ?? '',
+ ...params,
+ }).toString();
+ }, [inputValue, searchKey]);
+
+ const optionsUrl = `${basePath}?${optionsUrlParams}`;
+
+ const { data, isLoading } = useSWR(optionsUrl, async (url) => {
+ return await httpClientFetcher>(url);
+ });
+
+ const options = isResponseSuccess(data)
+ ? data.data.map((item) => {
+ return {
+ value: getByPath(item, valueKey as string),
+ label: getByPath(item, labelKey as string),
+ };
+ })
+ : [];
+
+ return {
+ inputValue,
+ setInputValue,
+ options,
+ isLoadingOptions: isLoading,
+ rawData: data,
+ };
+};
+
+export { useSelect };
export default SelectInput;
diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx
new file mode 100644
index 00000000..52d21016
--- /dev/null
+++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx
@@ -0,0 +1,633 @@
+'use client';
+
+import { ChangeEventHandler, useState } from 'react';
+import useSWR from 'swr';
+import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
+import toast from 'react-hot-toast';
+
+import { Icon } from '@iconify/react';
+import Table from '@/components/Table';
+import DebouncedTextInput from '@/components/input/DebouncedTextInput';
+import Button from '@/components/Button';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+import SelectInput, {
+ OptionType,
+ useSelect,
+} from '@/components/input/SelectInput';
+import RowDropdownOptions from '@/components/table/RowDropdownOptions';
+import RowCollapseOptions from '@/components/table/RowCollapseOptions';
+import TextInput from '@/components/input/TextInput';
+import CheckboxInput from '@/components/input/CheckboxInput';
+
+import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
+import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
+import { cn, formatDate } from '@/lib/helper';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import { ROWS_OPTIONS } from '@/config/constant';
+import { Flock } from '@/types/api/master-data/flock';
+import { FlockApi } from '@/services/api/master-data';
+
+const RowOptionsMenu = ({
+ type = 'dropdown',
+ props,
+ approveClickHandler,
+ rejectClickHandler,
+ deleteClickHandler,
+}: {
+ type: 'dropdown' | 'collapse';
+ props: CellContext;
+ approveClickHandler: () => void;
+ rejectClickHandler: () => void;
+ deleteClickHandler: () => void;
+}) => {
+ return (
+
+
+
+ Detail
+
+
+
+
+ Edit
+
+
+
+
+ Approve
+
+
+
+
+ Reject
+
+
+
+
+ Delete
+
+
+ );
+};
+
+const TransferToLayingsTable = () => {
+ const {
+ state: tableFilterState,
+ updateFilter,
+ setPage,
+ setPageSize,
+ toQueryString: getTableFilterQueryString,
+ } = useTableFilter({
+ initial: {
+ search: '',
+ transferDate: '',
+ flockSource: '',
+ flockDestination: '',
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ transferDate: 'transfer_date',
+ flockSource: 'flock_source',
+ flockDestination: 'flock_destination',
+ },
+ });
+
+ const {
+ data: transferToLayings,
+ isLoading,
+ mutate: refreshTransferToLayings,
+ } = useSWR(
+ `${TransferToLayingApi.basePath}${getTableFilterQueryString()}`,
+ TransferToLayingApi.getAllFetcher
+ );
+
+ // Modal hooks
+ const deleteModal = useModal();
+ const approveModal = useModal();
+ const rejectModal = useModal();
+
+ // Flocks data
+ const {
+ setInputValue: setFlockSourceInputValue,
+ options: flockSourceOptions,
+ isLoadingOptions: isLoadingFlockSourceOptions,
+ } = useSelect(FlockApi.basePath, 'id', 'name');
+
+ const {
+ setInputValue: setFlockDestinationInputValue,
+ options: flockDestinationOptions,
+ isLoadingOptions: isLoadingFlockDestinationOptions,
+ } = useSelect(FlockApi.basePath, 'id', 'name');
+
+ // Flocks value
+ const [selectedFlockSource, setSelectedFlockSource] =
+ useState(null);
+ const [selectedFlockDestination, setSelectedFlockDestination] =
+ useState(null);
+
+ const [selectedTransferToLaying, setSelectedTransferToLaying] = useState<
+ TransferToLaying | undefined
+ >(undefined);
+
+ // Modal loading state
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
+ const [isRejectLoading, setIsRejectLoading] = useState(false);
+
+ const [sorting, setSorting] = useState([]);
+ const [rowSelection, setRowSelection] = useState>({});
+ const selectedRowIds = Object.keys(rowSelection).map((item) =>
+ parseInt(item)
+ );
+
+ const transferToLayingsColumns: ColumnDef[] = [
+ {
+ id: 'select',
+ header: ({ table }) => (
+
+
+
+ ),
+ cell: ({ row }) => (
+
+
+
+ ),
+ },
+ {
+ header: '#',
+ cell: (props) =>
+ tableFilterState.pageSize * (tableFilterState.page - 1) +
+ props.row.index +
+ 1,
+ },
+ {
+ accessorKey: 'transfer_date',
+ header: 'Tanggal Transfer',
+ cell: (props) => formatDate(props.getValue() as string, 'DD MMM YYYY'),
+ },
+ {
+ accessorKey: 'flock_source',
+ header: 'Flock Asal',
+ cell: (props) => props.row.original.flock_source.name,
+ },
+ {
+ accessorKey: 'flock_destination',
+ header: 'Flock Tujuan',
+ cell: (props) => props.row.original.flock_destination.name,
+ },
+ {
+ accessorKey: 'quantity',
+ header: 'Kuantitas',
+ },
+ {
+ accessorKey: 'reason',
+ header: 'Alasan Transfer',
+ },
+ {
+ header: 'Aksi',
+ cell: (props) => {
+ const currentPageSize = props.table.getPaginationRowModel().rows.length;
+ const currentPageRows = props.table.getPaginationRowModel().flatRows;
+ const currentRowRelativeIndex =
+ currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
+
+ const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
+
+ const approveClickHandler = () => {
+ setSelectedTransferToLaying(props.row.original);
+
+ // Set row selection
+ setRowSelection({
+ [String(props.row.original.id)]: true,
+ });
+
+ approveModal.openModal();
+ };
+
+ const rejectClickHandler = () => {
+ setSelectedTransferToLaying(props.row.original);
+
+ // Set row selection
+ setRowSelection({
+ [String(props.row.original.id)]: true,
+ });
+
+ rejectModal.openModal();
+ };
+
+ const deleteClickHandler = () => {
+ setSelectedTransferToLaying(props.row.original);
+ deleteModal.openModal();
+ };
+
+ return (
+ <>
+ {currentPageSize > 2 && (
+
+
+
+ )}
+
+ {currentPageSize <= 2 && (
+
+
+
+ )}
+ >
+ );
+ },
+ },
+ ];
+
+ const bulkApproveClickHandler = () => {
+ approveModal.openModal();
+ };
+
+ const bulkRejectClickHandler = () => {
+ rejectModal.openModal();
+ };
+
+ // Modal confirm click handler
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ await TransferToLayingApi.delete(selectedTransferToLaying?.id as number);
+ refreshTransferToLayings();
+
+ deleteModal.closeModal();
+ toast.success('Berhasil menghapus data transfer ke laying!');
+ setIsDeleteLoading(false);
+ };
+
+ const confirmationModalApproveClickHandler = async () => {
+ setIsApproveLoading(true);
+
+ const bulkApproveResponse = await TransferToLayingApi.bulkApprove(
+ selectedRowIds
+ );
+
+ if (isResponseSuccess(bulkApproveResponse)) {
+ refreshTransferToLayings();
+ approveModal.closeModal();
+
+ // TODO: remove console.log
+ console.log('Approved data:', selectedRowIds);
+
+ toast.success(
+ `Berhasil approve ${selectedRowIds.length} data transfer ke laying!`
+ );
+
+ setRowSelection({});
+ } else {
+ approveModal.closeModal();
+
+ toast.error(
+ `Gagal approve ${selectedRowIds.length} data transfer ke laying!`
+ );
+ }
+
+ setIsApproveLoading(false);
+ };
+
+ const confirmationModalRejectClickHandler = async () => {
+ setIsRejectLoading(true);
+
+ const bulkRejectResponse = await TransferToLayingApi.bulkReject(
+ selectedRowIds
+ );
+
+ if (isResponseSuccess(bulkRejectResponse)) {
+ refreshTransferToLayings();
+ rejectModal.closeModal();
+
+ // TODO: remove console.log
+ console.log('Rejected data:', selectedRowIds);
+
+ toast.success(
+ `Berhasil reject ${selectedRowIds.length} data transfer ke laying!`
+ );
+ setRowSelection({});
+ } else {
+ rejectModal.closeModal();
+
+ toast.error(
+ `Gagal reject ${selectedRowIds.length} data transfer ke laying!`
+ );
+ }
+
+ setIsRejectLoading(false);
+ };
+
+ const searchChangeHandler: ChangeEventHandler = (e) => {
+ updateFilter('search', e.target.value);
+ };
+
+ const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+
+ setPageSize(newVal.value as number);
+ };
+
+ const transferDateChangeHandler: ChangeEventHandler = (
+ e
+ ) => {
+ updateFilter('transferDate', e.target.value);
+ };
+
+ const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
+ setSelectedFlockSource(val as OptionType);
+ updateFilter(
+ 'flockSource',
+ val ? ((val as OptionType).value as string) : ''
+ );
+ };
+
+ const flockDestinationChangeHandler = (
+ val: OptionType | OptionType[] | null
+ ) => {
+ setSelectedFlockDestination(val as OptionType);
+ updateFilter(
+ 'flockDestination',
+ val ? ((val as OptionType).value as string) : ''
+ );
+ };
+
+ // track sorting
+ // useEffect(() => {
+ // const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
+
+ // if (!isNameSorted) {
+ // updateFilter('nameSort', '');
+ // } else {
+ // updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
+ // }
+ // }, [sorting, updateFilter]);
+
+ return (
+ <>
+
+
+
+
+
+
+ Tambah Transfer ke Laying
+
+
+ {selectedRowIds.length > 0 && (
+ <>
+
+
+ Approve
+
+
+
+
+ Reject
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ data={
+ isResponseSuccess(transferToLayings) ? transferToLayings?.data : []
+ }
+ columns={transferToLayingsColumns}
+ pageSize={tableFilterState.pageSize}
+ page={
+ isResponseSuccess(transferToLayings)
+ ? transferToLayings?.meta?.page
+ : 0
+ }
+ totalItems={
+ isResponseSuccess(transferToLayings)
+ ? transferToLayings?.meta?.total_results
+ : 0
+ }
+ onPageChange={setPage}
+ isLoading={isLoading}
+ sorting={sorting}
+ setSorting={setSorting}
+ rowSelection={rowSelection}
+ setRowSelection={setRowSelection}
+ className={{
+ containerClassName: cn({
+ 'mb-20':
+ isResponseSuccess(transferToLayings) &&
+ transferToLayings?.data?.length === 0,
+ }),
+ tableWrapperClassName: 'overflow-x-auto min-h-full!',
+ tableClassName: 'font-inter w-full table-auto min-h-full!',
+ headerRowClassName: 'border-b border-b-gray-200',
+ headerColumnClassName:
+ 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
+ bodyRowClassName: 'border-b border-b-gray-200',
+ bodyColumnClassName:
+ 'px-6 py-3 last:flex last:flex-row last:justify-end',
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TransferToLayingsTable;
diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts
new file mode 100644
index 00000000..e0273a9a
--- /dev/null
+++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema.ts
@@ -0,0 +1,83 @@
+import * as Yup from 'yup';
+
+type TransferToLayingFormSchemaType = {
+ transfer_date?: string;
+ flockSource?: {
+ value: number;
+ label: string;
+ };
+ flockDestination?: {
+ value: number;
+ label: string;
+ };
+
+ totalQuantity?: number;
+ maxTotalQuantity?: number; // original cap (hidden), helper
+
+ kandangs: {
+ kandang: {
+ value: number;
+ label: string;
+ };
+ quantity: number | string; // editable
+ maxQuantity?: number; // original cap (hidden), helper
+ }[];
+ reason?: string;
+};
+
+export const TransferToLayingFormSchema: Yup.ObjectSchema =
+ Yup.object({
+ transfer_date: Yup.string().required('Tanggal transfer wajib diisi!'),
+
+ flockSource: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Flock asal wajib diisi!'),
+
+ flockDestination: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Flock tujuan wajib diisi!'),
+
+ totalQuantity: Yup.number()
+ .min(1, 'Jumlah transfer minimal 1')
+ .max(
+ Yup.ref('maxTotalQuantity'),
+ ({ max }) => `Kuantitas maksimal ${max}!`
+ )
+ .required('Jumlah transfer wajib diisi!'),
+
+ maxTotalQuantity: Yup.number()
+ .min(1, 'Jumlah transfer minimal 1')
+ .required('Jumlah transfer wajib diisi!'),
+
+ kandangs: Yup.array()
+ .of(
+ Yup.object({
+ kandang: Yup.object({
+ value: Yup.number().min(1).required(),
+ label: Yup.string().required(),
+ }).required('Kandang wajib diisi!'),
+
+ quantity: Yup.number()
+ .min(0, 'Kuantitas minimal 0!')
+ .max(
+ Yup.ref('maxQuantity'),
+ ({ max }) => `Kuantitas maksimal ${max}!`
+ )
+ .required('Kuantitas wajib diisi!'),
+
+ maxQuantity: Yup.number().min(1).required(), // internal helper field
+ })
+ )
+ .min(1, 'Minimal 1 kandang terisi!')
+ .required('Kandang wajib diisi!'),
+
+ reason: Yup.string().required('Alasan transfer wajib diisi!'),
+ });
+
+export const UpdateTransferToLayingFormSchema = TransferToLayingFormSchema;
+
+export type TransferToLayingFormValues = Yup.InferType<
+ typeof TransferToLayingFormSchema
+>;
diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
new file mode 100644
index 00000000..def2cde8
--- /dev/null
+++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
@@ -0,0 +1,688 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { useFormik } from 'formik';
+import { toast } from 'react-hot-toast';
+import useSWR from 'swr';
+
+import { Icon } from '@iconify/react';
+import Button from '@/components/Button';
+import TextInput from '@/components/input/TextInput';
+import SelectInput, {
+ OptionType,
+ // useSelect,
+} from '@/components/input/SelectInput';
+import TextArea from '@/components/input/TextArea';
+import { useModal } from '@/components/Modal';
+import ConfirmationModal from '@/components/modal/ConfirmationModal';
+
+import {
+ TransferToLayingFormSchema,
+ TransferToLayingFormValues,
+ UpdateTransferToLayingFormSchema,
+} from '@/components/pages/production/transfer-to-laying/form/TransferToLayingForm.schema';
+import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
+import {
+ TransferToLaying,
+ CreateTransferToLayingPayload,
+ UpdateTransferToLayingPayload,
+} from '@/types/api/production/transfer-to-laying';
+import { cn } from '@/lib/helper';
+
+import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
+
+interface TransferToLayingFormProps {
+ type?: 'add' | 'edit' | 'detail';
+ initialValues?: TransferToLaying;
+}
+
+const TransferToLayingForm = ({
+ type = 'add',
+ initialValues,
+}: TransferToLayingFormProps) => {
+ const router = useRouter();
+
+ // Modal hooks
+ const deleteModal = useModal();
+ const approveModal = useModal();
+ const rejectModal = useModal();
+
+ const [formErrorMessage, setFormErrorMessage] = useState('');
+
+ // Modal loading state
+ const [isDeleteLoading, setIsDeleteLoading] = useState(false);
+ const [isApproveLoading, setIsApproveLoading] = useState(false);
+ const [isRejectLoading, setIsRejectLoading] = useState(false);
+
+ const createTransferToLayingHandler = useCallback(
+ async (payload: CreateTransferToLayingPayload) => {
+ console.log('Create transfer to laying:', { payload });
+
+ toast.success('Berhasil menambahkan data transfer ke laying!');
+ },
+ [router]
+ );
+
+ const updateTransferToLayingHandler = useCallback(
+ async (
+ transferToLayingId: number,
+ payload: UpdateTransferToLayingPayload
+ ) => {
+ console.log(
+ `Update transfer to laying with ID of ${transferToLayingId}:`,
+ { payload }
+ );
+
+ toast.success('Berhasil mengubah data transfer ke laying!');
+ },
+ [router]
+ );
+
+ const formikInitialValues = useMemo(() => {
+ return {
+ transfer_date: initialValues?.transfer_date ?? '',
+ flockSource: initialValues?.flock_source
+ ? {
+ value: initialValues?.flock_source.id,
+ label: initialValues?.flock_source.name,
+ }
+ : undefined,
+ flockDestination: initialValues?.flock_destination
+ ? {
+ value: initialValues?.flock_destination.id,
+ label: initialValues?.flock_destination.name,
+ }
+ : undefined,
+ totalQuantity: initialValues?.quantity ?? undefined,
+
+ kandangs: initialValues?.kandangs
+ ? initialValues.kandangs.map((kandang) => ({
+ kandang: {
+ value: kandang.kandang.id,
+ label: kandang.kandang.name,
+ },
+ quantity: kandang.quantity,
+ }))
+ : [],
+
+ reason: initialValues?.reason ?? undefined,
+ };
+ }, [initialValues]);
+
+ const formik = useFormik({
+ initialValues: formikInitialValues,
+ validationSchema:
+ type === 'edit'
+ ? UpdateTransferToLayingFormSchema
+ : TransferToLayingFormSchema,
+ onSubmit: async (values) => {
+ console.log({ values });
+
+ setFormErrorMessage('');
+
+ const transferToLayingPayload: CreateTransferToLayingPayload = {
+ transfer_date: values.transfer_date as string,
+ flock_source_id: values.flockSource?.value as number,
+ flock_destination_id: values.flockDestination?.value as number,
+ totalQuantity: values.totalQuantity as number,
+
+ kandangs: values.kandangs?.map((kandang) => ({
+ kandang_id: kandang.kandang.value,
+ quantity: kandang.quantity,
+ })) as {
+ kandang_id: number;
+ quantity: number;
+ }[],
+
+ reason: values.reason as string,
+ };
+
+ switch (type) {
+ case 'add':
+ await createTransferToLayingHandler(transferToLayingPayload);
+ break;
+
+ case 'edit':
+ await updateTransferToLayingHandler(
+ initialValues?.id as number,
+ transferToLayingPayload
+ );
+ break;
+ }
+ },
+ });
+
+ const { setValues: formikSetValues, values: formikValues } = formik;
+ const { kandangs: kandangsValue } = formikValues;
+
+ const deleteTransferToLayingClickHandler = () => {
+ deleteModal.openModal();
+ };
+
+ const approveClickHandler = () => {
+ approveModal.openModal();
+ };
+
+ const rejectClickHandler = () => {
+ rejectModal.openModal();
+ };
+
+ // Modal confirm click handler
+ const confirmationModalDeleteClickHandler = async () => {
+ setIsDeleteLoading(true);
+
+ // TODO: delete data and integrate to real API
+ deleteModal.closeModal();
+ toast.success('Berhasil menghapus data transfer ke laying!');
+
+ setIsDeleteLoading(false);
+ };
+
+ const confirmationModalApproveClickHandler = async () => {
+ setIsApproveLoading(true);
+
+ const approveResponse = await TransferToLayingApi.approve(
+ initialValues?.id as number
+ );
+
+ if (isResponseSuccess(approveResponse)) {
+ approveModal.closeModal();
+
+ toast.success('Berhasil approve data transfer ke laying!');
+ } else {
+ approveModal.closeModal();
+
+ toast.error('Gagal approve data transfer ke laying!');
+ }
+
+ setIsApproveLoading(false);
+ };
+
+ const confirmationModalRejectClickHandler = async () => {
+ setIsRejectLoading(true);
+
+ const rejectResponse = await TransferToLayingApi.reject(
+ initialValues?.id as number
+ );
+
+ if (isResponseSuccess(rejectResponse)) {
+ rejectModal.closeModal();
+
+ toast.success('Berhasil reject data transfer ke laying!');
+ } else {
+ rejectModal.closeModal();
+
+ toast.error('Gagal reject data transfer ke laying!');
+ }
+
+ setIsRejectLoading(false);
+ };
+
+ const isRepeaterInputError = (
+ column: keyof TransferToLayingFormValues['kandangs'][0],
+ idx: number
+ ) => {
+ return (
+ formik.touched.kandangs?.[idx]?.[column] &&
+ Boolean(
+ formik.errors.kandangs?.[idx] instanceof Object &&
+ formik.errors.kandangs?.[idx]?.[column]
+ )
+ );
+ };
+
+ const repeaterInputErrorMessage = (
+ column: keyof TransferToLayingFormValues['kandangs'][0],
+ idx: number
+ ) => {
+ return (formik.errors.kandangs?.[idx] as Record)?.[column];
+ };
+
+ // TODO: remove dummy data and use real data
+ // Flock Source
+ // const {
+ // inputValue: flockSourceInputValue,
+ // setInputValue: setFlockSourceInputValue,
+ // options: flockSourceOptions,
+ // isLoadingOptions: isLoadingFlockSourceOptions,
+ // } = useSelect('/transfer-to-laying/production/get-flock-source', 'id', 'name');
+
+ // TODO: remove this dummy data
+ const { data: flockSources, isLoading: isLoadingFlockSourceOptions } = useSWR(
+ 'test',
+ () => TransferToLayingApi.getFlockSource()
+ );
+
+ const flockSourceOptions = isResponseSuccess(flockSources)
+ ? flockSources?.data.map((flockSource) => ({
+ value: flockSource.id,
+ label: flockSource.name,
+ }))
+ : [];
+
+ const flockSourceChangeHandler = (val: OptionType | OptionType[] | null) => {
+ // Get flock source data for total quantity and kandang
+ const flockSource =
+ isResponseSuccess(flockSources) && val !== null
+ ? flockSources.data.find(
+ (item) => item.id === (val as OptionType).value
+ )
+ : undefined;
+
+ // Set total quantity and kandangs
+ if (flockSource) {
+ const formattedKandangs = flockSource.kandangs.map((item) => ({
+ kandang: {
+ value: item.kandang.id,
+ label: item.kandang.name,
+ },
+ quantity: '',
+ maxQuantity: item.quantity,
+ }));
+
+ formik.setFieldValue('totalQuantity', flockSource.totalQuantity);
+ formik.setFieldValue('maxTotalQuantity', flockSource.totalQuantity);
+ formik.setFieldValue('kandangs', formattedKandangs);
+ } else {
+ formik.setFieldValue('totalQuantity', undefined);
+ formik.setFieldValue('kandangs', undefined);
+ formik.setFieldValue('reason', '');
+ }
+
+ formik.setFieldTouched('flockSource', true);
+ formik.setFieldValue('flockSource', val);
+ };
+
+ // TODO: remove dummy data and use real data
+ // Flock Destination
+ // const {
+ // inputValue: flockDestinationInputValue,
+ // setInputValue: setFlockDestinationInputValue,
+ // options: flockDestinationOptions,
+ // isLoadingOptions: isLoadingFlockDestinationOptions,
+ // } = useSelect('/transfer-to-laying/production/get-flock-destination', 'id', 'name');
+
+ // TODO: remove this dummy data
+ const {
+ data: flockDestinations,
+ isLoading: isLoadingFlockDestinationOptions,
+ } = useSWR('test', () => TransferToLayingApi.getFlockSource());
+
+ const flockDestinationOptions = isResponseSuccess(flockDestinations)
+ ? flockDestinations?.data.map((flockDestination) => ({
+ value: flockDestination.id,
+ label: flockDestination.name,
+ }))
+ : [];
+
+ const flockDestinationChangeHandler = (
+ val: OptionType | OptionType[] | null
+ ) => {
+ formik.setFieldTouched('flockDestination', true);
+ formik.setFieldValue('flockDestination', val);
+ };
+
+ useEffect(() => {
+ formikSetValues(formikInitialValues);
+ }, [formikSetValues, formikInitialValues]);
+
+ useEffect(() => {
+ // calculate total quantity if kandangs quantity change
+ if (kandangsValue && kandangsValue.length > 0) {
+ let newTotalQuantity = 0;
+
+ kandangsValue.forEach((item) => {
+ newTotalQuantity += item.quantity as number;
+ });
+
+ formik.setFieldValue('totalQuantity', newTotalQuantity);
+ formik.validateField('totalQuantity');
+ }
+ }, [formikSetValues, kandangsValue]);
+
+ return (
+ <>
+
+
+
+
+ Kembali
+
+
+
+ {type === 'add' && 'Tambah Transfer ke Laying'}
+ {type === 'edit' && 'Edit Transfer ke Laying'}
+ {type === 'detail' && 'Detail Transfer ke Laying'}
+
+
+
+
+ {type === 'detail' && (
+ <>
+
+
+ Approve
+
+
+
+
+ Reject
+
+ >
+ )}
+
+
+
+
+
+ {type !== 'add' && (
+
+ )}
+
+ {type === 'detail' && (
+ <>
+
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default TransferToLayingForm;
diff --git a/src/config/constant.ts b/src/config/constant.ts
index 2b87c4d7..bf2bb0d9 100644
--- a/src/config/constant.ts
+++ b/src/config/constant.ts
@@ -13,7 +13,7 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
},
{
- title: 'Production',
+ title: 'Produksi',
link: '/production',
icon: 'material-symbols:conveyor-belt-outline-rounded',
submenu: [
@@ -32,6 +32,11 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/production/recording',
icon: 'mdi:clipboard-text',
},
+ {
+ title: 'Transfer ke Laying',
+ link: '/production/transfer-to-laying',
+ icon: 'streamline:transfer-van',
+ },
],
},
@@ -40,11 +45,11 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
link: '/inventory',
icon: 'mdi:warehouse',
submenu: [
- {
- title: 'Product',
- link: '/inventory/product',
- icon: 'mdi:package-variant-closed',
- },
+ // {
+ // title: 'Product',
+ // link: '/inventory/product',
+ // icon: 'mdi:package-variant-closed',
+ // },
{
title: 'Penyesuaian Stok',
link: '/inventory/adjustment',
diff --git a/src/lib/helper.ts b/src/lib/helper.ts
index 2ec60598..e3bfda65 100644
--- a/src/lib/helper.ts
+++ b/src/lib/helper.ts
@@ -1,7 +1,11 @@
import moment from 'moment';
+import 'moment/locale/id';
import { twMerge } from 'tailwind-merge';
import clsx, { ClassValue } from 'clsx';
+// set locale globally
+moment.locale('id');
+
export const sleep = (ms: number = 1000) =>
new Promise((resolve) => setTimeout(resolve, ms));
@@ -40,3 +44,37 @@ export const formatCurrency = (
maximumFractionDigits,
}).format(value);
};
+
+/**
+ * Retrieves a nested value from an object using a dot-delimited key path.
+ * Supports array indexes (e.g., "users.0.name") and returns a default value
+ * if the path does not exist.
+ *
+ * @param obj - The source object to search.
+ * @param path - Dot-delimited key string (e.g., "user.address.city").
+ * @param defaultValue - Optional value to return if the key path is not found.
+ * @returns The value found at the specified path, or the default value.
+ */
+export function getByPath(
+ obj: T,
+ path: string,
+ defaultValue?: D
+): D {
+ if (obj == null) return defaultValue as D;
+ if (!path) return obj as D;
+
+ const segments = path.split('.').filter(Boolean);
+ let cur: { [key: string]: unknown } = obj;
+
+ for (const seg of segments) {
+ if (cur == null) return defaultValue as D;
+ const key: string | number =
+ Array.isArray(cur) && /^\d+$/.test(seg) ? Number(seg) : seg;
+ if (Object(cur) !== cur || !(key in cur)) {
+ return defaultValue as D;
+ }
+ cur = cur[key] as { [key: string]: unknown };
+ }
+
+ return cur as D;
+}
diff --git a/src/services/api/production/transfer-to-laying.ts b/src/services/api/production/transfer-to-laying.ts
new file mode 100644
index 00000000..6f808a8c
--- /dev/null
+++ b/src/services/api/production/transfer-to-laying.ts
@@ -0,0 +1,876 @@
+import { sleep } from '@/lib/helper';
+import { BaseApiService } from '@/services/api/base';
+import { BaseApiResponse } from '@/types/api/api-general';
+
+import {
+ CreateTransferToLayingPayload,
+ TransferToLaying,
+ UpdateTransferToLayingPayload,
+} from '@/types/api/production/transfer-to-laying';
+import { FlockWithKandangs } from '@/types/api/master-data/flock';
+
+// TODO: delete this dummy data
+const GET_ALL_TRANSFER_TO_LAYING_DUMMY_DATA: BaseApiResponse<
+ TransferToLaying[]
+> = {
+ code: 200,
+ status: 'success',
+ message: 'Successfully get all transfer to laying data!',
+ meta: {
+ page: 1,
+ limit: 10,
+ total_pages: 1,
+ total_results: 8,
+ },
+ data: [
+ {
+ id: 201,
+ transfer_date: '2025-10-10',
+ flock_source: { id: 1, name: 'Flock A' },
+ flock_destination: { id: 10, name: 'Laying House 1' },
+ quantity: 1200,
+ kandangs: [
+ {
+ kandang: {
+ id: 11,
+ name: 'KDG-11',
+ status: 'ACTIVE',
+ location: {
+ id: 101,
+ name: 'Farm Alpha',
+ address: 'Jl. Raya Alpha No. 1',
+ area: { id: 1001, name: 'Area Utara' },
+ },
+ pic: {
+ id: 501,
+ id_user: 501,
+ email: 'rina@farm.co',
+ name: 'Rina Setiawan',
+ },
+ created_user: {
+ id: 9001,
+ id_user: 9001,
+ email: 'admin@farm.co',
+ name: 'System Admin',
+ },
+ created_at: '2025-09-01T08:00:00Z',
+ updated_at: '2025-10-01T08:00:00Z',
+ },
+ quantity: 600,
+ },
+ {
+ kandang: {
+ id: 12,
+ name: 'KDG-12',
+ status: 'ACTIVE',
+ location: {
+ id: 101,
+ name: 'Farm Alpha',
+ address: 'Jl. Raya Alpha No. 1',
+ area: { id: 1001, name: 'Area Utara' },
+ },
+ pic: {
+ id: 502,
+ id_user: 502,
+ email: 'budi@farm.co',
+ name: 'Budi Hartono',
+ },
+ created_user: {
+ id: 9002,
+ id_user: 9002,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-08-25T08:00:00Z',
+ updated_at: '2025-09-20T08:00:00Z',
+ },
+ quantity: 600,
+ },
+ ],
+ reason: 'Start laying cycle',
+ created_user: {
+ id: 9100,
+ id_user: 9100,
+ email: 'planner@farm.co',
+ name: 'Planning',
+ },
+ created_at: '2025-10-10T06:00:00Z',
+ updated_at: '2025-10-10T06:00:00Z',
+ },
+ {
+ id: 202,
+ transfer_date: '2025-10-11',
+ flock_source: { id: 2, name: 'Flock B' },
+ flock_destination: { id: 10, name: 'Laying House 1' },
+ quantity: 950,
+ kandangs: [
+ {
+ kandang: {
+ id: 13,
+ name: 'KDG-13',
+ status: 'ACTIVE',
+ location: {
+ id: 102,
+ name: 'Farm Beta',
+ address: 'Jl. Melati 2',
+ area: { id: 1002, name: 'Area Selatan' },
+ },
+ pic: {
+ id: 503,
+ id_user: 503,
+ email: 'sari@farm.co',
+ name: 'Sari Wulandari',
+ },
+ created_user: {
+ id: 9003,
+ id_user: 9003,
+ email: 'system@farm.co',
+ name: 'System',
+ },
+ created_at: '2025-09-05T08:00:00Z',
+ updated_at: '2025-10-02T08:00:00Z',
+ },
+ quantity: 500,
+ },
+ {
+ kandang: {
+ id: 14,
+ name: 'KDG-14',
+ status: 'MAINTENANCE',
+ location: {
+ id: 102,
+ name: 'Farm Beta',
+ address: 'Jl. Melati 2',
+ area: { id: 1002, name: 'Area Selatan' },
+ },
+ pic: {
+ id: 504,
+ id_user: 504,
+ email: 'agus@farm.co',
+ name: 'Agus Pratama',
+ },
+ created_user: {
+ id: 9003,
+ id_user: 9003,
+ email: 'system@farm.co',
+ name: 'System',
+ },
+ created_at: '2025-07-15T08:00:00Z',
+ updated_at: '2025-09-15T08:00:00Z',
+ },
+ quantity: 450,
+ },
+ ],
+ reason: 'Capacity balancing',
+ created_user: {
+ id: 9101,
+ id_user: 9101,
+ email: 'planner@farm.co',
+ name: 'Planning',
+ },
+ created_at: '2025-10-11T06:00:00Z',
+ updated_at: '2025-10-11T06:00:00Z',
+ },
+ {
+ id: 203,
+ transfer_date: '2025-10-12',
+ flock_source: { id: 3, name: 'Flock C' },
+ flock_destination: { id: 11, name: 'Laying House 2' },
+ quantity: 1100,
+ kandangs: [
+ {
+ kandang: {
+ id: 21,
+ name: 'KDG-21',
+ status: 'ACTIVE',
+ location: {
+ id: 103,
+ name: 'Farm Gamma',
+ address: 'Dusun Gamma',
+ area: { id: 1003, name: 'Area Timur' },
+ },
+ pic: {
+ id: 505,
+ id_user: 505,
+ email: 'dewi@farm.co',
+ name: 'Dewi Lestari',
+ },
+ created_user: {
+ id: 9004,
+ id_user: 9004,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-10T08:00:00Z',
+ updated_at: '2025-10-05T08:00:00Z',
+ },
+ quantity: 700,
+ },
+ {
+ kandang: {
+ id: 22,
+ name: 'KDG-22',
+ status: 'ACTIVE',
+ location: {
+ id: 103,
+ name: 'Farm Gamma',
+ address: 'Dusun Gamma',
+ area: { id: 1003, name: 'Area Timur' },
+ },
+ pic: {
+ id: 506,
+ id_user: 506,
+ email: 'hadi@farm.co',
+ name: 'Hadi Santoso',
+ },
+ created_user: {
+ id: 9004,
+ id_user: 9004,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-12T08:00:00Z',
+ updated_at: '2025-10-06T08:00:00Z',
+ },
+ quantity: 400,
+ },
+ ],
+ reason: 'Align age group',
+ created_user: {
+ id: 9102,
+ id_user: 9102,
+ email: 'scheduler@farm.co',
+ name: 'Scheduler',
+ },
+ created_at: '2025-10-12T06:00:00Z',
+ updated_at: '2025-10-12T06:00:00Z',
+ },
+ {
+ id: 204,
+ transfer_date: '2025-10-13',
+ flock_source: { id: 1, name: 'Flock A' },
+ flock_destination: { id: 12, name: 'Laying House 3' },
+ quantity: 800,
+ kandangs: [
+ {
+ kandang: {
+ id: 15,
+ name: 'KDG-15',
+ status: 'ACTIVE',
+ location: {
+ id: 101,
+ name: 'Farm Alpha',
+ address: 'Jl. Raya Alpha No. 1',
+ area: { id: 1001, name: 'Area Utara' },
+ },
+ pic: {
+ id: 507,
+ id_user: 507,
+ email: 'nadia@farm.co',
+ name: 'Nadia Putri',
+ },
+ created_user: {
+ id: 9001,
+ id_user: 9001,
+ email: 'admin@farm.co',
+ name: 'System Admin',
+ },
+ created_at: '2025-08-01T08:00:00Z',
+ updated_at: '2025-09-28T08:00:00Z',
+ },
+ quantity: 800,
+ },
+ ],
+ reason: 'Open capacity in LH3',
+ created_user: {
+ id: 9103,
+ id_user: 9103,
+ email: 'opslead@farm.co',
+ name: 'Ops Lead',
+ },
+ created_at: '2025-10-13T06:00:00Z',
+ updated_at: '2025-10-13T06:00:00Z',
+ },
+ {
+ id: 205,
+ transfer_date: '2025-10-14',
+ flock_source: { id: 4, name: 'Flock D' },
+ flock_destination: { id: 11, name: 'Laying House 2' },
+ quantity: 650,
+ kandangs: [
+ {
+ kandang: {
+ id: 23,
+ name: 'KDG-23',
+ status: 'ACTIVE',
+ location: {
+ id: 104,
+ name: 'Farm Delta',
+ address: 'Jl. Delta 4',
+ area: { id: 1004, name: 'Area Barat' },
+ },
+ pic: {
+ id: 508,
+ id_user: 508,
+ email: 'yoga@farm.co',
+ name: 'Yoga Prabowo',
+ },
+ created_user: {
+ id: 9005,
+ id_user: 9005,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-20T08:00:00Z',
+ updated_at: '2025-10-10T08:00:00Z',
+ },
+ quantity: 350,
+ },
+ {
+ kandang: {
+ id: 24,
+ name: 'KDG-24',
+ status: 'ACTIVE',
+ location: {
+ id: 104,
+ name: 'Farm Delta',
+ address: 'Jl. Delta 4',
+ area: { id: 1004, name: 'Area Barat' },
+ },
+ pic: {
+ id: 509,
+ id_user: 509,
+ email: 'mega@farm.co',
+ name: 'Mega Anggraini',
+ },
+ created_user: {
+ id: 9005,
+ id_user: 9005,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-21T08:00:00Z',
+ updated_at: '2025-10-10T08:00:00Z',
+ },
+ quantity: 300,
+ },
+ ],
+ reason: 'Fill remaining LH2 slots',
+ created_user: {
+ id: 9104,
+ id_user: 9104,
+ email: 'scheduler@farm.co',
+ name: 'Scheduler',
+ },
+ created_at: '2025-10-14T06:00:00Z',
+ updated_at: '2025-10-14T06:00:00Z',
+ },
+ {
+ id: 206,
+ transfer_date: '2025-10-15',
+ flock_source: { id: 2, name: 'Flock B' },
+ flock_destination: { id: 12, name: 'Laying House 3' },
+ quantity: 720,
+ kandangs: [
+ {
+ kandang: {
+ id: 16,
+ name: 'KDG-16',
+ status: 'ACTIVE',
+ location: {
+ id: 102,
+ name: 'Farm Beta',
+ address: 'Jl. Melati 2',
+ area: { id: 1002, name: 'Area Selatan' },
+ },
+ pic: {
+ id: 510,
+ id_user: 510,
+ email: 'rizky@farm.co',
+ name: 'Rizky Maulana',
+ },
+ created_user: {
+ id: 9003,
+ id_user: 9003,
+ email: 'system@farm.co',
+ name: 'System',
+ },
+ created_at: '2025-08-12T08:00:00Z',
+ updated_at: '2025-10-01T08:00:00Z',
+ },
+ quantity: 420,
+ },
+ {
+ kandang: {
+ id: 17,
+ name: 'KDG-17',
+ status: 'ACTIVE',
+ location: {
+ id: 102,
+ name: 'Farm Beta',
+ address: 'Jl. Melati 2',
+ area: { id: 1002, name: 'Area Selatan' },
+ },
+ pic: {
+ id: 511,
+ id_user: 511,
+ email: 'tia@farm.co',
+ name: 'Tia Safitri',
+ },
+ created_user: {
+ id: 9003,
+ id_user: 9003,
+ email: 'system@farm.co',
+ name: 'System',
+ },
+ created_at: '2025-08-18T08:00:00Z',
+ updated_at: '2025-10-01T08:00:00Z',
+ },
+ quantity: 300,
+ },
+ ],
+ reason: 'Stage-by-stage transfer',
+ created_user: {
+ id: 9105,
+ id_user: 9105,
+ email: 'opslead@farm.co',
+ name: 'Ops Lead',
+ },
+ created_at: '2025-10-15T06:00:00Z',
+ updated_at: '2025-10-15T06:00:00Z',
+ },
+ {
+ id: 207,
+ transfer_date: '2025-10-16',
+ flock_source: { id: 5, name: 'Flock E' },
+ flock_destination: { id: 10, name: 'Laying House 1' },
+ quantity: 500,
+ kandangs: [
+ {
+ kandang: {
+ id: 18,
+ name: 'KDG-18',
+ status: 'ACTIVE',
+ location: {
+ id: 105,
+ name: 'Farm Epsilon',
+ address: 'Kp. Epsilon',
+ area: { id: 1005, name: 'Area Tengah' },
+ },
+ pic: {
+ id: 512,
+ id_user: 512,
+ email: 'lukas@farm.co',
+ name: 'Lukas Aditya',
+ },
+ created_user: {
+ id: 9006,
+ id_user: 9006,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-01T08:00:00Z',
+ updated_at: '2025-10-12T08:00:00Z',
+ },
+ quantity: 250,
+ },
+ {
+ kandang: {
+ id: 19,
+ name: 'KDG-19',
+ status: 'ACTIVE',
+ location: {
+ id: 105,
+ name: 'Farm Epsilon',
+ address: 'Kp. Epsilon',
+ area: { id: 1005, name: 'Area Tengah' },
+ },
+ pic: {
+ id: 513,
+ id_user: 513,
+ email: 'maya@farm.co',
+ name: 'Maya Kartika',
+ },
+ created_user: {
+ id: 9006,
+ id_user: 9006,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-03T08:00:00Z',
+ updated_at: '2025-10-12T08:00:00Z',
+ },
+ quantity: 250,
+ },
+ ],
+ reason: 'Trial batch',
+ created_user: {
+ id: 9106,
+ id_user: 9106,
+ email: 'planner@farm.co',
+ name: 'Planning',
+ },
+ created_at: '2025-10-16T06:00:00Z',
+ updated_at: '2025-10-16T06:00:00Z',
+ },
+ {
+ id: 208,
+ transfer_date: '2025-10-17',
+ flock_source: { id: 6, name: 'Flock F' },
+ flock_destination: { id: 13, name: 'Laying House 4' },
+ quantity: 1300,
+ kandangs: [
+ {
+ kandang: {
+ id: 25,
+ name: 'KDG-25',
+ status: 'ACTIVE',
+ location: {
+ id: 106,
+ name: 'Farm Zeta',
+ address: 'Jl. Zeta 6',
+ area: { id: 1006, name: 'Area Tenggara' },
+ },
+ pic: {
+ id: 514,
+ id_user: 514,
+ email: 'rara@farm.co',
+ name: 'Rara Pertiwi',
+ },
+ created_user: {
+ id: 9007,
+ id_user: 9007,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-07T08:00:00Z',
+ updated_at: '2025-10-13T08:00:00Z',
+ },
+ quantity: 700,
+ },
+ {
+ kandang: {
+ id: 26,
+ name: 'KDG-26',
+ status: 'ACTIVE',
+ location: {
+ id: 106,
+ name: 'Farm Zeta',
+ address: 'Jl. Zeta 6',
+ area: { id: 1006, name: 'Area Tenggara' },
+ },
+ pic: {
+ id: 515,
+ id_user: 515,
+ email: 'doni@farm.co',
+ name: 'Doni Firmansyah',
+ },
+ created_user: {
+ id: 9007,
+ id_user: 9007,
+ email: 'ops@farm.co',
+ name: 'Ops Bot',
+ },
+ created_at: '2025-09-09T08:00:00Z',
+ updated_at: '2025-10-13T08:00:00Z',
+ },
+ quantity: 600,
+ },
+ ],
+ reason: 'Open new house',
+ created_user: {
+ id: 9107,
+ id_user: 9107,
+ email: 'scheduler@farm.co',
+ name: 'Scheduler',
+ },
+ created_at: '2025-10-17T06:00:00Z',
+ updated_at: '2025-10-17T06:00:00Z',
+ },
+ ],
+};
+
+// TODO: delete this dummy data
+const FLOCK_SOURCE_DUMMY_DATA: BaseApiResponse = {
+ code: 200,
+ status: 'success',
+ message: 'Get all projectflocks successfully',
+ meta: {
+ page: 1,
+ limit: 10,
+ total_pages: 1,
+ total_results: 2,
+ },
+ data: [
+ {
+ id: 2,
+ name: 'Flock Banten',
+ totalQuantity: 300,
+ kandangs: [
+ {
+ kandang: {
+ id: 3,
+ name: 'Cikaum 1',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ {
+ kandang: {
+ id: 4,
+ name: 'Cikaum 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 150,
+ },
+ {
+ kandang: {
+ id: 5,
+ name: 'Cikaum 3',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 50,
+ },
+ ],
+ },
+
+ {
+ id: 3,
+ name: 'Flock Priangan',
+ totalQuantity: 200,
+ kandangs: [
+ {
+ kandang: {
+ id: 3,
+ name: 'Cikaum 1',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'Priangan',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ {
+ kandang: {
+ id: 4,
+ name: 'Cikaum 2',
+ status: 'ACTIVE',
+ location: {
+ id: 1,
+ name: 'Singaparna',
+ address: 'Tasik',
+ area: {
+ id: 1,
+ name: 'test area',
+ },
+ },
+ pic: {
+ id: 1,
+ id_user: 1,
+ email: 'admin@mbugroup.id',
+ name: 'Super Admin',
+ },
+ },
+ quantity: 100,
+ },
+ ],
+ },
+ ],
+};
+
+export class TransferToLayingService extends BaseApiService<
+ TransferToLaying,
+ CreateTransferToLayingPayload,
+ UpdateTransferToLayingPayload
+> {
+ constructor(basePath: string = '') {
+ super(basePath);
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ override async getAllFetcher(
+ endpoint: string
+ ): Promise> {
+ // return await httpClientFetcher>(endpoint);
+
+ await sleep(750);
+
+ return GET_ALL_TRANSFER_TO_LAYING_DUMMY_DATA;
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async getFlockSource(): Promise<
+ BaseApiResponse | undefined
+ > {
+ try {
+ // const getFlockSourcePath = `${this.basePath}/${flockSourcePath}`;
+ // const getSingleRes = await httpClient(getFlockSourcePath);
+ // return getSingleRes;
+
+ await sleep(500);
+
+ return FLOCK_SOURCE_DUMMY_DATA;
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async approve(
+ id: number
+ ): Promise | undefined> {
+ try {
+ await sleep(750);
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Berhasil approve data transfer ke laying!',
+ data: {
+ message: 'Berhasil approve data transfer ke laying!',
+ },
+ };
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async bulkApprove(
+ ids: number[]
+ ): Promise | undefined> {
+ try {
+ await sleep(750);
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Berhasil approve data transfer ke laying!',
+ data: {
+ message: 'Berhasil approve data transfer ke laying!',
+ },
+ };
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async reject(
+ id: number
+ ): Promise | undefined> {
+ try {
+ await sleep(750);
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Berhasil reject data transfer ke laying!',
+ data: {
+ message: 'Berhasil reject data transfer ke laying!',
+ },
+ };
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+
+ // TODO: remove dummy data and integrate to real API
+ async bulkReject(
+ ids: number[]
+ ): Promise | undefined> {
+ try {
+ await sleep(750);
+
+ return {
+ code: 200,
+ status: 'success',
+ message: 'Berhasil reject data transfer ke laying!',
+ data: {
+ message: 'Berhasil reject data transfer ke laying!',
+ },
+ };
+ } catch (error) {
+ // if (axios.isAxiosError>(error)) {
+ // return error.response?.data;
+ // }
+
+ return undefined;
+ }
+ }
+}
+
+export const TransferToLayingApi = new TransferToLayingService('');
diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts
index 2a720018..a42eaa3f 100644
--- a/src/types/api/api-general.d.ts
+++ b/src/types/api/api-general.d.ts
@@ -113,3 +113,5 @@ export type BaseApproval = {
action_by: CreatedUser;
action_at: string;
};
+
+export type ApproveAction = 'APPROVED' | 'REJECTED';
diff --git a/src/types/api/master-data/flock.d.ts b/src/types/api/master-data/flock.d.ts
index e0dcfda4..daa7babf 100644
--- a/src/types/api/master-data/flock.d.ts
+++ b/src/types/api/master-data/flock.d.ts
@@ -12,3 +12,15 @@ export type CreateFlockPayload = {
};
export type UpdateFlockPayload = CreateFlockPayload;
+
+// ---------------------------------------
+// TODO: adjust this later after Transfer to Laying API done
+import { BaseKandang } from '@/types/api/master-data/kandang';
+
+export type FlockWithKandangs = BaseFlock & {
+ totalQuantity: number;
+ kandangs: {
+ kandang: BaseKandang;
+ quantity: number;
+ }[];
+};
diff --git a/src/types/api/production/transfer-to-laying.d.ts b/src/types/api/production/transfer-to-laying.d.ts
new file mode 100644
index 00000000..77d35d8f
--- /dev/null
+++ b/src/types/api/production/transfer-to-laying.d.ts
@@ -0,0 +1,37 @@
+import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general';
+import { Kandang } from '@/types/api/master-data/kandang';
+
+export type BaseTransferToLaying = {
+ id: number;
+ transfer_date: string;
+ flock_source: {
+ id: number;
+ name: string;
+ };
+ flock_destination: {
+ id: number;
+ name: string;
+ };
+ quantity: number;
+ kandangs: {
+ kandang: Kandang;
+ quantity: number;
+ }[];
+ reason: string;
+};
+
+export type TransferToLaying = BaseMetadata & BaseTransferToLaying;
+
+export type CreateTransferToLayingPayload = {
+ transfer_date: string;
+ flock_source_id: number;
+ flock_destination_id: number;
+ totalQuantity: number;
+ kandangs: {
+ kandang_id: number;
+ quantity: number;
+ }[];
+ reason: string;
+};
+
+export type UpdateTransferToLayingPayload = CreateTransferToLayingPayload;