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 (
-
+
MBU Logo { />

LTI ERP

+ +
+ +
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 ( +
+ + + + + + + + + +
+ ); +}; + +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.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 ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah Transfer ke Laying'} + {type === 'edit' && 'Edit Transfer ke Laying'} + {type === 'detail' && 'Detail Transfer ke Laying'} +

+
+ +
+ {type === 'detail' && ( + <> + + + + + )} +
+ +
+
+ + +
+ + + +
+ + + +
+
+ + + + + + + + + + {(!formik.values.kandangs || + formik.values.kandangs.length === 0) && ( + + + + )} + + {formik.values.kandangs && + formik.values.kandangs.map((kandang, idx) => ( + + + + + + ))} + +
KandangKuantitas
+

+ Pilih flock asal terlebih dahulu! +

+
+ + + +
+
+
+ +