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

376 lines
12 KiB
TypeScript

import { useEffect, useMemo, useState, useDeferredValue } from "react";
import { useLocalSearchParams } from "expo-router";
import { View, Text } from "react-native";
import { UserAccountsTable } from "@/components/features/users/UserAccountsTable";
import { UserModal } from "@/components/features/users/UserModal";
import { User } from "@/store/models/User.model";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import {
useDeleteUserMutation,
useGetUsersQuery,
useLockUserMutation,
useUpdateUserMutation,
useDeleteUsersMutation,
useCreateUserMutation,
} from "@/store/api/usersApi";
import { BulkDeleteUserModal } from "@/components/features/users/BulkDeleteUserModal";
import { LockUserModal } from "@/components/features/users/LockUserModal";
import { DeleteUserModal } from "@/components/features/users/DeleteUserModal";
import { NoData } from "@/components/ui/NoData";
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
import { RiskEnum } from "@/constants/riskEnum";
import { useAuthContext } from "@/auth/AuthContext";
import { OperationMode } from "@/constants/operationModeEnum";
import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
import { AppSnackbar } from "@/components/ui/AppSnackbar";
export const UsersPage = () => {
const params = useLocalSearchParams<{ q?: string }>();
const { user } = useAuthContext();
const [page, setPage] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState(25);
const [searchQuery, setSearchQuery] = useState("");
const [selectedRisks, setSelectedRisks] = useState<RiskEnum[]>([]);
const [sortColumn, setSortColumn] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<TableSortDirection>(
TableSortDirection.ASCENDING
);
const isMobile = useAppSelector(selectIsMobile);
const deferredSearch = useDeferredValue(searchQuery);
const [debouncedSearch, setDebouncedSearch] = useState(deferredSearch);
useEffect(() => {
const handler = setTimeout(
() => setDebouncedSearch(deferredSearch.trim()),
450
);
return () => clearTimeout(handler);
}, [deferredSearch]);
// Initialize search query from route param
useEffect(() => {
const qParam = (params.q ?? "").toString().trim();
if (qParam && qParam !== searchQuery) {
setSearchQuery(qParam);
setPage(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params.q]);
const queryArgs = useMemo(() => {
const allowedSorts = ["name", "email", "title", "risk"] as const;
const sortBy = (allowedSorts as readonly string[]).includes(
sortColumn || ""
)
? (sortColumn as (typeof allowedSorts)[number])
: undefined;
const sortDir = sortDirection;
const risks = selectedRisks.length ? selectedRisks : undefined;
const searchParam =
debouncedSearch && debouncedSearch.length >= 2
? debouncedSearch
: undefined;
return {
roleType: "user" as const,
page: page + 1,
limit: itemsPerPage,
search: searchParam,
risks,
sortBy,
sortDir,
};
}, [
page,
itemsPerPage,
debouncedSearch,
selectedRisks,
sortColumn,
sortDirection,
]);
const {
data: usersPage,
isLoading,
error,
refetch,
isFetching,
} = useGetUsersQuery(queryArgs, {
refetchOnMountOrArgChange: true,
refetchOnFocus: true,
refetchOnReconnect: true,
});
const users = usersPage?.items ?? [];
const totalItems = usersPage?.total ?? 0;
const from = users.length ? page * itemsPerPage + 1 : 0;
const to = users.length ? Math.min(from + users.length - 1, totalItems) : 0;
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();
const [lockUser, { isLoading: isLocking }] = useLockUserMutation();
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
const [createUser, { isLoading: isCreating }] = useCreateUserMutation();
const [bulkDeleteUsers, { isLoading: isBulkDeleting }] =
useDeleteUsersMutation();
const [userModalVisible, setUserModalVisible] = useState(false);
const [userModalMode, setUserModalMode] = useState<OperationMode>(
OperationMode.ADD
);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [bulkDeleteModalVisible, setBulkDeleteModalVisible] = useState(false);
const [lockModalVisible, setLockModalVisible] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [selectedUsersIds, setSelectedUsersIds] = useState<string[]>([]);
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [snackbarVisible, setSnackbarVisible] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState("");
const showSnackbar = (message: string) => {
setSnackbarMessage(message);
setSnackbarVisible(true);
};
const handleEdit = (user: User) => {
setCurrentUser(user);
setUserModalMode(OperationMode.EDIT);
setUserModalVisible(true);
};
const handleAddUser = () => {
setCurrentUser(null);
setUserModalMode(OperationMode.ADD);
setUserModalVisible(true);
};
const openDeleteModal = (user: User) => {
setCurrentUser(user);
setDeleteModalVisible(true);
};
const openLockModal = (user: User) => {
setCurrentUser(user);
setLockModalVisible(true);
};
const handleDeleteUser = async (user: User) => {
try {
await deleteUser(user.id).unwrap();
if (selectedUsersIds.includes(user.id)) {
setSelectedUsersIds(selectedUsersIds.filter((id) => id !== user.id));
}
refetch();
showSnackbar(
`${user.firstName} ${user.lastName} has been deleted successfully`
);
setDeleteModalVisible(false);
} catch (error) {
console.error(
`Error deleting user ${user.firstName} ${user.lastName}:`,
error
);
showSnackbar(`Error deleting ${user.firstName} ${user.lastName}`);
}
};
const handleLockUser = async (user: User) => {
const action = user.isLocked ? "unlock" : "lock";
try {
await lockUser(user.id).unwrap();
refetch();
showSnackbar(
`${user.firstName} ${user.lastName} has been ${action}ed successfully`
);
setLockModalVisible(false);
} catch (error) {
console.error(
`Error ${action}ing user ${user.firstName} ${user.lastName}:`,
error
);
showSnackbar(`Error ${action}ing ${user.firstName} ${user.lastName}`);
}
};
const handleBulkDeleteUsers = (users: User[]) => {
if (users.length === 0) return;
setSelectedUsersIds(users.map((user) => user.id));
setSelectedUsers(users);
setBulkDeleteModalVisible(true);
};
const confirmBulkDelete = async () => {
if (selectedUsersIds.length === 0) return;
try {
const userIds = selectedUsersIds;
const count = selectedUsersIds.length;
await bulkDeleteUsers(userIds).unwrap();
refetch();
showSnackbar(`${count} users deleted successfully`);
setBulkDeleteModalVisible(false);
setSelectedUsersIds([]);
} catch (error: any) {
console.error(`Error deleting multiple users:`, error);
const errorMessage =
error?.data?.error ||
error?.data?.details ||
"Error deleting multiple users";
showSnackbar(errorMessage);
}
};
const handleSubmitUser = async (payload: Partial<User>) => {
try {
if (userModalMode === OperationMode.EDIT) {
await updateUser(payload as User).unwrap();
showSnackbar(
`${payload.firstName} ${payload.lastName} updated successfully`
);
} else {
const res = await createUser(payload).unwrap();
showSnackbar(
`${res.firstName} ${res.lastName} added successfully${
!res.isActivated ? " and invitation sent" : ""
}`
);
}
setUserModalVisible(false);
refetch();
} catch (error) {
console.error(`Error saving user:`, error);
showSnackbar(`Error saving user`);
}
};
useEffect(() => {
// Refetch when auth user state changes (e.g., after activation/login)
refetch();
}, [user?.id, user?.isActivated, refetch]);
return (
<>
<View
className={`flex-1 bg-primaryPurpleLight ${isMobile ? "p-1" : "p-5"}`}
>
<Text
className={`text-2xl font-bold ${
isMobile ? "mb-1" : "mb-4"
} text-white`}
>
Users manager
</Text>
<View
className={`bg-lightGray rounded-lg ${
isMobile ? "p-1" : "p-5"
} flex-1`}
>
{isLoading ? (
<View className="flex-1 justify-center items-center py-10">
<LoadingSpinner />
<Text className="mt-2">Loading users...</Text>
</View>
) : error ? (
<View className="flex-1 justify-center items-center py-10">
<NoData
message="Error loading users"
onRetry={refetch}
isRetrying={isFetching}
/>
</View>
) : (
<UserAccountsTable
users={users}
onSelect={setSelectedUsersIds}
selectedUserIds={selectedUsersIds}
onEdit={handleEdit}
onAddUser={handleAddUser}
onDelete={openDeleteModal}
onLock={openLockModal}
onBulkDelete={handleBulkDeleteUsers}
page={page}
itemsPerPage={itemsPerPage}
totalItems={totalItems}
from={from}
to={to}
searchQuery={searchQuery}
selectedRisks={selectedRisks}
sortColumn={sortColumn}
sortDirection={sortDirection}
onAddingUser={isCreating}
onSearch={(q) => {
setSearchQuery(q);
setPage(0);
}}
onSelectRisks={(risks) => {
setSelectedRisks(risks);
setPage(0);
}}
onSort={(column) => {
if (sortColumn === column) {
setSortDirection((d) =>
d === TableSortDirection.ASCENDING
? TableSortDirection.DESCENDING
: TableSortDirection.ASCENDING
);
} else {
setSortColumn(column);
setSortDirection(TableSortDirection.ASCENDING);
}
setPage(0);
}}
onItemsPerPageChange={(value) => {
setItemsPerPage(value);
setPage(0);
}}
onPageChange={(newPage) => setPage(newPage)}
/>
)}
</View>
<UserModal
visible={userModalVisible}
mode={userModalMode}
user={currentUser}
onClose={() => setUserModalVisible(false)}
onSubmit={handleSubmitUser}
isLoading={isUpdating || isCreating}
/>
<DeleteUserModal
visible={deleteModalVisible}
user={currentUser}
onDismiss={() => setDeleteModalVisible(false)}
onDelete={handleDeleteUser}
isLoading={isDeleting}
/>
<BulkDeleteUserModal
visible={bulkDeleteModalVisible}
users={selectedUsers}
onDismiss={() => setBulkDeleteModalVisible(false)}
onDelete={confirmBulkDelete}
isLoading={isBulkDeleting}
/>
<LockUserModal
visible={lockModalVisible}
user={currentUser}
onDismiss={() => setLockModalVisible(false)}
onLock={handleLockUser}
isLoading={isLocking}
/>
</View>
<AppSnackbar
visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)}
action={{ label: "Close", onPress: () => setSnackbarVisible(false) }}
>
{snackbarMessage}
</AppSnackbar>
</>
);
};