diff --git a/src/app/production/transfer-to-laying/detail/page.tsx b/src/app/production/transfer-to-laying/detail/page.tsx
index 4d603098..de5426c8 100644
--- a/src/app/production/transfer-to-laying/detail/page.tsx
+++ b/src/app/production/transfer-to-laying/detail/page.tsx
@@ -13,7 +13,7 @@ import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
// TODO: delete dummy data
const DUMMY_TRANSFER_TO_LAYING_DETAIL: TransferToLaying = {
id: 1,
- transfer_date: '14-10-2025',
+ transfer_date: '2025-10-14',
flock_source: {
id: 1,
name: 'Flock asal test',
@@ -114,9 +114,11 @@ const TransferToLayingDetail = () => {
);
}
+ // TODO: remove dummy data and integrate with real API
if (
!isLoadingTransferToLaying &&
- (!transferToLaying || isResponseError(transferToLaying))
+ (!transferToLaying ||
+ (isResponseError(transferToLaying) && !DUMMY_TRANSFER_TO_LAYING_DETAIL))
) {
router.replace('/404');
return;
@@ -127,12 +129,18 @@ const TransferToLayingDetail = () => {
{isLoadingTransferToLaying && (
)}
- {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
+ {/* {!isLoadingTransferToLaying && isResponseSuccess(transferToLaying) && (
- )}
+ )} */}
+
+ {/* TODO: remove this dummy data and integrate to real API */}
+
);
};
diff --git a/src/app/production/transfer-to-laying/page.tsx b/src/app/production/transfer-to-laying/page.tsx
index 40829c20..84513542 100644
--- a/src/app/production/transfer-to-laying/page.tsx
+++ b/src/app/production/transfer-to-laying/page.tsx
@@ -1,14 +1,9 @@
-import { Icon } from '@iconify/react';
-import Button from '@/components/Button';
+import TransferToLayingsTable from '@/components/pages/production/transfer-to-laying/TransferToLayingsTable';
const TransferToLaying = () => {
return (
- Transfer to Laying
-
+
);
};
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/Table.tsx b/src/components/Table.tsx
index cfd77df6..d3498e33 100644
--- a/src/components/Table.tsx
+++ b/src/components/Table.tsx
@@ -48,6 +48,8 @@ export interface TableProps
{
sorting?: SortingState;
setSorting?: OnChangeFn;
manualSorting?: boolean;
+ rowSelection?: Record;
+ setRowSelection?: OnChangeFn>;
}
const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}];
@@ -86,6 +88,8 @@ const Table = ({
sorting,
setSorting,
manualSorting = false,
+ rowSelection,
+ setRowSelection,
}: TableProps) => {
const isServerSideTable =
totalItems !== undefined &&
@@ -137,6 +141,15 @@ const Table = ({
};
}
+ if (rowSelection && setRowSelection) {
+ tableOptions.onRowSelectionChange = setRowSelection;
+ tableOptions.state = {
+ ...tableOptions.state,
+ rowSelection,
+ };
+ tableOptions.getRowId = (row) => (row as { id: string }).id;
+ }
+
const table = useReactTable(tableOptions);
const { setPageSize } = table;
diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx
new file mode 100644
index 00000000..fb0c95c7
--- /dev/null
+++ b/src/components/input/CheckboxInput.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import { HTMLProps, useEffect, useRef } from 'react';
+import { cn } from '@/lib/helper';
+
+interface CheckboxInputProps extends HTMLProps {
+ name: string;
+ label?: string;
+ indeterminate?: boolean;
+ classNames?: {
+ wrapper?: string;
+ inputWrapper?: string;
+ checkbox?: string;
+ label?: string;
+ };
+ isError?: boolean;
+ isValid?: boolean;
+ errorMessage?: string;
+}
+
+const CheckboxInput = ({
+ indeterminate,
+ name,
+ label,
+ className,
+ classNames,
+ isValid,
+ isError,
+ errorMessage,
+ ...rest
+}: CheckboxInputProps) => {
+ const ref = useRef(null!);
+
+ useEffect(() => {
+ if (typeof indeterminate === 'boolean') {
+ ref.current.indeterminate = !rest.checked && indeterminate;
+ }
+ }, [ref, indeterminate]);
+
+ return (
+
+
+
+
+ {label && (
+
+ )}
+
+
+ {errorMessage &&
{errorMessage}}
+
+ );
+};
+
+export default CheckboxInput;
diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx
index b491077f..6aaedc17 100644
--- a/src/components/input/SelectInput.tsx
+++ b/src/components/input/SelectInput.tsx
@@ -252,8 +252,8 @@ const useSelect = (
const options = isResponseSuccess(data)
? data.data.map((item) => {
return {
- value: getByPath(item, valueKey as string),
- label: getByPath(item, labelKey as string),
+ value: getByPath(item, valueKey as string),
+ label: getByPath(item, labelKey as string),
};
})
: [];
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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
+ <>
+
+
+
+
+
+
+ {selectedRowIds.length > 0 && (
+ <>
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
index c4428a72..b026b47f 100644
--- a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
+++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx
@@ -288,7 +288,7 @@ const TransferToLayingForm = ({