From e53325cdc5eaefec0e9592e93cf9bf8b3fcf8592 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 11:53:12 +0700 Subject: [PATCH 01/11] feat(FE-147): show Transfer to Laying table --- src/app/production/transfer-to-laying/page.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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

- +
); }; From c24c0817aec51eb81da39c5b5de47b4dddb22cd4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 11:53:35 +0700 Subject: [PATCH 02/11] chore(FE-147): add rowSelection and setRowSelection props --- src/components/Table.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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; From 1e2ea79a6a2d4e583a819921950f797c09dc4f69 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:51:20 +0700 Subject: [PATCH 03/11] chore(FE-147): add close button for MainDrawer --- src/components/MainDrawer.tsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 (
-
+
MBU Logo { />

LTI ERP

+ +
+ +
From 204369e0feb41e820fe712858d3b775690ba9e35 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:51:39 +0700 Subject: [PATCH 04/11] feat(FE-147): add CheckboxInput component --- src/components/input/CheckboxInput.tsx | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/components/input/CheckboxInput.tsx 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; From 3c8bdfbdac062e2754c3521d79e0340d896a1950 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:52:29 +0700 Subject: [PATCH 05/11] chore(FE-147): set generic when using getByPath function --- src/components/input/SelectInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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), }; }) : []; From 5e710a792f473055e9d4b0223ef0e55b97e297d8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:52:51 +0700 Subject: [PATCH 06/11] chore(FE-147): set moment locale to 'id' globally --- src/lib/helper.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 0f827e8a..fe5811fd 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)); @@ -42,9 +46,9 @@ export function getByPath( obj: T, path: string, defaultValue?: D -): unknown | D { +): D { if (obj == null) return defaultValue as D; - if (!path) return obj as unknown; + if (!path) return obj as D; const segments = path.split('.').filter(Boolean); let cur: { [key: string]: unknown } = obj; @@ -59,5 +63,5 @@ export function getByPath( cur = cur[key] as { [key: string]: unknown }; } - return cur as unknown; + return cur as D; } From d2f24723fce85c9b05285dd47e49d6169afe2aad Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:53:41 +0700 Subject: [PATCH 07/11] chore(FE-141): set dummy data for Transfer to Laying detail page --- .../transfer-to-laying/detail/page.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) 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 */} +
); }; From ab9fbc903273a18a48a5f9f2711c9c0c651d57ad Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:54:02 +0700 Subject: [PATCH 08/11] feat(FE-147): create TransferToLayingsTable component --- .../TransferToLayingsTable.tsx | 633 ++++++++++++++++++ 1 file changed, 633 insertions(+) create mode 100644 src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx 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; From 9a51b2876f78ce2b2aa5aeed4d3be37735b51ffb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Oct 2025 12:54:46 +0700 Subject: [PATCH 09/11] chore(FE-113,140,141): adjust back button link --- .../transfer-to-laying/form/TransferToLayingForm.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 = ({