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>
<p className="font-medium text-dark dark:text-white">Phone</p>
<p className="text-gray-600 dark:text-gray-300">
{formatPhoneNumber(data?.phone)}
{data?.phone ? formatPhoneNumber(data?.phone) : "-"}
</p>
</div>
<div>
@@ -84,7 +84,7 @@ export default function Page() {
<div>
<p className="font-medium text-dark dark:text-white">Status</p>
<div className="w-fit">
<Status status={data?.status}>{data?.status}</Status>
<Status status={data?.status ?? ""}>{data?.status ?? "-"}</Status>
</div>
</div>
</div>
+74 -77
View File
@@ -1,6 +1,7 @@
"use client";
import { PencilSquareIcon, TrashIcon, UserSettingIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
@@ -65,85 +66,81 @@ const AdminsTable = () => {
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
{!allUsers.length ? (
<TableRow>
<TableCell>
<h2>No user</h2>
</TableCell>
</TableRow>
) : (
allUsers.map((item, index) => {
return (
<TableRow
key={item?.id || index}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.full_name}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.email}
</TableCell>
<EmptyListWrapper
list={allUsers}
columns={columns}
emptyMessage="No admins were found"
>
{allUsers.map((item, index) => (
<TableRow
key={item?.id || index}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell className="text-start" colSpan={100}>
{index + 1}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.full_name}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.email}
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.username}
</TableCell>
<TableCell className="text-start" colSpan={100}>
<Status status={item?.status ?? ""}>{item?.status}</Status>
</TableCell>
<TableCell className="text-start" colSpan={100}>
{item?.username}
</TableCell>
<TableCell className="text-start" colSpan={100}>
<Status status={item?.status ?? ""}>{item?.status}</Status>
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentUser(item);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="change-status"
data-tooltip-content="Change status"
data-tooltip-place="top"
className="hover:text-yellow-dark"
onClick={() => {
setCurrentUser(item);
setIsChangeStatusModalOpen(true);
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "change-status" })}
/>
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() => router.push(`/admins/edit/${item.id}`)}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Admin </span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
);
})
)}
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentUser(item);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="change-status"
data-tooltip-content="Change status"
data-tooltip-place="top"
className="hover:text-yellow-dark"
onClick={() => {
setCurrentUser(item);
setIsChangeStatusModalOpen(true);
}}
>
<Tooltip
{..._TooltipDefaultParams({ id: "change-status" })}
/>
<span className="sr-only">Change Admin Status</span>
<UserSettingIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() => router.push(`/admins/edit/${item.id}`)}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Admin </span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
))}
</EmptyListWrapper>
</TableBody>
</Table>
<Modal
@@ -40,7 +40,7 @@ export const useAdminsPresenter = () => {
} = useForm<TChangeAdminStatusCredentials>({
mode: "onChange",
defaultValues: {
status: currentUser?.status,
status: currentUser?.status ?? "",
},
});
const [params, setParams] = useState<Record<string, any>>({});
@@ -1,6 +1,7 @@
"use client";
import { PencilSquareIcon, TrashIcon } from "@/assets/icons";
import { Switch } from "@/components/FormElements/switch";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import IsVisible from "@/components/ui/IsVisible";
import {
@@ -44,6 +45,15 @@ const CompanyCategoriesTable = () => {
isMutating,
} = useCompanyCategoriesPresenter();
const columns = [
"Row",
"Title",
"Description",
"Created at",
"Published",
"Actions",
];
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">
<ListHeader
@@ -61,77 +71,75 @@ const CompanyCategoriesTable = () => {
<TableHead colSpan={100}>Actions</TableHead>
</TableRow>
</TableHeader>
{isLoading && <TableSkeleton column={6} />}
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
<IsVisible condition={!categories}>
<TableRow>
<TableCell colSpan={400} className="text-center">
<h6>No category found</h6>
</TableCell>
</TableRow>
</IsVisible>
<IsVisible condition={!!categories}>
{categories?.map((category, index) => {
return (
<TableRow key={category.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{category.name}</TableCell>
<TableCell colSpan={100}>{category.description}</TableCell>
<TableCell colSpan={100}>
{unixToDate({ unix: category.created_at, hasTime: true })}
</TableCell>
<TableCell colSpan={100}>
<IsVisible condition={isToggling}>
<Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" />
</IsVisible>
<IsVisible condition={!isToggling}>
<Switch
withIcon
onChange={(e) => {
togglePublished(category.id);
}}
checked={category.published}
onToggle={(e) => {
console.log(e);
}}
/>
</IsVisible>
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentCategory(category);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() =>
router.push(`${pathName}/edit/${category.id}`)
}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Category</span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
);
})}
</IsVisible>
<EmptyListWrapper
list={categories ?? []}
columns={columns}
emptyMessage="No categories were found"
>
{(categories ?? []).map((category, index) => (
<TableRow
key={category.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{category.name}</TableCell>
<TableCell colSpan={100}>{category.description}</TableCell>
<TableCell colSpan={100}>
{unixToDate({ unix: category.created_at, hasTime: true })}
</TableCell>
<TableCell colSpan={100}>
<IsVisible condition={isToggling}>
<Skeleton className="h-8 bg-neutral-400 dark:bg-dark-4" />
</IsVisible>
<IsVisible condition={!isToggling}>
<Switch
withIcon
onChange={(e) => {
togglePublished(category.id);
}}
checked={category.published}
onToggle={(e) => {
console.log(e);
}}
/>
</IsVisible>
</TableCell>
<TableCell className="text-start xl:pr-7.5">
<div className="flex items-center justify-start gap-x-3.5">
<button
data-tooltip-id="delete"
data-tooltip-content="Delete"
data-tooltip-place="top"
className="hover:text-red-500"
onClick={() => {
setCurrentCategory(category);
setIsModalOpen(true);
}}
>
<Tooltip {..._TooltipDefaultParams({ id: "delete" })} />
<span className="sr-only">Delete Admin </span>
<TrashIcon />
</button>
<button
data-tooltip-id="edit"
data-tooltip-content="Edit"
data-tooltip-place="top"
className="hover:text-primary"
onClick={() =>
router.push(`${pathName}/edit/${category.id}`)
}
>
<Tooltip {..._TooltipDefaultParams({ id: "edit" })} />
<span className="sr-only">Edit Category</span>
<PencilSquareIcon />
</button>
</div>
</TableCell>
</TableRow>
))}
</EmptyListWrapper>
</TableBody>
</Table>
<Modal
+12 -6
View File
@@ -5,6 +5,7 @@ import {
PencilSquareIcon,
TrashIcon,
} from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import {
Table,
@@ -44,6 +45,9 @@ const CompaniesTable = () => {
setFilterValue,
isMutating,
} = useCompanyListPresenter();
const companies = allCompanies.filter(
(c): c is NonNullable<typeof c> => c != null,
);
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">
<ListHeader
@@ -66,10 +70,12 @@ const CompaniesTable = () => {
</TableHeader>
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
{allCompanies.map((company, index) =>
!company ? (
<h6 key={index}>No Data was Found</h6>
) : (
<EmptyListWrapper
list={companies}
columns={columns}
emptyMessage="No companies were found"
>
{companies.map((company, index) => (
<TableRow
key={company.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -156,8 +162,8 @@ const CompaniesTable = () => {
</div>
</TableCell>
</TableRow>
),
)}
))}
</EmptyListWrapper>
</TableBody>
</Table>
<Modal
+14
View File
@@ -5,6 +5,7 @@ import { Building } from "@/components/Layouts/sidebar/icons";
import ConfirmationModal from "@/components/ui/ConfirmationModal";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
import {
Table,
@@ -30,6 +31,7 @@ const CustomersTable = () => {
router,
columns,
allCustomers,
metadata,
isModalOpen,
assignSelectedCompanies,
selectedCompanies,
@@ -48,6 +50,7 @@ const CustomersTable = () => {
watch,
setFilterValue,
isMutating,
setParams,
} = useCustomerListPresenter();
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">
@@ -170,6 +173,17 @@ const CustomersTable = () => {
</EmptyListWrapper>
</TableBody>
</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
isOpen={isFilterModalOpen}
onClose={() => setIsFilterModalOpen(false)}
@@ -2,14 +2,15 @@
import {
useAssignCompany,
useCustomersInfiniteQuery,
useCustomersQuery,
useDeleteCustomerQuery,
} from "@/hooks/queries";
import { TAssignCustomerToCompanyCredentials, TCustomer } from "@/lib/api";
import { apiDefaultParams } from "@/constants/Api_Params";
import { deleteEmptyKeys } from "@/utils/shared";
import { useIsMutating } from "@tanstack/react-query";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
const useCustomerListPresenter = () => {
@@ -25,8 +26,9 @@ const useCustomerListPresenter = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
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 isMutating = useIsMutating();
@@ -38,18 +40,7 @@ const useCustomerListPresenter = () => {
setValue: setFilterValue,
} = useForm();
useEffect(() => {
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 { data, isPending } = useCustomersQuery(params);
const { mutate: deleteCustomer } = useDeleteCustomerQuery(() =>
setIsModalOpen(false),
);
@@ -61,8 +52,9 @@ const useCustomerListPresenter = () => {
);
const search = (data: any) => {
const newParams = { ...params, ...data };
const newParams = { ...params, ...data, offset: 0 };
const cleanedParams = deleteEmptyKeys(newParams);
setParams(cleanedParams);
const queryString = new URLSearchParams(cleanedParams).toString();
router.push(`${pathName}?${queryString}`);
setIsFilterModalOpen(false);
@@ -85,9 +77,10 @@ const useCustomerListPresenter = () => {
}
};
const customers = data?.pages.flatMap((page) => page.data.customers) ?? [];
const customers = data?.data?.customers ?? [];
return {
allCustomers: customers,
metadata: data?.meta,
currentCustomer,
columns,
isPending,
@@ -110,6 +103,7 @@ const useCustomerListPresenter = () => {
watch,
setFilterValue,
isMutating,
setParams,
};
};
+9 -10
View File
@@ -1,4 +1,5 @@
import { ExclamationIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import {
Table,
TableBody,
@@ -48,14 +49,12 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
{!feedback?.length ? (
<TableRow className="w-full">
<TableCell colSpan={500} className="text-center">
<h2>No feedback</h2>
</TableCell>
</TableRow>
) : (
feedback?.map((item, index) => (
<EmptyListWrapper
list={feedback ?? []}
columns={columns}
emptyMessage="No feedback were found"
>
{(feedback ?? []).map((item, index) => (
<TableRow
key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -87,8 +86,8 @@ const FeedbackTable = ({ id, paramKey }: IProps) => {
</button>
</TableCell>
</TableRow>
))
)}
))}
</EmptyListWrapper>
</TableBody>
</Table>
</ShowcaseSection>
@@ -1,5 +1,6 @@
"use client";
import { EyeIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import { ShowcaseSection } from "@/components/Layouts/showcase-section";
import Pagination from "@/components/ui/pagination";
import Status from "@/components/ui/Status";
@@ -60,14 +61,12 @@ const NotificationHistoryTable = () => {
</TableHeader>
{isLoading && <TableSkeleton column={columns.length} />}
<TableBody>
{!notificationHistory?.length ? (
<TableRow>
<TableCell>
<h2>No notifications</h2>
</TableCell>
</TableRow>
) : (
notificationHistory.map((item, index) => (
<EmptyListWrapper
list={notificationHistory ?? []}
columns={columns}
emptyMessage="No notifications were found"
>
{(notificationHistory ?? []).map((item, index) => (
<TableRow key={item.id}>
<TableCell colSpan={100}>{index + 1}</TableCell>
<TableCell colSpan={100}>{item.title}</TableCell>
@@ -114,8 +113,8 @@ const NotificationHistoryTable = () => {
</button>
</TableCell>
</TableRow>
))
)}
))}
</EmptyListWrapper>
</TableBody>
</Table>
<Pagination
+10 -6
View File
@@ -1,5 +1,6 @@
"use client";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import Status from "@/components/ui/Status";
import {
Table,
@@ -13,6 +14,7 @@ import { Tooltip } from "react-tooltip";
import useTenderListPresenter from "./useTenderListPresenter";
import { ExclamationIcon, EyeIcon } from "@/assets/icons";
import EmptyListWrapper from "@/components/HOC/EmptyListWrapper";
import ListHeader from "@/components/ui/ListHeader";
import Modal from "@/components/ui/modal";
import Pagination from "@/components/ui/pagination";
@@ -58,10 +60,12 @@ const TendersTable = () => {
{isPending && <TableSkeleton column={columns.length} />}
<TableBody>
{allTenders?.data.tenders?.map((item, index) =>
!item ? (
<h6 key={index}>No tenders were found</h6>
) : (
<EmptyListWrapper
list={allTenders?.data.tenders ?? []}
columns={columns}
emptyMessage="No tenders were found"
>
{(allTenders?.data.tenders ?? []).map((item, index) => (
<TableRow
key={item.id}
className="odd:bg-gray-2 dark:odd:bg-gray-7"
@@ -121,8 +125,8 @@ const TendersTable = () => {
</button>
</TableCell>
</TableRow>
),
)}
))}
</EmptyListWrapper>
</TableBody>
</Table>
<Modal
@@ -66,7 +66,7 @@ const useCreateNotificationFormPresenter = () => {
],
[NotificationTargetGroups.SPECIFIC_USERS]:
users?.data.users?.map((user) => ({
label: user.full_name,
label: user.full_name ?? "",
value: user.id,
})) ?? null,
[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 = () => {
const queryKey = useMemo(() => ["GET ALL CUSTOMERS"], []);
return useQuery({