Files
AthenaFE/components/features/admins/AdminAccountsTable.tsx
T
2025-11-02 10:08:56 +01:00

327 lines
11 KiB
TypeScript

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 &quot;{searchQuery}&quot;
</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>
</>
);
};