project setup
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user