From b5a0614218bb8f7390e4a2aacb697976b1dd1685 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 22 May 2026 11:13:52 +0700 Subject: [PATCH 1/2] feat: implement url query param tab navigation --- src/app/report/expense/layout.tsx | 11 ++++++++ .../report/expense/ReportExpenseTabs.tsx | 26 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/app/report/expense/layout.tsx diff --git a/src/app/report/expense/layout.tsx b/src/app/report/expense/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/expense/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/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx index 045e7a99..f700a0bb 100644 --- a/src/components/pages/report/expense/ReportExpenseTabs.tsx +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -1,26 +1,38 @@ 'use client'; -import { useState } from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import Tabs from '@/components/Tabs'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import ReportExpenseTab from '@/components/pages/report/expense/tab/ReportExpenseTab'; import ReportDepreciationTab from '@/components/pages/report/expense/tab/ReportDepreciationTab'; +const VALID_TAB_IDS = ['operational-expense', 'depreciation']; + const ReportExpenseTabs = () => { - const [activeTabId, setActiveTabId] = useState('1'); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const tabParam = searchParams.get('tab') ?? 'operational-expense'; + const activeTabId = VALID_TAB_IDS.includes(tabParam) + ? tabParam + : 'operational-expense'; const tabActions = useTabActionsStore((state) => state.tabActions); + const handleTabChange = (tabId: string) => { + router.push(`${pathname}?tab=${tabId}`); + }; + const tabs = [ { - id: '1', + id: 'operational-expense', label: 'Laporan Biaya Operasional', - content: , + content: , }, { - id: '2', + id: 'depreciation', label: 'Laporan Depresiasi', - content: , + content: , }, ]; @@ -30,7 +42,7 @@ const ReportExpenseTabs = () => { tabs={tabs} variant='boxed' activeTabId={activeTabId} - onTabChange={setActiveTabId} + onTabChange={handleTabChange} className={{ tabHeaderWrapper: 'justify-between items-center p-3 border-b border-base-content/10', From 05138dbb6f1669e5b2f3ebbb70af85f244a38e82 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 22 May 2026 11:14:16 +0700 Subject: [PATCH 2/2] feat: implement table filter state persist --- .../tab/ReportDepreciationFilterModal.tsx | 137 +++++++----------- .../expense/tab/ReportDepreciationTab.tsx | 33 +++-- 2 files changed, 70 insertions(+), 100 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx index 4c4a814e..a334e137 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useEffect, useMemo, useState } from 'react'; +import { RefObject } from 'react'; import { useFormik } from 'formik'; import * as yup from 'yup'; @@ -20,32 +20,34 @@ import { Location } from '@/types/api/master-data/location'; import { ProjectFlock } from '@/types/api/production/project-flock'; export type ReportDepreciationFilterValues = { - area_id: string | null; - location_id: string | null; - project_flock_id: string | null; + area?: OptionType; + location?: OptionType; + projectFlock?: OptionType; period: string | null; }; export const ReportDepreciationFilterSchema = yup.object({ - area_id: yup.string().nullable(), - location_id: yup.string().nullable(), - project_flock_id: yup.string().nullable(), + area: yup.mixed>().optional(), + location: yup.mixed>().optional(), + projectFlock: yup.mixed>().optional(), period: yup.string().nullable().required('Periode wajib dipilih'), -}) as yup.ObjectSchema; +}); interface ReportDepreciationFilterModalProps { ref: RefObject; - initialValues?: ReportDepreciationFilterValues; + initialValues?: Partial; onSubmit?: (values: Partial) => void; onReset?: () => void; } -const defaultInitialValues: ReportDepreciationFilterValues = { - area_id: null, - location_id: null, - project_flock_id: null, - period: null, -}; +const defaultInitialValues: ( + initialValues?: Partial +) => ReportDepreciationFilterValues = (initialValues) => ({ + area: undefined, + location: undefined, + projectFlock: undefined, + period: initialValues?.period ?? null, +}); const ReportDepreciationFilterModal = ({ ref, @@ -53,22 +55,19 @@ const ReportDepreciationFilterModal = ({ onSubmit, onReset, }: ReportDepreciationFilterModalProps) => { - const [selectedAreaId, setSelectedAreaId] = useState( - initialValues?.area_id || undefined - ); - const [selectedLocationId, setSelectedLocationId] = useState< - string | undefined - >(initialValues?.location_id || undefined); - - useEffect(() => { - setSelectedAreaId(initialValues?.area_id || undefined); - setSelectedLocationId(initialValues?.location_id || undefined); - }, [initialValues?.area_id, initialValues?.location_id]); - const closeModalHandler = () => { ref.current?.close(); }; + const formik = useFormik({ + initialValues: { ...defaultInitialValues(initialValues), ...initialValues }, + validationSchema: ReportDepreciationFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + }); + const { setInputValue: setAreaInputValue, options: areaOptions, @@ -82,7 +81,7 @@ const ReportDepreciationFilterModal = ({ isLoadingOptions: isLoadingLocationOptions, loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - area_id: selectedAreaId || '', + area_id: String(formik.values.area?.value ?? ''), }); const { @@ -96,73 +95,35 @@ const ReportDepreciationFilterModal = ({ 'flock_name', 'search', { - location_id: selectedLocationId || '', + location_id: String(formik.values.location?.value ?? ''), } ); - const formik = useFormik({ - initialValues: initialValues || defaultInitialValues, - enableReinitialize: true, - validationSchema: ReportDepreciationFilterSchema, - onSubmit: async (values) => { - onSubmit?.(values); - closeModalHandler(); - }, - onReset: (_) => { - onReset?.(); - closeModalHandler(); - }, - }); - - const areaValue = useMemo(() => { - if (!formik.values.area_id) return null; - return ( - areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || - null - ); - }, [formik.values.area_id, areaOptions]); - - const locationValue = useMemo(() => { - if (!formik.values.location_id) return null; - return ( - locationOptions.find( - (opt) => String(opt.value) === formik.values.location_id - ) || null - ); - }, [formik.values.location_id, locationOptions]); - - const projectFlockValue = useMemo(() => { - if (!formik.values.project_flock_id) return null; - return ( - projectFlockOptions.find( - (opt) => String(opt.value) === formik.values.project_flock_id - ) || null - ); - }, [formik.values.project_flock_id, projectFlockOptions]); + const formikResetHandler = () => { + onReset?.(); + formik.resetForm({ values: defaultInitialValues(initialValues) }); + closeModalHandler(); + }; const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - const areaId = val && !Array.isArray(val) ? String(val.value) : null; - - setSelectedAreaId(areaId || undefined); - formik.setFieldValue('area_id', areaId); - formik.setFieldValue('location_id', null); - formik.setFieldValue('project_flock_id', null); - setSelectedLocationId(undefined); + const area = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('area', area); + formik.setFieldValue('location', undefined); + formik.setFieldValue('projectFlock', undefined); }; const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const locationId = val && !Array.isArray(val) ? String(val.value) : null; - - setSelectedLocationId(locationId || undefined); - formik.setFieldValue('location_id', locationId); - formik.setFieldValue('project_flock_id', null); + const location = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('location', location); + formik.setFieldValue('projectFlock', undefined); }; const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - const projectFlockId = - val && !Array.isArray(val) ? String(val.value) : null; - - formik.setFieldValue('project_flock_id', projectFlockId); + const projectFlock = + val && !Array.isArray(val) ? (val as OptionType) : undefined; + formik.setFieldValue('projectFlock', projectFlock); }; return ( @@ -174,7 +135,7 @@ const ReportDepreciationFilterModal = ({ >
@@ -199,7 +160,7 @@ const ReportDepreciationFilterModal = ({ label='Area' placeholder='Pilih Area' options={areaOptions} - value={areaValue} + value={formik.values.area ?? null} onChange={areaChangeHandler} onInputChange={setAreaInputValue} onMenuScrollToBottom={loadMoreAreas} @@ -213,7 +174,7 @@ const ReportDepreciationFilterModal = ({ label='Lokasi' placeholder='Pilih Lokasi' options={locationOptions} - value={locationValue} + value={formik.values.location ?? null} onChange={locationChangeHandler} onInputChange={setLocationInputValue} onMenuScrollToBottom={loadMoreLocations} @@ -227,7 +188,7 @@ const ReportDepreciationFilterModal = ({ label='Project Flock' placeholder='Pilih Project Flock' options={projectFlockOptions} - value={projectFlockValue} + value={formik.values.projectFlock ?? null} onChange={projectFlockChangeHandler} onInputChange={setProjectFlockInputValue} onMenuScrollToBottom={loadMoreProjectFlocks} diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx index 41c2f9e8..abab3df7 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -17,6 +17,7 @@ import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import { ReportDepreciation } from '@/types/api/report/report-expense'; import { DepreciationReportApi } from '@/services/api/report/expense-report'; import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { OptionType } from '@/components/input/SelectInput'; import { isResponseSuccess } from '@/lib/api-helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; @@ -32,20 +33,27 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { setPageSize, toQueryString: getTableFilterQueryString, reset: resetFilter, - } = useTableFilter({ + } = useTableFilter<{ + area?: OptionType; + location?: OptionType; + projectFlock?: OptionType; + period: string; + }>({ initial: { - area_id: '', - location_id: '', - project_flock_id: '', + area: undefined, + location: undefined, + projectFlock: undefined, period: formatDate(Date.now(), 'YYYY-MM-DD'), }, paramMap: { pageSize: 'limit', - area_id: 'area_id', - location_id: 'location_id', - project_flock_id: 'project_flock_id', + area: 'area_id', + location: 'location_id', + projectFlock: 'project_flock_id', period: 'period', }, + persist: true, + storeName: 'report-depreciation-table', }); const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = @@ -109,7 +117,7 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { filterModal.openModal()} + onClick={filterModal.openModal} variant='outline' className='px-3 py-2.5' /> @@ -239,12 +247,13 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { initialValues={tableFilterState} onReset={resetFilter} onSubmit={(values) => { - updateFilter('area_id', values.area_id ?? ''); - updateFilter('location_id', values.location_id ?? ''); - updateFilter('project_flock_id', values.project_flock_id ?? ''); + updateFilter('area', values.area, true); + updateFilter('location', values.location, true); + updateFilter('projectFlock', values.projectFlock, true); updateFilter( 'period', - values.period ? formatDate(values.period, 'YYYY-MM-DD') : '' + values.period ? formatDate(values.period, 'YYYY-MM-DD') : '', + true ); }} />