feat: implement return to url query param

This commit is contained in:
ValdiANS
2026-04-15 16:38:56 +07:00
parent 5e907d7e53
commit 7a5ee2aca1
5 changed files with 140 additions and 29 deletions
@@ -1,6 +1,7 @@
'use client';
import { useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
@@ -9,6 +10,7 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont
import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent';
import { Expense } from '@/types/api/expense';
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
interface ExpenseDetailProps {
initialValues?: Expense;
@@ -16,6 +18,8 @@ interface ExpenseDetailProps {
const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
const [activeTab, setActiveTab] = useState<string>('request');
const searchParams = useSearchParams();
const returnTo = getExpenseListReturnTo(searchParams);
const expenseDetailTabs = useMemo(() => {
const validTabs = [
@@ -46,7 +50,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
<section className='w-full max-w-full pb-16'>
<header className='flex flex-col gap-4'>
<Button
href='/expense'
href={returnTo}
variant='link'
className='w-fit p-0 text-primary'
>
@@ -1,5 +1,8 @@
'use client';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Icon } from '@iconify/react';
@@ -16,6 +19,7 @@ import {
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
import { ExpenseApi } from '@/services/api/expense';
import { isResponseSuccess } from '@/lib/api-helper';
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
interface ExpenseRealizationContentProps {
@@ -25,6 +29,8 @@ interface ExpenseRealizationContentProps {
const ExpenseRealizationContent = ({
initialValues,
}: ExpenseRealizationContentProps) => {
const searchParams = useSearchParams();
const formik = useFormik<UploadRequestDocumentsFormValues>({
initialValues: {
documents: [],
@@ -74,7 +80,11 @@ const ExpenseRealizationContent = ({
<Button
type='button'
color='warning'
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
href={buildExpenseActionHref(
'/expense/realization/edit/',
initialValues?.id as number,
searchParams
)}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
@@ -31,6 +31,10 @@ import { ExpenseApi } from '@/services/api/expense';
import { isResponseSuccess } from '@/lib/api-helper';
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
import { BaseApiResponse } from '@/types/api/api-general';
import {
buildExpenseActionHref,
getExpenseListReturnTo,
} from '@/lib/expense-list-navigation';
interface ExpenseRequestContentProps {
initialValues?: Expense;
@@ -40,6 +44,8 @@ const ExpenseRequestContent = ({
initialValues,
}: ExpenseRequestContentProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const returnTo = getExpenseListReturnTo(searchParams);
const { approvals: approvalHistory, isLoading: isLoadingApprovalHistory } =
useApprovalSteps({
@@ -148,7 +154,7 @@ const ExpenseRequestContent = ({
if (isResponseSuccess(deleteResponse)) {
toast.success('Berhasil menghapus data biaya operasional!');
router.push('/expense');
router.push(returnTo);
} else {
toast.error('Gagal menghapus data biaya operasional!');
}
@@ -164,7 +170,7 @@ const ExpenseRequestContent = ({
if (isResponseSuccess(completeRes)) {
toast.success(completeRes.message);
router.push('/expense');
router.push(returnTo);
} else {
toast.error(completeRes?.message as string);
}
@@ -204,7 +210,7 @@ const ExpenseRequestContent = ({
toast.success(approveResponse?.message);
setApprovalNotes('');
router.push('/expense');
router.push(returnTo);
} else {
approveModal.closeModal();
@@ -239,7 +245,7 @@ const ExpenseRequestContent = ({
toast.success(rejectResponse.message);
setApprovalNotes('');
router.push('/expense');
router.push(returnTo);
} else {
rejectModal.closeModal();
@@ -365,7 +371,11 @@ const ExpenseRequestContent = ({
<Button
variant='outline'
color='info'
href={`/expense/realization/?expenseId=${initialValues?.id}`}
href={buildExpenseActionHref(
'/expense/realization/',
initialValues?.id as number,
searchParams
)}
className='w-full sm:w-fit'
>
<Icon
@@ -384,7 +394,11 @@ const ExpenseRequestContent = ({
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
href={buildExpenseActionHref(
'/expense/detail/edit/',
initialValues?.id as number,
searchParams
)}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
+94 -14
View File
@@ -1,7 +1,13 @@
'use client';
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import { usePathname } from 'next/navigation';
import {
ChangeEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useUiStore } from '@/stores/ui/ui.store';
import useSWR from 'swr';
import {
@@ -31,19 +37,32 @@ import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTab
import { Expense } from '@/types/api/expense';
import { ExpenseApi } from '@/services/api/expense';
import { buildExpenseActionHref } from '@/lib/expense-list-navigation';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { BaseApiResponse } from '@/types/api/api-general';
type ExpenseTableFilters = {
search: string;
nameSort: string;
transactionDate: string;
realizationDate: string;
locationId: string;
vendorId: string;
userId: string;
};
const RowOptionsMenu = ({
popoverPosition = 'bottom',
props,
deleteClickHandler,
returnToSearchParams,
}: {
popoverPosition: 'bottom' | 'top';
props: CellContext<Expense, unknown>;
deleteClickHandler: () => void;
returnToSearchParams: URLSearchParams;
}) => {
const popoverId = `expense#${props.row.original.id}`;
const popoverAnchorName = `--anchor-expense#${props.row.original.id}`;
@@ -86,7 +105,11 @@ const RowOptionsMenu = ({
<div className='flex flex-col bg-base-100 rounded-xl'>
<RequirePermission permissions='lti.expense.detail'>
<Button
href={`/expense/detail/?expenseId=${props.row.original.id}`}
href={buildExpenseActionHref(
'/expense/detail/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
@@ -100,7 +123,11 @@ const RowOptionsMenu = ({
{showEditButton && (
<RequirePermission permissions='lti.expense.update'>
<Button
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
href={buildExpenseActionHref(
'/expense/detail/edit/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
@@ -115,7 +142,11 @@ const RowOptionsMenu = ({
{showRealizationButton && (
<RequirePermission permissions='lti.expense.create.realization'>
<Button
href={`/expense/realization/?expenseId=${props.row.original.id}`}
href={buildExpenseActionHref(
'/expense/realization/',
props.row.original.id,
returnToSearchParams
)}
variant='ghost'
color='none'
className='p-3 justify-start text-sm font-semibold w-full'
@@ -155,6 +186,8 @@ const RowOptionsMenu = ({
const ExpensesTable = () => {
const { searchValue, setSearchValue, setTableState } = useUiStore();
const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
const {
state: tableFilterState,
@@ -162,9 +195,11 @@ const ExpensesTable = () => {
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
} = useTableFilter<ExpenseTableFilters>({
initial: {
search: '',
page: Number(searchParams.get('page')) || 1,
pageSize: Number(searchParams.get('limit')) || 10,
search: searchValue,
nameSort: '',
transactionDate: '',
realizationDate: '',
@@ -193,6 +228,54 @@ const ExpensesTable = () => {
ExpenseApi.getAllFetcher
);
const syncPaginationToUrl = useCallback(
(page: number, pageSize: number) => {
const nextQueryString = new URLSearchParams({
page: String(page),
limit: String(pageSize),
}).toString();
router.replace(
nextQueryString ? `${pathname}?${nextQueryString}` : pathname,
{
scroll: false,
}
);
},
[pathname, router]
);
const pageChangeHandler = useCallback(
(page: number) => {
setPage(page);
syncPaginationToUrl(page, tableFilterState.pageSize);
},
[setPage, syncPaginationToUrl, tableFilterState.pageSize]
);
const pageSizeChangeHandler = useCallback(
(pageSize: number) => {
setPageSize(pageSize);
syncPaginationToUrl(1, pageSize);
},
[setPageSize, syncPaginationToUrl]
);
const returnToSearchParams = useMemo(() => {
const returnToParams = new URLSearchParams();
const queryString = new URLSearchParams({
page: String(tableFilterState.page),
limit: String(tableFilterState.pageSize),
}).toString();
returnToParams.set(
'returnTo',
queryString ? `${pathname}?${queryString}` : pathname
);
return returnToParams;
}, [pathname, tableFilterState.page, tableFilterState.pageSize]);
const deleteModal = useModal();
const approveModal = useModal();
const rejectModal = useModal();
@@ -373,6 +456,7 @@ const ExpensesTable = () => {
popoverPosition={isLast2Rows ? 'top' : 'bottom'}
props={props}
deleteClickHandler={deleteClickHandler}
returnToSearchParams={returnToSearchParams}
/>
);
},
@@ -512,10 +596,6 @@ const ExpensesTable = () => {
setIsRejectLoading(false);
};
useEffect(() => {
updateFilter('search', searchValue);
}, [searchValue, updateFilter]);
useEffect(() => {
setTableState('expense-table', pathname);
}, [pathname, setTableState]);
@@ -554,7 +634,7 @@ const ExpensesTable = () => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
updateFilter('nameSort', '', false);
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
@@ -734,8 +814,8 @@ const ExpensesTable = () => {
totalItems={
isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0
}
onPageChange={setPage}
onPageSizeChange={setPageSize}
onPageChange={pageChangeHandler}
onPageSizeChange={pageSizeChangeHandler}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
@@ -1,7 +1,7 @@
'use client';
import { useCallback, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';
@@ -35,6 +35,7 @@ import { isResponseError } from '@/lib/api-helper';
import { LocationApi, SupplierApi } from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
import { getExpenseListReturnTo } from '@/lib/expense-list-navigation';
import { cn } from '@/lib/helper';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
@@ -48,6 +49,8 @@ const ExpenseRealizationForm = ({
initialValues,
}: ExpenseRealizationFormProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const returnTo = getExpenseListReturnTo(searchParams);
const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState('');
@@ -64,9 +67,9 @@ const ExpenseRealizationForm = ({
}
toast.success(createExpenseRes?.message as string);
router.push('/expense');
router.push(returnTo);
},
[router]
[initialValues?.id, returnTo, router]
);
const updateExpenseHandler = useCallback(
@@ -83,9 +86,9 @@ const ExpenseRealizationForm = ({
toast.success(updateExpenseRes?.message as string);
router.refresh();
router.push('/expense');
router.push(returnTo);
},
[router]
[returnTo, router]
);
const formik = useFormik<ExpenseRealizationFormValues>({
@@ -258,7 +261,7 @@ const ExpenseRealizationForm = ({
<section className='w-full'>
<header className='flex flex-col gap-4'>
<Button
href='/expense'
href={returnTo}
variant='link'
className='w-fit p-0 text-primary'
>