feat(tables): integrate EmptyListWrapper component for improved empty state handling

- Add EmptyListWrapper to various tables (Admins, Companies, Customers, Feedback, Notification History, Tenders) to provide a consistent empty state message when no data is available.
- Update phone number and status display logic in the Profile page to handle null values gracefully.
- Introduce worktree setup configuration in worktrees.json for easier project setup.
This commit is contained in:
AmirReza Jamali
2026-02-10 18:19:49 +03:30
parent 7755384d51
commit da82399d07
13 changed files with 240 additions and 202 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"setup-worktree": [
"npm install"
]
}
+2 -2
View File
@@ -62,7 +62,7 @@ export default function Page() {
<div> <div>
<p className="font-medium text-dark dark:text-white">Phone</p> <p className="font-medium text-dark dark:text-white">Phone</p>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300">
{formatPhoneNumber(data?.phone)} {data?.phone ? formatPhoneNumber(data?.phone) : "-"}
</p> </p>
</div> </div>
<div> <div>
@@ -84,7 +84,7 @@ export default function Page() {
<div> <div>
<p className="font-medium text-dark dark:text-white">Status</p> <p className="font-medium text-dark dark:text-white">Status</p>
<div className="w-fit"> <div className="w-fit">
<Status status={data?.status}>{data?.status}</Status> <Status status={data?.status ?? ""}>{data?.status ?? "-"}</Status>
</div> </div>
</div> </div>
</div> </div>
+9 -12
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader"; import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
@@ -65,15 +66,12 @@ const AdminsTable = () => {
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={columns.length} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{!allUsers.length ? ( <EmptyListWrapper
<TableRow> list={allUsers}
<TableCell> columns={columns}
<h2>No user</h2> emptyMessage="No admins were found"
</TableCell> >
</TableRow> {allUsers.map((item, index) => (
) : (
allUsers.map((item, index) => {
return (
<TableRow <TableRow
key={item?.id || index} key={item?.id || index}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -141,9 +139,8 @@ const AdminsTable = () => {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
); ))}
}) </EmptyListWrapper>
)}
</TableBody> </TableBody>
</Table> </Table>
<Modal <Modal
@@ -40,7 +40,7 @@ export const useAdminsPresenter = () => {
} = useForm<TChangeAdminStatusCredentials>({ } = useForm<TChangeAdminStatusCredentials>({
mode: "onChange", mode: "onChange",
defaultValues: { defaultValues: {
status: currentUser?.status, status: currentUser?.status ?? "",
}, },
}); });
const [params, setParams] = useState<Record<string, any>>({}); const [params, setParams] = useState<Record<string, any>>({});
@@ -1,6 +1,7 @@
"use client"; "use client";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons"; import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import { Switch } from "@/components/FormElements/switch"; import { Switch } from "@/components/FormElements/switch";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible"; import IsVisible from "@/components/ui/IsVisible";
import { import {
@@ -44,6 +45,15 @@ const CompanyCategoriesTable = () => {
isMutating, isMutating,
} = useCompanyCategoriesPresenter(); } = useCompanyCategoriesPresenter();
const columns = [
"Row",
"Title",
"Description",
"Created at",
"Published",
"Actions",
];
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader <ListHeader
@@ -61,19 +71,18 @@ const CompanyCategoriesTable = () => {
<TableHead colSpan={100}>Actions</TableHead> <TableHead colSpan={100}>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
{isLoading && <TableSkeleton column={6} />} {isLoading && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
<IsVisible condition={!categories}> <EmptyListWrapper
<TableRow> list={categories ?? []}
<TableCell colSpan={400} className="text-center"> columns={columns}
<h6>No category found</h6> emptyMessage="No categories were found"
</TableCell> >
</TableRow> {(categories ?? []).map((category, index) => (
</IsVisible> <TableRow
<IsVisible condition={!!categories}> key={category.id}
{categories?.map((category, index) => { className="odd:bg-gray-2 dark:odd:bg-gray-7"
return ( >
<TableRow key={category.id}>
<TableCell colSpan={100}>{index + 1}</TableCell> <TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{category.name}</TableCell> <TableCell colSpan={100}>{category.name}</TableCell>
<TableCell colSpan={100}>{category.description}</TableCell> <TableCell colSpan={100}>{category.description}</TableCell>
@@ -129,9 +138,8 @@ const CompanyCategoriesTable = () => {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
); ))}
})} </EmptyListWrapper>
</IsVisible>
</TableBody> </TableBody>
</Table> </Table>
<Modal <Modal
+12 -6
View File
@@ -5,6 +5,7 @@ import {
PencilSquareIcon, PencilSquareIcon,
TrashIcon, TrashIcon,
} from "@/assets/icons"; } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import { import {
Table, Table,
@@ -44,6 +45,9 @@ const CompaniesTable = () => {
setFilterValue, setFilterValue,
isMutating, isMutating,
} = useCompanyListPresenter(); } = useCompanyListPresenter();
const companies = allCompanies.filter(
(c): c is NonNullable<typeof c> => c != null,
);
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
<ListHeader <ListHeader
@@ -66,10 +70,12 @@ const CompaniesTable = () => {
</TableHeader> </TableHeader>
{isPending && <TableSkeleton column={columns.length} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allCompanies.map((company, index) => <EmptyListWrapper
!company ? ( list={companies}
<h6 key={index}>No Data was Found</h6> columns={columns}
) : ( emptyMessage="No companies were found"
>
{companies.map((company, index) => (
<TableRow <TableRow
key={company.id} key={company.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -156,8 +162,8 @@ const CompaniesTable = () => {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
), ))}
)} </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
<Modal <Modal
+14
View File
@@ -5,6 +5,7 @@ import { Building } from "@/components/Layouts/sidebar/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal"; import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader"; import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import { import {
Table, Table,
@@ -30,6 +31,7 @@ const CustomersTable = () => {
router, router,
columns, columns,
allCustomers, allCustomers,
metadata,
isModalOpen, isModalOpen,
assignSelectedCompanies, assignSelectedCompanies,
selectedCompanies, selectedCompanies,
@@ -48,6 +50,7 @@ const CustomersTable = () => {
watch, watch,
setFilterValue, setFilterValue,
isMutating, isMutating,
setParams,
} = useCustomerListPresenter(); } = useCustomerListPresenter();
return ( return (
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card"> <div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
@@ -170,6 +173,17 @@ const CustomersTable = () => {
</EmptyListWrapper> </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
<Pagination
currentPage={metadata?.page ? metadata?.page - 1 : 0}
totalPages={metadata?.pages ?? 1}
onPageChange={(e) => {
setParams((currentParams) => ({
...currentParams,
offset: e.selected * (metadata?.limit ?? 10),
limit: metadata?.limit ?? 10,
}));
}}
/>
<Modal <Modal
isOpen={isFilterModalOpen} isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)} onClose={() => setIsFilterModalOpen(false)}
@@ -2,14 +2,15 @@
import { import {
useAssignCompany, useAssignCompany,
useCustomersInfiniteQuery, useCustomersQuery,
useDeleteCustomerQuery, useDeleteCustomerQuery,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api"; import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
import { apiDefaultParams } from "@/constants/Api_Params";
import { deleteEmptyKeys } from "@/utils/shared"; import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query"; import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
const useCustomerListPresenter = () => { const useCustomerListPresenter = () => {
@@ -25,8 +26,9 @@ const useCustomerListPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const pathName = usePathname(); const pathName = usePathname();
const searchParams = useSearchParams(); const [params, setParams] = useState<Record<string, any>>({
const [params, setParams] = useState<Record<string, any>>({}); ...apiDefaultParams,
});
const router = useRouter(); const router = useRouter();
const isMutating = useIsMutating(); const isMutating = useIsMutating();
@@ -38,18 +40,7 @@ const useCustomerListPresenter = () => {
setValue: setFilterValue, setValue: setFilterValue,
} = useForm(); } = useForm();
useEffect(() => { const { data, isPending } = useCustomersQuery(params);
const newParams: Record<string, any> = {
sort: "created_at",
};
for (const [key, value] of searchParams.entries()) {
newParams[key] = value;
}
setParams(newParams);
filterFormReset(newParams);
}, [searchParams, filterFormReset]);
const { data, isPending } = useCustomersInfiniteQuery(params);
const { mutate: deleteCustomer } = useDeleteCustomerQuery(() => const { mutate: deleteCustomer } = useDeleteCustomerQuery(() =>
setIsModalOpen(false), setIsModalOpen(false),
); );
@@ -61,8 +52,9 @@ const useCustomerListPresenter = () => {
); );
const search = (data: any) => { const search = (data: any) => {
const newParams = { ...params, ...data }; const newParams = { ...params, ...data, offset: 0 };
const cleanedParams = deleteEmptyKeys(newParams); const cleanedParams = deleteEmptyKeys(newParams);
setParams(cleanedParams);
const queryString = new URLSearchParams(cleanedParams).toString(); const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`); router.push(`${pathName}?${queryString}`);
setIsFilterModalOpen(false); setIsFilterModalOpen(false);
@@ -85,9 +77,10 @@ const useCustomerListPresenter = () => {
} }
}; };
const customers = data?.pages.flatMap((page) => page.data.customers) ?? []; const customers = data?.data?.customers ?? [];
return { return {
allCustomers: customers, allCustomers: customers,
metadata: data?.meta,
currentCustomer, currentCustomer,
columns, columns,
isPending, isPending,
@@ -110,6 +103,7 @@ const useCustomerListPresenter = () => {
watch, watch,
setFilterValue, setFilterValue,
isMutating, isMutating,
setParams,
}; };
}; };
+9 -10
View File
@@ -1,4 +1,5 @@
import { ExclamationIcon } from "@/assets/icons"; import { ExclamationIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { import {
Table, Table,
TableBody, TableBody,
@@ -48,14 +49,12 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
{isLoading && <TableSkeleton column={columns.length} />} {isLoading && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{!feedback?.length ? ( <EmptyListWrapper
<TableRow className="w-full"> list={feedback ?? []}
<TableCell colSpan={500} className="text-center"> columns={columns}
<h2>No feedback</h2> emptyMessage="No feedback were found"
</TableCell> >
</TableRow> {(feedback ?? []).map((item, index) => (
) : (
feedback?.map((item, index) => (
<TableRow <TableRow
key={item.id} key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -87,8 +86,8 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
</button> </button>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))}
)} </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
</ShowcaseSection> </ShowcaseSection>
@@ -1,5 +1,6 @@
"use client"; "use client";
import { EyeIcon } from "@/assets/icons"; import { EyeIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { ShowcaseSection } from "@/components/Layouts/showcase-section"; import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import Pagination from "@/components/ui/pagination"; import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
@@ -60,14 +61,12 @@ const NotificationHistoryTable = () => {
</TableHeader> </TableHeader>
{isLoading && <TableSkeleton column={columns.length} />} {isLoading && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{!notificationHistory?.length ? ( <EmptyListWrapper
<TableRow> list={notificationHistory ?? []}
<TableCell> columns={columns}
<h2>No notifications</h2> emptyMessage="No notifications were found"
</TableCell> >
</TableRow> {(notificationHistory ?? []).map((item, index) => (
) : (
notificationHistory.map((item, index) => (
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell colSpan={100}>{index + 1}</TableCell> <TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{item.title}</TableCell> <TableCell colSpan={100}>{item.title}</TableCell>
@@ -114,8 +113,8 @@ const NotificationHistoryTable = () => {
</button> </button>
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))}
)} </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
<Pagination <Pagination
+10 -6
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import Status from "@/components/ui/Status"; import Status from "@/components/ui/Status";
import { import {
Table, Table,
@@ -13,6 +14,7 @@ import { Tooltip } from "react-tooltip";
import useTenderListPresenter from "./useTenderListPresenter"; import useTenderListPresenter from "./useTenderListPresenter";
import { ExclamationIcon, EyeIcon } from "@/assets/icons"; import { ExclamationIcon, EyeIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ListHeader from "@/components/ui/ListHeader"; import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal"; import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination"; import Pagination from "@/components/ui/pagination";
@@ -58,10 +60,12 @@ const TendersTable = () => {
{isPending && <TableSkeleton column={columns.length} />} {isPending && <TableSkeleton column={columns.length} />}
<TableBody> <TableBody>
{allTenders?.data.tenders?.map((item, index) => <EmptyListWrapper
!item ? ( list={allTenders?.data.tenders ?? []}
<h6 key={index}>No tenders were found</h6> columns={columns}
) : ( emptyMessage="No tenders were found"
>
{(allTenders?.data.tenders ?? []).map((item, index) => (
<TableRow <TableRow
key={item.id} key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7" className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -121,8 +125,8 @@ const TendersTable = () => {
</button> </button>
</TableCell> </TableCell>
</TableRow> </TableRow>
), ))}
)} </EmptyListWrapper>
</TableBody> </TableBody>
</Table> </Table>
<Modal <Modal
@@ -66,7 +66,7 @@ const useCreateNotificationFormPresenter = () => {
], ],
[NotificationTargetGroups.SPECIFIC_USERS]: [NotificationTargetGroups.SPECIFIC_USERS]:
users?.data.users?.map((user) => ({ users?.data.users?.map((user) => ({
label: user.full_name, label: user.full_name ?? "",
value: user.id, value: user.id,
})) ?? null, })) ?? null,
[NotificationTargetGroups.SPECIFIC_CUSTOMER]: [NotificationTargetGroups.SPECIFIC_CUSTOMER]:
+12
View File
@@ -36,6 +36,18 @@ export const useCustomersInfiniteQuery = (params?: Record<string, any>) => {
}, },
}); });
}; };
export const useCustomersQuery = (params?: Record<string, any>) => {
const queryKey = useMemo(
() => [API_ENDPOINTS.CUSTOMERS.READ_ALL, params],
[params],
);
return useQuery({
queryKey,
queryFn: () => customersService.getCustomers(params),
});
};
export const useGetAllCustomers = () => { export const useGetAllCustomers = () => {
const queryKey = useMemo(() => ["GET ALL CUSTOMERS"], []); const queryKey = useMemo(() => ["GET ALL CUSTOMERS"], []);
return useQuery({ return useQuery({