diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..250df482 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,15 @@ +{ + "singleQuote": true, + "jsxSingleQuote": true, + "endOfLine": "lf", + "arrowParens": "always", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "printWidth": 80, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "semi": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/src/app/master-data/bank/add/page.tsx b/src/app/master-data/bank/add/page.tsx new file mode 100644 index 00000000..0bb6e532 --- /dev/null +++ b/src/app/master-data/bank/add/page.tsx @@ -0,0 +1,11 @@ +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +const AddBank = () => { + return ( +
+ +
+ ); +}; + +export default AddBank; diff --git a/src/app/master-data/bank/detail/edit/page.tsx b/src/app/master-data/bank/detail/edit/page.tsx new file mode 100644 index 00000000..a0939af9 --- /dev/null +++ b/src/app/master-data/bank/detail/edit/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +import { BankApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const BankEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const bankId = searchParams.get('bankId'); + + const { data: bank, isLoading: isLoadingBank } = useSWR( + bankId, + (id: number) => BankApi.getSingle(id) + ); + + if (!bankId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingBank && (!bank || isResponseError(bank))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingBank && } + {!isLoadingBank && isResponseSuccess(bank) && ( + + )} +
+ ); +}; + +export default BankEdit; diff --git a/src/app/master-data/bank/detail/layout.tsx b/src/app/master-data/bank/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/bank/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/master-data/bank/detail/page.tsx b/src/app/master-data/bank/detail/page.tsx new file mode 100644 index 00000000..bd1661d8 --- /dev/null +++ b/src/app/master-data/bank/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import BankForm from '@/components/pages/master-data/bank/form/BankForm'; + +import { BankApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const BankDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const bankId = searchParams.get('bankId'); + + const { data: bank, isLoading: isLoadingBank } = useSWR( + bankId, + (id: number) => BankApi.getSingle(id) + ); + + if (!bankId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingBank && (!bank || isResponseError(bank))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingBank && } + {!isLoadingBank && isResponseSuccess(bank) && ( + + )} +
+ ); +}; + +export default BankDetail; diff --git a/src/app/master-data/bank/page.tsx b/src/app/master-data/bank/page.tsx new file mode 100644 index 00000000..3f913c55 --- /dev/null +++ b/src/app/master-data/bank/page.tsx @@ -0,0 +1,11 @@ +import BanksTable from '@/components/pages/master-data/bank/BanksTable'; + +const Bank = () => { + return ( +
+ +
+ ); +}; + +export default Bank; diff --git a/src/app/master-data/fcr/add/page.tsx b/src/app/master-data/fcr/add/page.tsx new file mode 100644 index 00000000..9a74034d --- /dev/null +++ b/src/app/master-data/fcr/add/page.tsx @@ -0,0 +1,11 @@ +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +const AddFcr = () => { + return ( +
+ +
+ ); +}; + +export default AddFcr; diff --git a/src/app/master-data/fcr/detail/edit/page.tsx b/src/app/master-data/fcr/detail/edit/page.tsx new file mode 100644 index 00000000..54277e8a --- /dev/null +++ b/src/app/master-data/fcr/detail/edit/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +import { FcrApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { FcrWithStandards } from '@/types/api/master-data/fcr'; + +const FcrEdit = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const fcrId = searchParams.get('fcrId'); + + const { data: fcr, isLoading: isLoadingFcr } = useSWR( + fcrId, + (id: number) => + FcrApi.getSingle(id) as Promise< + BaseApiResponse | undefined + > + ); + + if (!fcrId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFcr && } + {!isLoadingFcr && isResponseSuccess(fcr) && ( + + )} +
+ ); +}; + +export default FcrEdit; diff --git a/src/app/master-data/fcr/detail/layout.tsx b/src/app/master-data/fcr/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/fcr/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/master-data/fcr/detail/page.tsx b/src/app/master-data/fcr/detail/page.tsx new file mode 100644 index 00000000..5db1ab32 --- /dev/null +++ b/src/app/master-data/fcr/detail/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import FcrForm from '@/components/pages/master-data/fcr/form/FcrForm'; + +import { FcrApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { FcrWithStandards } from '@/types/api/master-data/fcr'; +import { BaseApiResponse } from '@/types/api/api-general'; + +const FcrDetail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const fcrId = searchParams.get('fcrId'); + + const { data: fcr, isLoading: isLoadingFcr } = useSWR( + fcrId, + (id: number) => + FcrApi.getSingle(id) as Promise< + BaseApiResponse | undefined + > + ); + + if (!fcrId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingFcr && (!fcr || isResponseError(fcr))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingFcr && } + {!isLoadingFcr && isResponseSuccess(fcr) && ( + + )} +
+ ); +}; + +export default FcrDetail; diff --git a/src/app/master-data/fcr/page.tsx b/src/app/master-data/fcr/page.tsx new file mode 100644 index 00000000..9ca9c55d --- /dev/null +++ b/src/app/master-data/fcr/page.tsx @@ -0,0 +1,11 @@ +import FcrsTable from '@/components/pages/master-data/fcr/FcrsTable'; + +const Fcr = () => { + return ( +
+ +
+ ); +}; + +export default Fcr; diff --git a/src/app/master-data/nonstock/detail/edit/page.tsx b/src/app/master-data/nonstock/detail/edit/page.tsx index a0fbb6b3..3b3db5f5 100644 --- a/src/app/master-data/nonstock/detail/edit/page.tsx +++ b/src/app/master-data/nonstock/detail/edit/page.tsx @@ -5,8 +5,8 @@ import useSWR from 'swr'; import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; -import { getNonstock } from '@/services/api/master-data/nonstock'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; const NonstockEdit = () => { const router = useRouter(); @@ -16,7 +16,7 @@ const NonstockEdit = () => { const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( nonstockId, - getNonstock + (id: number) => NonstockApi.getSingle(id) ); if (!nonstockId) { @@ -29,7 +29,7 @@ const NonstockEdit = () => { ); } - if (!isLoadingNonstock && !nonstock) { + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { router.replace('/404'); return; } diff --git a/src/app/master-data/nonstock/detail/page.tsx b/src/app/master-data/nonstock/detail/page.tsx index 375ec999..798a843e 100644 --- a/src/app/master-data/nonstock/detail/page.tsx +++ b/src/app/master-data/nonstock/detail/page.tsx @@ -5,8 +5,8 @@ import useSWR from 'swr'; import NonstockForm from '@/components/pages/master-data/nonstock/form/NonstockForm'; -import { getNonstock } from '@/services/api/master-data/nonstock'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { NonstockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; const NonstockDetail = () => { const router = useRouter(); @@ -16,7 +16,7 @@ const NonstockDetail = () => { const { data: nonstock, isLoading: isLoadingNonstock } = useSWR( nonstockId, - getNonstock + (id: number) => NonstockApi.getSingle(id) ); if (!nonstockId) { @@ -29,7 +29,7 @@ const NonstockDetail = () => { ); } - if (!isLoadingNonstock && !nonstock) { + if (!isLoadingNonstock && (!nonstock || isResponseError(nonstock))) { router.replace('/404'); return; } diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index f41316f4..be87f069 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -201,7 +201,7 @@ const MainDrawer = ({ if (!hasSubmenu || !menu.submenu) return; - const activeSubmenu = menu.submenu.find((item) => + const activeSubmenu = menu.submenu?.find((item) => isPathActive(pathname, item.link) ); diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 4a1cecd2..eec312c1 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -122,7 +122,9 @@ const TextInput = ({ {!isError && bottomLabel && (

{bottomLabel}

)} - {isError &&

{errorMessage}

} + {isError && errorMessage && ( +

{errorMessage}

+ )} ); }; diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx new file mode 100644 index 00000000..0d084491 --- /dev/null +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { ChangeEventHandler, useEffect, 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 } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Bank } from '@/types/api/master-data/bank'; +import { BankApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + + + +
+ ); +}; + +const BanksTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + }); + + const { + data: banks, + isLoading, + mutate: refreshBanks, + } = useSWR( + `${BankApi.basePath}${getTableFilterQueryString()}`, + BankApi.getAllFetcher + ); + + const deleteModal = useModal(); + + const [selectedBank, setSelectedBank] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); + + const banksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'alias', + header: 'Alias', + }, + { + accessorKey: 'account_number', + header: 'No. Rekening', + }, + { + accessorKey: 'owner', + header: 'Pemilik', + cell: (props) => (props.getValue() ? props.getValue() : '-'), + }, + { + 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 deleteClickHandler = () => { + setSelectedBank(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await BankApi.delete(selectedBank?.id as number); + refreshBanks(); + + deleteModal.closeModal(); + toast.success('Successfully delete Bank!'); + setIsDeleteLoading(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); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + data={isResponseSuccess(banks) ? banks?.data : []} + columns={banksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(banks) ? banks?.meta?.page : 0} + totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0} + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(banks) && banks?.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 BanksTable; diff --git a/src/components/pages/master-data/bank/form/BankForm.schema.ts b/src/components/pages/master-data/bank/form/BankForm.schema.ts new file mode 100644 index 00000000..0bf48c76 --- /dev/null +++ b/src/components/pages/master-data/bank/form/BankForm.schema.ts @@ -0,0 +1,14 @@ +import * as Yup from 'yup'; + +export const BankFormSchema = Yup.object({ + name: Yup.string().required('Nama wajib diisi!'), + alias: Yup.string() + .max(5, 'Maksimal 5 karakter!') + .required('Alias wajib diisi!'), + account_number: Yup.string().required('Rekening wajib diisi!'), + owner: Yup.string(), +}); + +export const UpdateBankFormSchema = BankFormSchema; + +export type BankFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/bank/form/BankForm.tsx b/src/components/pages/master-data/bank/form/BankForm.tsx new file mode 100644 index 00000000..442d5c76 --- /dev/null +++ b/src/components/pages/master-data/bank/form/BankForm.tsx @@ -0,0 +1,301 @@ +'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 { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + BankFormSchema, + BankFormValues, + UpdateBankFormSchema, +} from '@/components/pages/master-data/bank/form/BankForm.schema'; +import { isResponseError } from '@/lib/api-helper'; +import { + CreateBankPayload, + Bank, + UpdateBankPayload, +} from '@/types/api/master-data/bank'; +import { BankApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; + +interface BankFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: Bank; +} + +const BankForm = ({ type = 'add', initialValues }: BankFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [bankFormErrorMessage, setBankFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createBankHandler = useCallback( + async (payload: CreateBankPayload) => { + const createBankRes = await BankApi.create(payload); + + if (isResponseError(createBankRes)) { + setBankFormErrorMessage(createBankRes.message); + return; + } + + toast.success(createBankRes?.message as string); + router.push('/master-data/bank'); + }, + [router] + ); + + const updateBankHandler = useCallback( + async (bankId: number, payload: UpdateBankPayload) => { + const updateBankRes = await BankApi.update(bankId, payload); + + if (updateBankRes?.status === 'error') { + setBankFormErrorMessage(updateBankRes.message); + return; + } + + toast.success(updateBankRes?.message as string); + router.refresh(); + router.push('/master-data/bank'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => { + return { + name: initialValues?.name ?? '', + alias: initialValues?.alias ?? '', + account_number: initialValues?.account_number ?? '', + owner: initialValues?.owner, + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateBankFormSchema : BankFormSchema, + onSubmit: async (values) => { + setBankFormErrorMessage(''); + + const bankPayload: CreateBankPayload = { + name: values.name, + alias: values.alias, + account_number: values.account_number.toString(), + owner: values.owner ? values.owner : '', + }; + + switch (type) { + case 'add': + await createBankHandler(bankPayload); + break; + + case 'edit': + await updateBankHandler(initialValues?.id as number, bankPayload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const deleteBankClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await BankApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete Bank!'); + setIsDeleteLoading(false); + router.push('/master-data/bank'); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah Bank'} + {type === 'edit' && 'Edit Bank'} + {type === 'detail' && 'Detail Bank'} +

+
+ +
+
+ + + + + + + +
+ +
+ {type !== 'add' && ( +
+ + + {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {bankFormErrorMessage && ( +
+ + {bankFormErrorMessage} +
+ )} +
+
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default BankForm; diff --git a/src/components/pages/master-data/fcr/FcrsTable.tsx b/src/components/pages/master-data/fcr/FcrsTable.tsx new file mode 100644 index 00000000..5f0285bb --- /dev/null +++ b/src/components/pages/master-data/fcr/FcrsTable.tsx @@ -0,0 +1,276 @@ +'use client'; + +import { ChangeEventHandler, useEffect, 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 } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Fcr } from '@/types/api/master-data/fcr'; +import { FcrApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( +
+ + + + + +
+ ); +}; + +const FcrsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '' }, + paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + }); + + const { + data: fcrs, + isLoading, + mutate: refreshFcrs, + } = useSWR( + `${FcrApi.basePath}${getTableFilterQueryString()}`, + FcrApi.getAllFetcher + ); + + const deleteModal = useModal(); + + const [selectedFcr, setSelectedFcr] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); + + const fcrsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + 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 deleteClickHandler = () => { + setSelectedFcr(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FcrApi.delete(selectedFcr?.id as number); + refreshFcrs(); + + deleteModal.closeModal(); + toast.success('Successfully delete FCR!'); + setIsDeleteLoading(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); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ + +
+ +
+ +
+
+ + + data={isResponseSuccess(fcrs) ? fcrs?.data : []} + columns={fcrsColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(fcrs) ? fcrs?.meta?.page : 0} + totalItems={isResponseSuccess(fcrs) ? fcrs?.meta?.total_results : 0} + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': isResponseSuccess(fcrs) && fcrs?.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 FcrsTable; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts new file mode 100644 index 00000000..21b0b9ee --- /dev/null +++ b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts @@ -0,0 +1,26 @@ +import * as Yup from 'yup'; + +const FcrStandardSchema: Yup.ObjectSchema<{ + weight: number | string; + fcr_number: number | string; + mortality: number | string; +}> = Yup.object({ + weight: Yup.number().nullable().required('Bobot wajib diisi!'), + fcr_number: Yup.number() + .nullable() + .typeError('FCR harus angka!') + .required('FCR harus diisi!'), + mortality: Yup.number().nullable().required('Mortalitas wajib diisi!'), +}); + +export const FcrFormSchema = Yup.object({ + name: Yup.string().required('Nama wajib diisi!'), + fcrStandards: Yup.array() + .of(FcrStandardSchema) + .min(1, 'Minimal 1 FCR Standard diisi1') + .required('FCR wajib diisi!'), +}); + +export const UpdateFcrFormSchema = FcrFormSchema; + +export type FcrFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.tsx b/src/components/pages/master-data/fcr/form/FcrForm.tsx new file mode 100644 index 00000000..ab17eda8 --- /dev/null +++ b/src/components/pages/master-data/fcr/form/FcrForm.tsx @@ -0,0 +1,389 @@ +'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 { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import TextInput from '@/components/input/TextInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; + +import { + FcrFormSchema, + FcrFormValues, + UpdateFcrFormSchema, +} from '@/components/pages/master-data/fcr/form/FcrForm.schema'; +import { isResponseError } from '@/lib/api-helper'; +import { + CreateFcrPayload, + Fcr, + FcrWithStandards, + UpdateFcrPayload, +} from '@/types/api/master-data/fcr'; +import { FcrApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; + +interface FcrFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: FcrWithStandards; +} + +const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { + const router = useRouter(); + const deleteModal = useModal(); + + const [fcrFormErrorMessage, setFcrFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createFcrHandler = useCallback( + async (payload: CreateFcrPayload) => { + const createFcrRes = await FcrApi.create(payload); + + if (isResponseError(createFcrRes)) { + setFcrFormErrorMessage(createFcrRes.message); + return; + } + + toast.success(createFcrRes?.message as string); + router.push('/master-data/fcr'); + }, + [router] + ); + + const updateFcrHandler = useCallback( + async (fcrId: number, payload: UpdateFcrPayload) => { + const updateFcrRes = await FcrApi.update(fcrId, payload); + + if (updateFcrRes?.status === 'error') { + setFcrFormErrorMessage(updateFcrRes.message); + return; + } + + toast.success(updateFcrRes?.message as string); + router.refresh(); + router.push('/master-data/fcr'); + }, + [router] + ); + + const formikInitialValues = useMemo(() => { + return { + name: initialValues?.name ?? '', + fcrStandards: initialValues?.fcr_standards + ? initialValues?.fcr_standards + : [ + { + weight: '', + fcr_number: '', + mortality: '', + }, + ], + }; + }, [initialValues]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: type === 'edit' ? UpdateFcrFormSchema : FcrFormSchema, + onSubmit: async (values) => { + setFcrFormErrorMessage(''); + + const fcrPayload: CreateFcrPayload = { + name: values.name, + fcr_standards: values.fcrStandards as CreateFcrPayload['fcr_standards'], + }; + + switch (type) { + case 'add': + await createFcrHandler(fcrPayload); + break; + + case 'edit': + await updateFcrHandler(initialValues?.id as number, fcrPayload); + break; + } + }, + }); + + const { setValues: formikSetValues } = formik; + + const addFcrStandard = () => + formik.setFieldValue('fcrStandards', [ + ...formik.values.fcrStandards, + { + weight: '', + fcr_number: '', + mortality: '', + }, + ]); + + const removeFcrStandard = (i: number) => + formik.setFieldValue( + 'fcrStandards', + formik.values.fcrStandards.filter((_, idx) => idx !== i) + ); + + const deleteFcrClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await FcrApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete FCR!'); + setIsDeleteLoading(false); + router.push('/master-data/fcr'); + }; + + const isRepeaterInputError = ( + column: keyof CreateFcrPayload['fcr_standards'][0], + idx: number + ) => { + return ( + formik.touched.fcrStandards?.[idx]?.[column] && + Boolean( + formik.errors.fcrStandards?.[idx] instanceof Object && + formik.errors.fcrStandards?.[idx]?.[column] + ) + ); + }; + + useEffect(() => { + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); + + return ( + <> +
+
+ + +

+ {type === 'add' && 'Tambah FCR'} + {type === 'edit' && 'Edit FCR'} + {type === 'detail' && 'Detail FCR'} +

+
+ +
+
+ + +
+
+ + + + + + + {type !== 'detail' && } + + + + + {formik.values.fcrStandards.map((fcrStandard, idx) => ( + + + + + {type !== 'detail' && ( + + )} + + ))} + +
BobotFCRMortalitasAksi
+ + + + + + + +
+
+
+ + {type !== 'detail' && ( + + )} +
+ +
+ {type !== 'add' && ( +
+ + + {type !== 'edit' && ( + + )} +
+ )} + + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {fcrFormErrorMessage && ( +
+ + {fcrFormErrorMessage} +
+ )} +
+
+ + {type !== 'add' && ( + + )} + + ); +}; + +export default FcrForm; diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 4a2dbc5b..462b3488 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -1,20 +1,31 @@ 'use client'; -import { ChangeEventHandler, useState } from 'react'; +import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; import useSWR from 'swr'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; +import { + CellContext, + ColumnDef, + ColumnSort, + SortingState, +} from '@tanstack/react-table'; +import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; -import TextInput from '@/components/input/TextInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; -import Collapse from '@/components/Collapse'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import { httpClientFetcher } from '@/services/http/client'; -import { Nonstock, NonstocksResponse } from '@/types/api/master-data/nonstock'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { NonstockApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { deleteNonstock } from '@/services/api/master-data/nonstock'; import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ type = 'dropdown', @@ -23,7 +34,7 @@ const RowOptionsMenu = ({ }: { type: 'dropdown' | 'collapse'; props: CellContext; - deleteClickHandler: () => Promise; + deleteClickHandler: () => void; }) => { return (
; - isLast2Rows: boolean; - deleteClickHandler: () => Promise; -}) => { - return ( -
- - - -
- ); -}; - -const RowCollapseOptions = ({ - props, - deleteClickHandler, -}: { - props: CellContext; - deleteClickHandler: () => Promise; -}) => { - return ( - - - - } - className='w-fit' - titleClassName='p-0! justify-self-end' - > - - - ); -}; - const NonstocksTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { search: '', nameSort: '', locationSort: '', picSort: '' }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + locationSort: 'sort_location', + picSort: ' sort_pic', + }, + }); + const { data: nonstocks, isLoading, mutate: refreshNonstocks, - } = useSWR('/master-data/nonstocks', httpClientFetcher); + } = useSWR( + `${NonstockApi.basePath}${getTableFilterQueryString()}`, + NonstockApi.getAllFetcher + ); - const [searchValue, setSearchValue] = useState(''); + const deleteModal = useModal(); + + const [selectedNonstock, setSelectedNonstock] = useState< + Nonstock | undefined + >(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const [sorting, setSorting] = useState([]); const nonstocksColumns: ColumnDef[] = [ { header: '#', - cell: (props) => props.row.index + 1, + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, }, { - header: 'Nama', accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'uom', + header: 'UOM', + cell: (props) => props.row.original.uom.name, + }, + { + accessorKey: 'suppliers', + header: 'Supplier', + cell: (props) => { + const supplierNames = props.row.original.suppliers.map( + (supplier) => supplier.name + ); + + return supplierNames.join(', ') || '-'; + }, + }, + { + accessorKey: 'flags', + header: 'Flag', + cell: (props) => props.row.original.flags?.join(', ') || '-', }, { header: 'Aksi', @@ -157,33 +164,31 @@ const NonstocksTable = () => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - const deleteClickHandler = async () => { - const confirmation = confirm( - 'Apakah anda yakin untuk menghapus non stock ini?' - ); - - if (confirmation) { - await deleteNonstock(props.row.original.id); - refreshNonstocks(); - alert('Nonstock berhasil dihapus!'); - } + const deleteClickHandler = () => { + setSelectedNonstock(props.row.original); + deleteModal.openModal(); }; return ( <> {currentPageSize > 2 && ( - + + + )} {currentPageSize <= 2 && ( - + + + )} ); @@ -191,52 +196,133 @@ const NonstocksTable = () => { }, ]; - const searchChangeHandler: ChangeEventHandler = (e) => { - setSearchValue(e.target.value); + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await NonstockApi.delete(selectedNonstock?.id as number); + refreshNonstocks(); + + deleteModal.closeModal(); + toast.success('Successfully delete Nonstock!'); + setIsDeleteLoading(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 updateSortingFilter = useCallback( + ( + sortName: Exclude, + sortFilter: ColumnSort | undefined + ) => { + if (!sortFilter) { + updateFilter(sortName, ''); + } else { + updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); + } + }, + [updateFilter] + ); + + // track sorting + useEffect(() => { + const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); + const locationSortFilter = sorting.find( + (sortItem) => sortItem.id === 'location' + ); + const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic'); + + updateSortingFilter('nameSort', nameSortFilter); + updateSortingFilter('locationSort', locationSortFilter); + updateSortingFilter('picSort', picSortFilter); + }, [sorting]); + return ( -
-
-
- + <> +
+
+
+
+ +
+ + +
+ +
+ +
- + data={isResponseSuccess(nonstocks) ? nonstocks?.data : []} + columns={nonstocksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(nonstocks) ? nonstocks?.meta?.page : 0} + totalItems={ + isResponseSuccess(nonstocks) ? nonstocks?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(nonstocks) && nonstocks?.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', + }} />
- - data={isResponseSuccess(nonstocks) ? nonstocks?.data : []} - columns={nonstocksColumns} - pageSize={10} - fuzzySearchValue={searchValue} - onFuzzySearchValueChange={setSearchValue} - isLoading={isLoading} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(nonstocks) && nonstocks?.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', + -
+ ); }; diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts index 50f69c7d..8039ef76 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts @@ -2,6 +2,22 @@ import * as Yup from 'yup'; export const NonstockFormSchema = Yup.object({ name: Yup.string().required('Nama wajib diisi!'), + + uomId: Yup.number().min(1, 'UOM wajib diisi!').required('UOM wajib diisi!'), + uom: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + + supplierIds: Yup.array().of(Yup.number().min(0, 'Supplier wajib diisi!')), + suppliers: Yup.array().of( + Yup.object({ + value: Yup.number().min(0).required(), + label: Yup.string().required(), + }) + ), + + flags: Yup.array().of(Yup.string()).notRequired(), }); export const UpdateNonstockFormSchema = NonstockFormSchema; diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index efc1f595..7a67c9a7 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -3,26 +3,31 @@ 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 } from '@/components/input/SelectInput'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { NonstockFormSchema, NonstockFormValues, UpdateNonstockFormSchema, } from '@/components/pages/master-data/nonstock/form/NonstockForm.schema'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { - CreateNonstockPayload, Nonstock, + CreateNonstockPayload, UpdateNonstockPayload, } from '@/types/api/master-data/nonstock'; -import { - createNonstock, - updateNonstock, -} from '@/services/api/master-data/nonstock'; +import { NonstockApi, SupplierApi, UomApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { flags } from '@/types/api/api-general'; +import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; interface NonstockFormProps { type?: 'add' | 'edit' | 'detail'; @@ -31,19 +36,21 @@ interface NonstockFormProps { const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const router = useRouter(); + const deleteModal = useModal(); const [nonstockFormErrorMessage, setNonstockFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const createNonstockHandler = useCallback( async (payload: CreateNonstockPayload) => { - const createNonstockRes = await createNonstock(payload); + const createNonstockRes = await NonstockApi.create(payload); if (isResponseError(createNonstockRes)) { setNonstockFormErrorMessage(createNonstockRes.message); return; } - alert(createNonstockRes?.message); + toast.success(createNonstockRes?.message as string); router.push('/master-data/nonstock'); }, [router] @@ -51,14 +58,14 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const updateNonstockHandler = useCallback( async (nonstockId: number, payload: UpdateNonstockPayload) => { - const updateNonstockRes = await updateNonstock(nonstockId, payload); + const updateNonstockRes = await NonstockApi.update(nonstockId, payload); if (updateNonstockRes?.status === 'error') { setNonstockFormErrorMessage(updateNonstockRes.message); return; } - alert(updateNonstockRes?.message); + toast.success(updateNonstockRes?.message as string); router.refresh(); router.push('/master-data/nonstock'); }, @@ -68,6 +75,22 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const formikInitialValues = useMemo(() => { return { name: initialValues?.name ?? '', + uomId: initialValues?.uom_id ?? 0, + uom: initialValues?.uom + ? { + value: initialValues?.uom.id, + label: initialValues?.uom.name, + } + : null, + supplierIds: + initialValues?.suppliers.map((supplier) => supplier.id) ?? [], + suppliers: + initialValues?.suppliers.map((supplier) => ({ + value: supplier.id, + label: supplier.name, + })) ?? [], + + flags: initialValues?.flags ?? [], }; }, [initialValues]); @@ -80,6 +103,9 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { const nonstockPayload: CreateNonstockPayload = { name: values.name, + uom_id: values.uomId, + supplier_ids: values.supplierIds as number[], + flags: values.flags as flags[], }; switch (type) { @@ -97,81 +123,268 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { }, }); + const { setValues: formikSetValues } = formik; + + // UOM + const [uomSelectInputValue, setUomSelectInputValue] = useState(''); + + const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ + search: uomSelectInputValue ?? '', + }).toString()}`; + + const { data: uoms, isLoading: isLoadingUoms } = useSWR( + uomsUrl, + UomApi.getAllFetcher + ); + + const uomOptions = isResponseSuccess(uoms) + ? uoms?.data.map((uom) => ({ + value: uom.id, + label: uom.name, + })) + : []; + + const uomChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('uom', true); + formik.setFieldValue('uom', val); + + formik.setFieldTouched('uomId', true); + formik.setFieldValue('uomId', (val as OptionType)?.value); + }; + + // supplier + const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); + + const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ + search: supplierSelectInputValue ?? '', + }).toString()}`; + + const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( + suppliersUrl, + SupplierApi.getAllFetcher + ); + + const supplierOptions = isResponseSuccess(suppliers) + ? suppliers?.data + .filter((sup) => sup.category === 'BOP') + .map((supplier) => ({ + value: supplier.id, + label: supplier.name, + })) + : []; + + const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('suppliers', true); + formik.setFieldValue('suppliers', val); + + const supplierIds = (val as OptionType[]).map( + (supplier) => supplier.value as number + ); + + formik.setFieldTouched('supplierIds', true); + formik.setFieldValue('supplierIds', supplierIds); + }; + + const deleteNonstockClickHandler = () => { + deleteModal.openModal(); + }; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + + await NonstockApi.delete(initialValues?.id as number); + + deleteModal.closeModal(); + toast.success('Successfully delete Nonstock!'); + setIsDeleteLoading(false); + router.push('/master-data/nonstock'); + }; + + const flagsChangeHandler = (val: OptionType | OptionType[] | null) => { + const formattedFlags = (val as OptionType[]).map( + (flag) => flag.value as string + ); + + formik.setFieldValue('flags', formattedFlags); + }; + useEffect(() => { - formik.setValues(formikInitialValues); - }, [formik, formikInitialValues]); + formikSetValues(formikInitialValues); + }, [formikSetValues, formikInitialValues]); return ( -
-
- + +

+ {type === 'add' && 'Tambah Nonstock'} + {type === 'edit' && 'Edit Nonstock'} + {type === 'detail' && 'Detail Nonstock'} +

+
+ +
- - Kembali - +
+ -

- {type === 'add' && 'Tambah Non Stock'} - {type === 'edit' && 'Edit Non Stock'} - {type === 'detail' && 'Detail Non Stock'} -

- + - -
- -
+ - {type !== 'detail' && ( - <> -
- + + formik.values.flags?.includes(opt.value) + )} + onChange={flagsChangeHandler} + options={SUPPLIER_FLAG_OPTIONS} + isError={formik.touched.flags && Boolean(formik.errors.flags)} + errorMessage={formik.errors.flags as string} + isDisabled={type === 'detail'} + isClearable + /> +
- -
+
+ {type !== 'add' && ( +
+ - {nonstockFormErrorMessage && ( -
- - {nonstockFormErrorMessage} + {type !== 'edit' && ( + + )}
)} - - )} - -
+ + {type !== 'detail' && ( +
+ + + +
+ )} +
+ + {nonstockFormErrorMessage && ( +
+ + {nonstockFormErrorMessage} +
+ )} + + + + {type !== 'add' && ( + + )} + ); }; diff --git a/src/config/constant.ts b/src/config/constant.ts index 8a5ca62f..badd2c3d 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,13 +1,11 @@ -export type MenuItem = { +type MAIN_DRAWER_MENU = { title: string; link: string; icon: string; - submenu?: MenuItem[]; + submenu?: MAIN_DRAWER_MENU[]; }; -type MainDrawerLink = MenuItem; - -export const MAIN_DRAWER_LINKS: MainDrawerLink[] = [ +export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ { title: 'Dashboard', link: '/dashboard', @@ -71,7 +69,7 @@ export const MAIN_DRAWER_LINKS: MainDrawerLink[] = [ }, { title: 'FCR', - link: '/master-data/FCR', + link: '/master-data/fcr', icon: 'fluent:food-chicken-leg-16-regular', }, { @@ -128,3 +126,7 @@ export const PRODUCT_FLAG_OPTIONS = [ { label: 'VITAMIN', value: 'VITAMIN' }, { label: 'KIMIA', value: 'KIMIA' }, ]; + +export const SUPPLIER_FLAG_OPTIONS = [ + { label: 'EKSPEDISI', value: 'EKSPEDISI' }, +]; diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 7429b8ef..259bff8b 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -39,6 +39,21 @@ import { Supplier, UpdateSupplierPayload, } from '@/types/api/master-data/supplier'; +import { + CreateNonstockPayload, + Nonstock, + UpdateNonstockPayload, +} from '@/types/api/master-data/nonstock'; +import { + Bank, + CreateBankPayload, + UpdateBankPayload, +} from '@/types/api/master-data/bank'; +import { + CreateFcrPayload, + Fcr, + UpdateFcrPayload, +} from '@/types/api/master-data/fcr'; export const UomApi = new BaseApiService< Uom, @@ -86,4 +101,22 @@ export const SupplierApi = new BaseApiService< Supplier, CreateSupplierPayload, UpdateSupplierPayload ->('/master-data/suppliers'); \ No newline at end of file +>('/master-data/suppliers'); + +export const NonstockApi = new BaseApiService< + Nonstock, + CreateNonstockPayload, + UpdateNonstockPayload +>('/master-data/nonstocks'); + +export const BankApi = new BaseApiService< + Bank, + CreateBankPayload, + UpdateBankPayload +>('/master-data/banks'); + +export const FcrApi = new BaseApiService< + Fcr, + CreateFcrPayload, + UpdateFcrPayload +>('/master-data/fcrs'); diff --git a/src/services/api/master-data/nonstock.ts b/src/services/api/master-data/nonstock.ts deleted file mode 100644 index 7340e37b..00000000 --- a/src/services/api/master-data/nonstock.ts +++ /dev/null @@ -1,87 +0,0 @@ -import axios from 'axios'; -import { httpClient } from '@/services/http/client'; - -import { - CreateNonstockPayload, - DeleteNonstockResponse, - NonstockResponse, - UpdateNonstockPayload, -} from '@/types/api/master-data/nonstock'; - -export const getNonstock = async (nonstockId: number) => { - try { - const getNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}` - ); - - return getNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const createNonstock = async (payload: CreateNonstockPayload) => { - try { - const createNonstockRes = await httpClient( - '/master-data/nonstocks', - { - method: 'POST', - body: payload, - } - ); - - return createNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const updateNonstock = async ( - nonstockId: number, - payload: UpdateNonstockPayload -) => { - try { - const updateNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}`, - { - method: 'PATCH', - body: payload, - } - ); - - return updateNonstockRes; - } catch (error: unknown) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; - -export const deleteNonstock = async (nonstockId: number) => { - try { - const deleteNonstockRes = await httpClient( - `/master-data/nonstocks/${nonstockId}`, - { - method: 'DELETE', - } - ); - - return deleteNonstockRes; - } catch (error) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } - - return undefined; - } -}; diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index 8a4c4de7..6a3fc6be 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -53,3 +53,16 @@ export type BaseMetadata = { export type Override = Omit & Overrides; + +export type flags = + | 'PAKAN' + | 'OBAT' + | 'VITAMIN' + | 'KIMIA' + | 'EKSPEDISI' + | 'IS_ACTIVE' + | 'DOC' + | 'PRE-STARTER' + | 'STARTER' + | 'FINISHER' + | 'OVK'; diff --git a/src/types/api/master-data/bank.d.ts b/src/types/api/master-data/bank.d.ts new file mode 100644 index 00000000..0b23b446 --- /dev/null +++ b/src/types/api/master-data/bank.d.ts @@ -0,0 +1,20 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseBank = { + id: number; + name: string; + alias: string; + owner?: string; + account_number: string; +}; + +export type Bank = BaseMetadata & BaseBank; + +export type CreateBankPayload = { + name: string; + alias: string; + account_number: string; + owner?: string; +}; + +export type UpdateBankPayload = CreateBankPayload; diff --git a/src/types/api/master-data/fcr.d.ts b/src/types/api/master-data/fcr.d.ts new file mode 100644 index 00000000..45ad25e5 --- /dev/null +++ b/src/types/api/master-data/fcr.d.ts @@ -0,0 +1,30 @@ +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseFcr = { + id: number; + name: string; +}; + +export type FcrStandard = { + id: number; + weight: number; + fcr_number: number; + mortality: number; +}; + +export type Fcr = BaseMetadata & BaseFcr; + +export type FcrWithStandards = Fcr & { + fcr_standards: FcrStandard[]; +}; + +export type CreateFcrPayload = { + name: string; + fcr_standards: { + weight: number; + fcr_number: number; + mortality: number; + }[]; +}; + +export type UpdateFcrPayload = CreateFcrPayload; diff --git a/src/types/api/master-data/nonstock.d.ts b/src/types/api/master-data/nonstock.d.ts index 682f7852..e4e79d8e 100644 --- a/src/types/api/master-data/nonstock.d.ts +++ b/src/types/api/master-data/nonstock.d.ts @@ -1,18 +1,23 @@ -import { BaseApiResponse } from '@/types/api/api-general'; +import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general'; +import { BaseUom } from '@/types/api/master-data/uom'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; -export type Nonstock = { +export type BaseNonstock = { id: number; name: string; + uom_id: number; + uom: BaseUom; + suppliers: BaseSupplier[]; + flags: flags[]; }; +export type Nonstock = BaseMetadata & BaseNonstock; + export type CreateNonstockPayload = { name: string; + uom_id: number; + supplier_ids: number[]; + flags: flags[]; }; export type UpdateNonstockPayload = CreateNonstockPayload; - -export type NonstockResponse = BaseApiResponse; - -export type NonstocksResponse = BaseApiResponse; - -export type DeleteNonstockResponse = BaseApiResponse;