From 3e7da624aa3d045b1a76e4740dceb4bc0ee6287a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 13:47:56 +0700 Subject: [PATCH 01/33] feat: add .prettierrc.json config --- .prettierrc.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .prettierrc.json 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" +} From 8461667ca216f7d1d23ab8878912966b046db214 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 14:56:52 +0700 Subject: [PATCH 02/33] chore(FE-41): delete nonstock api helper function file --- src/services/api/master-data/nonstock.ts | 87 ------------------------ 1 file changed, 87 deletions(-) delete mode 100644 src/services/api/master-data/nonstock.ts 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; - } -}; From 0e49e29002c4947ffb4edef52438d949219e7124 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 14:58:21 +0700 Subject: [PATCH 03/33] feat(FE-42): create SUPPLIER_FLAG_OPTIONS constant --- src/config/constant.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 1fbef81f..ab58df5b 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -120,3 +120,6 @@ export const PRODUCT_FLAG_OPTIONS = [ { label: 'KIMIA', value: 'KIMIA' }, ]; +export const SUPPLIER_FLAG_OPTIONS = [ + { label: 'EKSPEDISI', value: 'EKSPEDISI' }, +]; From 143d640a1e029e097f66ad83fe6a6dc47034aaaf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 14:58:54 +0700 Subject: [PATCH 04/33] chore(FE-41): refactor nonstock type --- src/types/api/master-data/nonstock.d.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) 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; From f3d0e12bcded069380bcdc34c59f17b8b73defdf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 14:59:14 +0700 Subject: [PATCH 05/33] feat(FE-42): create flags type --- src/types/api/api-general.d.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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'; From d24d50474d1ab4590e7ce7607091ad7cc0b23a1e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 14:59:39 +0700 Subject: [PATCH 06/33] feat(FE-41): create Nonstock API service --- src/services/api/master-data.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 7429b8ef..41975bd8 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -39,6 +39,11 @@ import { Supplier, UpdateSupplierPayload, } from '@/types/api/master-data/supplier'; +import { + CreateNonstockPayload, + Nonstock, + UpdateNonstockPayload, +} from '@/types/api/master-data/nonstock'; export const UomApi = new BaseApiService< Uom, @@ -86,4 +91,10 @@ 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'); From 96fea80f6258c38b149c83d2d49c593d88156b4b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:00:06 +0700 Subject: [PATCH 07/33] feat(FE-40,41): create NonstockForm component --- .../nonstock/form/NonstockForm.tsx | 361 ++++++++++++++---- 1 file changed, 287 insertions(+), 74 deletions(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 33dcba54..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); - }, [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' && ( + + )} + ); }; From c53f91ec3f282469a869b4d1557d3e1943da3cac Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:00:30 +0700 Subject: [PATCH 08/33] feat(FE-43): create NonstocksTable component --- .../master-data/nonstock/NonstocksTable.tsx | 330 +++++++++++------- 1 file changed, 208 insertions(+), 122 deletions(-) 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', + -
+ ); }; From b8548b72c95f668eadc64f16bb80571486e1e7b8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:00:41 +0700 Subject: [PATCH 09/33] feat(FE-40,41): create Nonstock form validation schema --- .../nonstock/form/NonstockForm.schema.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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; From 780c0bb9d0d0d341be8aa4227ad05250f565e3db Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:01:45 +0700 Subject: [PATCH 10/33] chore(FE-41): use Nonstock API service --- src/app/master-data/nonstock/detail/edit/page.tsx | 8 ++++---- src/app/master-data/nonstock/detail/page.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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; } From 293f457ecbdfbacba08b724e42b20a2c3eb160b1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:35:00 +0700 Subject: [PATCH 11/33] feat(FE-41): create Bank type --- src/types/api/master-data/bank.d.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/types/api/master-data/bank.d.ts 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; From 10749f06da103124d5b5df6a5f417c612696e6fb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:35:15 +0700 Subject: [PATCH 12/33] feat(FE-41): create Bank API service --- src/services/api/master-data.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 41975bd8..8a57e6e6 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -44,6 +44,11 @@ import { Nonstock, UpdateNonstockPayload, } from '@/types/api/master-data/nonstock'; +import { + Bank, + CreateBankPayload, + UpdateBankPayload, +} from '@/types/api/master-data/bank'; export const UomApi = new BaseApiService< Uom, @@ -98,3 +103,9 @@ export const NonstockApi = new BaseApiService< CreateNonstockPayload, UpdateNonstockPayload >('/master-data/nonstocks'); + +export const BankApi = new BaseApiService< + Bank, + CreateBankPayload, + UpdateBankPayload +>('/master-data/banks'); From 8c507aa410f494436daab22fb7e7bac306ff6ac1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:35:34 +0700 Subject: [PATCH 13/33] feat(FE-40,41): create BankForm component --- .../pages/master-data/bank/form/BankForm.tsx | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 src/components/pages/master-data/bank/form/BankForm.tsx 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; From 16a15fce66c6369099c1d4a4bc6be7976b496de1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:35:56 +0700 Subject: [PATCH 14/33] feat(FE-42): create Bank form validation schema --- .../pages/master-data/bank/form/BankForm.schema.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/components/pages/master-data/bank/form/BankForm.schema.ts 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; From ca42570a4086e349b21a50e0a4b991d08d2e11f1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:36:13 +0700 Subject: [PATCH 15/33] feat(FE-43): create BanksTable component --- .../pages/master-data/bank/BanksTable.tsx | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 src/components/pages/master-data/bank/BanksTable.tsx 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; From b9015ed673f4c160a5dd2cafd485d9dcc212134f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:37:01 +0700 Subject: [PATCH 16/33] feat(FE-43): create Master Data Bank page --- src/app/master-data/bank/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/master-data/bank/page.tsx 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; From 0d5e8383fda172d7d3852501d8d2c2db952fc68a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:37:26 +0700 Subject: [PATCH 17/33] feat(FE-40,41): create Master Data Add Bank page --- src/app/master-data/bank/add/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/master-data/bank/add/page.tsx 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; From 372f1698ca45c2f123d613836ec91e2e165c3440 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:38:01 +0700 Subject: [PATCH 18/33] feat(FE-40,41): create Master Data Detail Bank page --- src/app/master-data/bank/detail/page.tsx | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/app/master-data/bank/detail/page.tsx 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; From 1d7f10050756a7bfee025a8cc7430a5eff21f7b8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Oct 2025 15:38:10 +0700 Subject: [PATCH 19/33] feat(FE-40,41): create Master Data Edit Bank page --- src/app/master-data/bank/detail/edit/page.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/app/master-data/bank/detail/edit/page.tsx 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; From f8f5e8403a8edcb9e7b9c0696fea3c1e31e27217 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 09:55:14 +0700 Subject: [PATCH 20/33] chore(FE-40): create main drawer menu type --- src/config/constant.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index ab58df5b..badd2c3d 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,4 +1,11 @@ -export const MAIN_DRAWER_LINKS = [ +type MAIN_DRAWER_MENU = { + title: string; + link: string; + icon: string; + submenu?: MAIN_DRAWER_MENU[]; +}; + +export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ { title: 'Dashboard', link: '/dashboard', @@ -62,7 +69,7 @@ export const MAIN_DRAWER_LINKS = [ }, { title: 'FCR', - link: '/master-data/FCR', + link: '/master-data/fcr', icon: 'fluent:food-chicken-leg-16-regular', }, { From a6be56e6f23caea6d68be5c96c971fa0e5472e2b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 09:57:41 +0700 Subject: [PATCH 21/33] feat(FE-41): create Fcr api type --- src/types/api/master-data/fcr.d.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/types/api/master-data/fcr.d.ts 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; From 24269d8c76ccfce71c98c34eaf76579e22b9db16 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 09:59:34 +0700 Subject: [PATCH 22/33] feat(FE-41): create Fcr api service --- src/services/api/master-data.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index 8a57e6e6..259bff8b 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -49,6 +49,11 @@ import { CreateBankPayload, UpdateBankPayload, } from '@/types/api/master-data/bank'; +import { + CreateFcrPayload, + Fcr, + UpdateFcrPayload, +} from '@/types/api/master-data/fcr'; export const UomApi = new BaseApiService< Uom, @@ -109,3 +114,9 @@ export const BankApi = new BaseApiService< CreateBankPayload, UpdateBankPayload >('/master-data/banks'); + +export const FcrApi = new BaseApiService< + Fcr, + CreateFcrPayload, + UpdateFcrPayload +>('/master-data/fcrs'); From 95556bfdd7a4cc64eb0d2fb203ab611640442537 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 09:59:53 +0700 Subject: [PATCH 23/33] feat(FE-40,41): create FcrForm component --- .../pages/master-data/fcr/form/FcrForm.tsx | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 src/components/pages/master-data/fcr/form/FcrForm.tsx 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; From 5c0da471aeabbb73fa79a0f8020f1c92be409c6c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:00:29 +0700 Subject: [PATCH 24/33] feat(FE-42): create Fcr form validation schema --- .../master-data/fcr/form/FcrForm.schema.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/components/pages/master-data/fcr/form/FcrForm.schema.ts 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; From 764dacc627a1ac9bef9ff1b65972b12cddf6ce40 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:04:01 +0700 Subject: [PATCH 25/33] feat(FE-43): create FcrsTable component --- .../pages/master-data/fcr/FcrsTable.tsx | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 src/components/pages/master-data/fcr/FcrsTable.tsx 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; From 9b13ce2be617fe3c61ad22d29cffe0cd99fb40a9 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:04:20 +0700 Subject: [PATCH 26/33] chore(FE-40): render error message if isError and errorMessage exist --- src/components/input/TextInput.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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}

+ )}
); }; From da91201dde43255b2abd7abc8e99714c45d60b63 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:04:42 +0700 Subject: [PATCH 27/33] chore(FE-40): use optional chaining --- src/components/MainDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 6a5e6f38..309cddf2 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -199,7 +199,7 @@ const MainDrawer = ({ if (!hasSubmenu) return; - const activeSubmenu = menu.submenu.find((item) => + const activeSubmenu = menu.submenu?.find((item) => isPathActive(pathname, item.link) ); From d771b2095692aab5f82dddea2cc4c70499a8c0cc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:04:55 +0700 Subject: [PATCH 28/33] feat(FE-43): create Master Data FCR page --- src/app/master-data/fcr/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/master-data/fcr/page.tsx 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; From f6163b1f69a7b29515718d9615b00cc9a15edd0a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:05:07 +0700 Subject: [PATCH 29/33] feat(FE-40,41): create Master Data Add FCR page --- src/app/master-data/fcr/add/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/master-data/fcr/add/page.tsx 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; From 527a155997c38f32717986f5765d9a40e110028f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:05:17 +0700 Subject: [PATCH 30/33] feat(FE-40,41): create Master Data Detail FCR page --- src/app/master-data/fcr/detail/page.tsx | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/app/master-data/fcr/detail/page.tsx 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; From 6353b3aee47e85fa488472eff0634a8217a60446 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 10:05:24 +0700 Subject: [PATCH 31/33] feat(FE-40,41): create Master Data Edit FCR page --- src/app/master-data/fcr/detail/edit/page.tsx | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/app/master-data/fcr/detail/edit/page.tsx 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; From 9b56308cf04a225542a232c8c42cd9bcf8ddaa2c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 11:05:30 +0700 Subject: [PATCH 32/33] feat(FE-40): create SuspenseHelper component --- src/components/helper/SuspenseHelper.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/components/helper/SuspenseHelper.tsx diff --git a/src/components/helper/SuspenseHelper.tsx b/src/components/helper/SuspenseHelper.tsx new file mode 100644 index 00000000..a151cd9d --- /dev/null +++ b/src/components/helper/SuspenseHelper.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { Suspense } from 'react'; + +const SuspenseHelper = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return ( + + +
+ } + > + {children} + + ); +}; + +export default SuspenseHelper; From 94a5ce5604543c7c0554ad7d852b4cd00d4ad3d5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Oct 2025 11:05:55 +0700 Subject: [PATCH 33/33] feat(FE-40): create layout for /detail route and wrap the children in SuspenseHelper component --- src/app/master-data/area/detail/layout.tsx | 11 +++++++++++ src/app/master-data/bank/detail/layout.tsx | 11 +++++++++++ src/app/master-data/fcr/detail/layout.tsx | 11 +++++++++++ src/app/master-data/kandang/detail/layout.tsx | 11 +++++++++++ src/app/master-data/location/detail/layout.tsx | 11 +++++++++++ src/app/master-data/nonstock/detail/layout.tsx | 11 +++++++++++ .../master-data/product-category/detail/layout.tsx | 11 +++++++++++ src/app/master-data/product/detail/layout.tsx | 11 +++++++++++ src/app/master-data/uom/detail/layout.tsx | 11 +++++++++++ src/app/master-data/warehouse/detail/layout.tsx | 11 +++++++++++ 10 files changed, 110 insertions(+) create mode 100644 src/app/master-data/area/detail/layout.tsx create mode 100644 src/app/master-data/bank/detail/layout.tsx create mode 100644 src/app/master-data/fcr/detail/layout.tsx create mode 100644 src/app/master-data/kandang/detail/layout.tsx create mode 100644 src/app/master-data/location/detail/layout.tsx create mode 100644 src/app/master-data/nonstock/detail/layout.tsx create mode 100644 src/app/master-data/product-category/detail/layout.tsx create mode 100644 src/app/master-data/product/detail/layout.tsx create mode 100644 src/app/master-data/uom/detail/layout.tsx create mode 100644 src/app/master-data/warehouse/detail/layout.tsx diff --git a/src/app/master-data/area/detail/layout.tsx b/src/app/master-data/area/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/area/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/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/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/kandang/detail/layout.tsx b/src/app/master-data/kandang/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/kandang/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/location/detail/layout.tsx b/src/app/master-data/location/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/location/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/nonstock/detail/layout.tsx b/src/app/master-data/nonstock/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/nonstock/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/product-category/detail/layout.tsx b/src/app/master-data/product-category/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/product-category/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/product/detail/layout.tsx b/src/app/master-data/product/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/product/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/uom/detail/layout.tsx b/src/app/master-data/uom/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/uom/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/warehouse/detail/layout.tsx b/src/app/master-data/warehouse/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/master-data/warehouse/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;