project setup
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
import { useEffect, useMemo, useState, useDeferredValue } from "react";
|
||||
import { View, Text, ScrollView, GestureResponderEvent } from "react-native";
|
||||
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { NoData } from "@/components/ui/NoData";
|
||||
import {
|
||||
useGetUsersQuery,
|
||||
useCreateUserMutation,
|
||||
useUpdateUserMutation,
|
||||
useDeleteUserMutation,
|
||||
useResendInvitationMutation,
|
||||
} from "@/store/api/usersApi";
|
||||
import { AdminAccountsTableControls } from "@/components/features/admins/AdminAccountsTableControls";
|
||||
import { DesktopAdminAccountsTable } from "@/components/features/admins/DesktopAdminAccountsTable";
|
||||
import { MobileAdminAccountsTable } from "@/components/features/admins/MobileAdminAccountsTable";
|
||||
import { TablePagination } from "@/components/shared/TablePagination";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
import { AdminModal } from "@/components/features/admins/AdminModal";
|
||||
import { OperationMode } from "@/constants/operationModeEnum";
|
||||
import { DeleteUserModal } from "@/components/features/users/DeleteUserModal";
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { AppSnackbar } from "@/components/ui/AppSnackbar";
|
||||
|
||||
interface AdminAccountsTableProps {
|
||||
initialSearchQuery?: string;
|
||||
}
|
||||
|
||||
export const AdminAccountsTable = ({
|
||||
initialSearchQuery = "",
|
||||
}: AdminAccountsTableProps) => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<TableSortDirection>(
|
||||
TableSortDirection.ASCENDING
|
||||
);
|
||||
|
||||
const deferredSearch = useDeferredValue(searchQuery);
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
|
||||
const queryArgs = useMemo(() => {
|
||||
const allowedSorts = ["name", "email", "title", "role", "risk"] as const;
|
||||
const sortBy = (allowedSorts as readonly string[]).includes(
|
||||
sortColumn || ""
|
||||
)
|
||||
? (sortColumn as (typeof allowedSorts)[number])
|
||||
: undefined;
|
||||
const sortDir = sortDirection;
|
||||
const searchParam =
|
||||
debouncedSearch && debouncedSearch.length >= 2
|
||||
? debouncedSearch
|
||||
: undefined;
|
||||
return {
|
||||
roleType: "admin" as const,
|
||||
page: page + 1,
|
||||
limit: itemsPerPage,
|
||||
search: searchParam,
|
||||
sortBy,
|
||||
sortDir,
|
||||
};
|
||||
}, [page, itemsPerPage, debouncedSearch, sortColumn, sortDirection]);
|
||||
|
||||
const {
|
||||
data: adminsPageData,
|
||||
isLoading,
|
||||
error: fetchError,
|
||||
refetch,
|
||||
isFetching,
|
||||
} = useGetUsersQuery(queryArgs, { refetchOnMountOrArgChange: true });
|
||||
const adminUsers = adminsPageData?.items ?? [];
|
||||
const [createAdmin, { isLoading: isCreating }] = useCreateUserMutation();
|
||||
const [updateAdmin, { isLoading: isUpdating }] = useUpdateUserMutation();
|
||||
const [deleteAdmin, { isLoading: isDeleting }] = useDeleteUserMutation();
|
||||
const [resendInvitation, { isLoading: isResending }] =
|
||||
useResendInvitationMutation();
|
||||
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState({ x: 0, y: 0 });
|
||||
const [addAdminModalVisible, setAddAdminModalVisible] = useState(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [currentAdmin, setCurrentAdmin] = useState<User | null>(null);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
|
||||
const showSnackbar = (message: string) => {
|
||||
setSnackbarMessage(message);
|
||||
setSnackbarVisible(true);
|
||||
};
|
||||
|
||||
const totalItems = adminsPageData?.total ?? 0;
|
||||
const from = adminUsers.length ? page * itemsPerPage + 1 : 0;
|
||||
const to = adminUsers.length
|
||||
? Math.min(from + adminUsers.length - 1, totalItems)
|
||||
: 0;
|
||||
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
setSortColumn(column);
|
||||
setSortDirection(
|
||||
sortDirection === TableSortDirection.ASCENDING
|
||||
? TableSortDirection.DESCENDING
|
||||
: TableSortDirection.ASCENDING
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleItemsPerPageChange = (count: number) => {
|
||||
setItemsPerPage(count);
|
||||
};
|
||||
|
||||
const handleCreateAdmin = async (admin: Partial<User>) => {
|
||||
try {
|
||||
await createAdmin(admin).unwrap();
|
||||
showSnackbar(
|
||||
`${admin.firstName} ${admin.lastName} created. Invitation email sent if no password was provided.`
|
||||
);
|
||||
setAddAdminModalVisible(false);
|
||||
} catch {
|
||||
showSnackbar("Error creating admin");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateAdmin = async (admin: Partial<User>) => {
|
||||
try {
|
||||
await updateAdmin(admin).unwrap();
|
||||
showSnackbar(`${admin.firstName} ${admin.lastName} updated successfully`);
|
||||
setEditModalVisible(false);
|
||||
} catch {
|
||||
showSnackbar("Error updating admin");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAdmin = async (admin: User) => {
|
||||
try {
|
||||
await deleteAdmin(admin.id).unwrap();
|
||||
showSnackbar(`${admin.firstName} ${admin.lastName} deleted successfully`);
|
||||
setDeleteModalVisible(false);
|
||||
} catch {
|
||||
showSnackbar("Error deleting admin");
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendInvite = async (admin: User) => {
|
||||
try {
|
||||
await resendInvitation(admin.id).unwrap();
|
||||
showSnackbar(`Invitation resent to ${admin.email}`);
|
||||
} catch {
|
||||
showSnackbar("Error resending invitation");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (admin: User) => {
|
||||
setCurrentAdmin(admin);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const openDeleteModal = (admin: User) => {
|
||||
setCurrentAdmin(admin);
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebouncedSearch(deferredSearch.trim()), 450);
|
||||
return () => clearTimeout(t);
|
||||
}, [deferredSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = (initialSearchQuery || "").trim();
|
||||
if (q && q !== searchQuery) {
|
||||
setSearchQuery(q);
|
||||
setPage(0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialSearchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [itemsPerPage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-2xl font-bold ${
|
||||
isMobile ? "mb-1" : "mb-4"
|
||||
} text-white`}
|
||||
>
|
||||
Admins manager
|
||||
</Text>
|
||||
<View
|
||||
className={`bg-lightGray rounded-lg ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
} flex-1`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<LoadingSpinner />
|
||||
<Text className="mt-2">Loading admins...</Text>
|
||||
</View>
|
||||
) : fetchError ? (
|
||||
<NoData
|
||||
message="Error loading admins"
|
||||
onRetry={refetch}
|
||||
isRetrying={isFetching}
|
||||
/>
|
||||
) : (
|
||||
<View className="flex-1">
|
||||
<AdminAccountsTableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onSearch={handleSearch}
|
||||
onMenuDismiss={() => setMenuVisible(false)}
|
||||
onItemsPerPageChange={handleItemsPerPageChange}
|
||||
onAddAdmin={() => setAddAdminModalVisible(true)}
|
||||
/>
|
||||
|
||||
<AdminModal
|
||||
visible={addAdminModalVisible}
|
||||
onClose={() => setAddAdminModalVisible(false)}
|
||||
onSubmit={handleCreateAdmin}
|
||||
isLoading={isCreating}
|
||||
mode={OperationMode.ADD}
|
||||
/>
|
||||
|
||||
<AdminModal
|
||||
visible={editModalVisible}
|
||||
admin={currentAdmin}
|
||||
onClose={() => setEditModalVisible(false)}
|
||||
onSubmit={handleUpdateAdmin}
|
||||
isLoading={isUpdating}
|
||||
mode={OperationMode.EDIT}
|
||||
/>
|
||||
|
||||
<DeleteUserModal
|
||||
visible={deleteModalVisible}
|
||||
user={currentAdmin}
|
||||
onDismiss={() => setDeleteModalVisible(false)}
|
||||
onDelete={handleDeleteAdmin}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
|
||||
<View className="flex-1 overflow-hidden">
|
||||
{searchQuery && adminUsers.length === 0 ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-lg text-gray-500">
|
||||
No admins match your search for "{searchQuery}"
|
||||
</Text>
|
||||
</View>
|
||||
) : searchQuery.length === 0 && adminUsers.length === 0 ? (
|
||||
<View
|
||||
className={`flex-1 items-center justify-center ${
|
||||
isMobile ? "p-1" : "p-5"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-lg text-gray-500">
|
||||
No admins found
|
||||
</Text>
|
||||
</View>
|
||||
) : isMobile ? (
|
||||
<View className="flex-1">
|
||||
<MobileAdminAccountsTable
|
||||
admins={adminUsers}
|
||||
onEdit={handleEdit}
|
||||
onDelete={openDeleteModal}
|
||||
onResend={handleResendInvite}
|
||||
isResending={isResending}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView className="flex-1 h-96">
|
||||
<DesktopAdminAccountsTable
|
||||
admins={adminUsers}
|
||||
sortColumn={sortColumn}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
onEdit={handleEdit}
|
||||
onDelete={openDeleteModal}
|
||||
onResend={handleResendInvite}
|
||||
isResending={isResending}
|
||||
/>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TablePagination
|
||||
page={page}
|
||||
itemsPerPage={itemsPerPage}
|
||||
totalItems={totalItems}
|
||||
from={from}
|
||||
to={to}
|
||||
onPageChange={setPage}
|
||||
onItemsPerPageButtonPress={(event: GestureResponderEvent) => {
|
||||
const { pageX, pageY } = event.nativeEvent;
|
||||
setMenuAnchor({ x: pageX, y: pageY });
|
||||
setMenuVisible(true);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<AppSnackbar
|
||||
visible={snackbarVisible}
|
||||
onDismiss={() => setSnackbarVisible(false)}
|
||||
action={{ label: "Close", onPress: () => setSnackbarVisible(false) }}
|
||||
>
|
||||
{snackbarMessage}
|
||||
</AppSnackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user