project setup
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { PersonalInfo } from "@/components/features/accounts/PersonalInfo";
|
||||
import { PersonalMetrics } from "@/components/features/accounts/PersonalMetrics";
|
||||
import { PersonalTodoList } from "@/components/features/accounts/PersonalTodoList";
|
||||
import { PersonalLoginList } from "@/components/features/accounts/PersonalLoginList";
|
||||
import { PersonalActions } from "@/components/features/accounts/PersonalActions";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
|
||||
interface AccountDetailsProps {
|
||||
user: User;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const AccountDetails = ({ user, isLoading }: AccountDetailsProps) => {
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonalActions user={user} isLoading={isLoading} />
|
||||
<Divider
|
||||
style={{ height: 6, backgroundColor: "#C4BEBE" }}
|
||||
className={isMobile ? "my-1" : "my-4"}
|
||||
/>
|
||||
|
||||
<PersonalInfo user={user} manager={user.manager} />
|
||||
<Divider
|
||||
style={{ height: 6, backgroundColor: "#C4BEBE" }}
|
||||
className={isMobile ? "my-1" : "my-4"}
|
||||
/>
|
||||
<PersonalMetrics user={user} />
|
||||
<Divider
|
||||
style={{ height: 6, backgroundColor: "#C4BEBE" }}
|
||||
className={isMobile ? "my-1" : "my-4"}
|
||||
/>
|
||||
<PersonalTodoList user={user} />
|
||||
<Divider
|
||||
style={{ height: 6, backgroundColor: "#C4BEBE" }}
|
||||
className={isMobile ? "my-1" : "my-4"}
|
||||
/>
|
||||
<PersonalLoginList user={user} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ReactNode } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
|
||||
interface InfoRowProps {
|
||||
label: string;
|
||||
value: string | ReactNode;
|
||||
}
|
||||
|
||||
export const InfoRow = ({ label, value }: InfoRowProps) => {
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
|
||||
return (
|
||||
<View className="flex-row mb-2 items-center">
|
||||
<Text
|
||||
className={`font-medium mb-2 mr-[2px] text-gray-600 ${
|
||||
isMobile ? "w-[50%]" : "w-[28%]"
|
||||
}`}
|
||||
>
|
||||
{label}:
|
||||
</Text>
|
||||
<Text
|
||||
className={`font-medium mb-2 text-black ${
|
||||
isMobile ? "w-[50%]" : "w-[72%]"
|
||||
}`}
|
||||
>
|
||||
{value || "-"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
import { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { UserModal } from "@/components/features/users/UserModal";
|
||||
import { DeleteUserModal } from "@/components/features/users/DeleteUserModal";
|
||||
import { LockUserModal } from "@/components/features/users/LockUserModal";
|
||||
import { AdminModal } from "@/components/features/admins/AdminModal";
|
||||
import { OperationMode } from "@/constants/operationModeEnum";
|
||||
import { RoleEnum } from "@/constants/roleEnum";
|
||||
import {
|
||||
useUpdateUserMutation,
|
||||
useDeleteUserMutation,
|
||||
useLockUserMutation,
|
||||
useGetUserByIdQuery,
|
||||
} from "@/store/api/usersApi";
|
||||
import { AppSnackbar } from "@/components/ui/AppSnackbar";
|
||||
|
||||
interface PersonalActionsProps {
|
||||
user: User;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const PersonalActions = ({ user, isLoading }: PersonalActionsProps) => {
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [lockModalVisible, setLockModalVisible] = useState(false);
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState("");
|
||||
|
||||
const { refetch } = useGetUserByIdQuery(user.id, { skip: !user.id });
|
||||
|
||||
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation();
|
||||
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation();
|
||||
const [lockUser, { isLoading: isLocking }] = useLockUserMutation();
|
||||
|
||||
const showSnackbar = (message: string) => {
|
||||
setSnackbarMessage(message);
|
||||
setSnackbarVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
|
||||
const handleLock = () => {
|
||||
setLockModalVisible(true);
|
||||
};
|
||||
|
||||
const handleUpdateUser = async (updatedUser: Partial<User>) => {
|
||||
try {
|
||||
await updateUser(updatedUser).unwrap();
|
||||
showSnackbar(
|
||||
`${updatedUser.firstName} ${updatedUser.lastName} updated successfully`
|
||||
);
|
||||
setEditModalVisible(false);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showSnackbar("Error updating user");
|
||||
console.error("Error updating user:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userToDelete: User) => {
|
||||
try {
|
||||
await deleteUser(userToDelete.id).unwrap();
|
||||
showSnackbar(
|
||||
`${userToDelete.firstName} ${userToDelete.lastName} deleted successfully`
|
||||
);
|
||||
setDeleteModalVisible(false);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showSnackbar("Error deleting user");
|
||||
console.error("Error deleting user:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLockUser = async (userToLock: User) => {
|
||||
try {
|
||||
await lockUser(userToLock.id).unwrap();
|
||||
const action = userToLock.isLocked ? "unlocked" : "locked";
|
||||
showSnackbar(
|
||||
`${userToLock.firstName} ${userToLock.lastName} ${action} successfully`
|
||||
);
|
||||
setLockModalVisible(false);
|
||||
refetch();
|
||||
} catch (error) {
|
||||
showSnackbar("Error updating user lock status");
|
||||
console.error("Error locking/unlocking user:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="flex-row justify-end">
|
||||
<IconButton
|
||||
icon="pencil-outline"
|
||||
onPress={handleEdit}
|
||||
size={24}
|
||||
iconColor="#7A757C"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
icon="lock-outline"
|
||||
onPress={handleLock}
|
||||
size={24}
|
||||
iconColor={user.isLocked ? "#F83434" : "#7A757C"}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<IconButton
|
||||
icon="delete-outline"
|
||||
onPress={handleDelete}
|
||||
size={24}
|
||||
iconColor="#7A757C"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{user.role === RoleEnum.ADMIN || user.role === RoleEnum.MANAGER ? (
|
||||
<AdminModal
|
||||
visible={editModalVisible}
|
||||
onClose={() => setEditModalVisible(false)}
|
||||
onSubmit={handleUpdateUser}
|
||||
admin={user}
|
||||
mode={OperationMode.EDIT}
|
||||
isLoading={isUpdating}
|
||||
/>
|
||||
) : (
|
||||
<UserModal
|
||||
visible={editModalVisible}
|
||||
onClose={() => setEditModalVisible(false)}
|
||||
onSubmit={handleUpdateUser}
|
||||
user={user}
|
||||
mode={OperationMode.EDIT}
|
||||
isLoading={isUpdating}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeleteUserModal
|
||||
visible={deleteModalVisible}
|
||||
user={user}
|
||||
onDismiss={() => setDeleteModalVisible(false)}
|
||||
onDelete={handleDeleteUser}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
|
||||
<LockUserModal
|
||||
visible={lockModalVisible}
|
||||
user={user}
|
||||
onDismiss={() => setLockModalVisible(false)}
|
||||
onLock={handleLockUser}
|
||||
isLoading={isLocking}
|
||||
/>
|
||||
|
||||
<AppSnackbar
|
||||
visible={snackbarVisible}
|
||||
onDismiss={() => setSnackbarVisible(false)}
|
||||
action={{ label: "Close", onPress: () => setSnackbarVisible(false) }}
|
||||
>
|
||||
{snackbarMessage}
|
||||
</AppSnackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { View } from "react-native";
|
||||
import { Avatar } from "react-native-paper";
|
||||
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { getInitials, getRoleLabel } from "@/utils/userUtils";
|
||||
import { InfoRow } from "@/components/features/accounts/InfoRow";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
|
||||
interface PersonalInfoProps {
|
||||
user: User;
|
||||
manager?: User | null;
|
||||
}
|
||||
|
||||
export const PersonalInfo = ({ user, manager }: PersonalInfoProps) => {
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
const managerFullName =
|
||||
manager && manager?.firstName + " " + manager?.lastName;
|
||||
|
||||
return (
|
||||
<View className="bg-lightGray rounded-md">
|
||||
<View className="flex-row items-center justify-between mb-4">
|
||||
<View className="flex-row items-center">
|
||||
{user.imageUrl ? (
|
||||
<Avatar.Image source={{ uri: user.imageUrl }} size={56} />
|
||||
) : (
|
||||
<Avatar.Text
|
||||
label={getInitials(user.firstName, user.lastName)}
|
||||
size={50}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className={`gap-6 ${isMobile ? "flex-col" : "flex-row"}`}>
|
||||
<View className="flex-1">
|
||||
<InfoRow label="First Name" value={user.firstName || "-"} />
|
||||
<InfoRow label="Middle Name" value={user.middleName || "-"} />
|
||||
<InfoRow label="Last Name" value={user.lastName || "-"} />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<InfoRow label="Email" value={user.email || "-"} />
|
||||
<InfoRow label="Phone Number" value={user.phoneNumber || "-"} />
|
||||
<InfoRow label="Department" value={user.department || "-"} />
|
||||
</View>
|
||||
|
||||
<View className="flex-1">
|
||||
<InfoRow label="Role" value={getRoleLabel(user.role)} />
|
||||
<InfoRow label="Reports To" value={managerFullName || "-"} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
import { User } from "@/store/models/User.model";
|
||||
|
||||
interface PersonalLoginListProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const PersonalLoginList = ({ user }: PersonalLoginListProps) => {
|
||||
return (
|
||||
<View className="bg-lightGray rounded-md">
|
||||
<Text className="font-medium my-4 text-gray-600">Log-in List:</Text>
|
||||
<Text className="font-medium text-black">No logins assigned</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { InfoRow } from "./InfoRow";
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
import { useGetUserLogsQuery } from "@/store/api/usersApi";
|
||||
import { formatDate, formatTime } from "@/utils/dateUtils";
|
||||
import { RiskIndicator } from "@/components/ui/RiskIndicator";
|
||||
import { SizeEnum } from "@/constants/sizeEnum";
|
||||
import { RiskEnum, RiskLabels } from "@/constants/riskEnum";
|
||||
import { SecurityTypeLabels } from "@/constants/securityTypeEnum";
|
||||
|
||||
interface PersonalMetricsProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const PersonalMetrics = ({ user }: PersonalMetricsProps) => {
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
const {
|
||||
data: logsData,
|
||||
isLoading: logsLoading,
|
||||
isError: logsError,
|
||||
} = useGetUserLogsQuery(
|
||||
{ page: 1, limit: 1, search: user.email },
|
||||
{ skip: !user?.email }
|
||||
);
|
||||
|
||||
const lastLogItem = logsData?.items?.[0];
|
||||
const lastLoginAt = lastLogItem?.lastLoginAt;
|
||||
const activity = lastLogItem?.activity;
|
||||
|
||||
const lastLoginDisplay = logsLoading
|
||||
? "Loading..."
|
||||
: logsError
|
||||
? "-"
|
||||
: lastLoginAt
|
||||
? `${formatDate(lastLoginAt)} ${formatTime(lastLoginAt)}`
|
||||
: "-";
|
||||
|
||||
const lastPasswordChangeDisplay = logsLoading
|
||||
? "Loading..."
|
||||
: logsError
|
||||
? "-"
|
||||
: activity === "Password reset" && lastLoginAt
|
||||
? `${formatDate(lastLoginAt)} ${formatTime(lastLoginAt)}`
|
||||
: "-";
|
||||
|
||||
return (
|
||||
<View className="bg-lightGray rounded-md">
|
||||
<View className={`gap-4 ${isMobile ? "flex-col" : "flex-row"}`}>
|
||||
<View className="flex-1 mt-4">
|
||||
<InfoRow label="Last Log-in" value={lastLoginDisplay} />
|
||||
<InfoRow
|
||||
label="Last Password Change"
|
||||
value={lastPasswordChangeDisplay}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex-1 mt-4">
|
||||
{user.riskStatus && (
|
||||
<InfoRow
|
||||
label="Risk Status"
|
||||
value={
|
||||
<View className="flex-row items-baseline gap-4">
|
||||
<Text>
|
||||
{user.riskStatus === RiskEnum.NO_RISK
|
||||
? RiskLabels[user.riskStatus]
|
||||
: RiskLabels[user.riskStatus].split(" ")[0]}
|
||||
</Text>
|
||||
<RiskIndicator risk={user.riskStatus} size={SizeEnum.Small} />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InfoRow
|
||||
label="Security Type"
|
||||
value={SecurityTypeLabels[user.securityType]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
import { useGetTodosByUserQuery } from "@/store/api/todosApi";
|
||||
import { PriorityIndicator } from "@/components/ui/PriorityIndicator";
|
||||
import { SizeEnum } from "@/constants/sizeEnum";
|
||||
import { StatusTableItem } from "@/components/ui/StatusTableItem";
|
||||
import { User } from "@/store/models/User.model";
|
||||
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
|
||||
import { NoData } from "@/components/ui/NoData";
|
||||
|
||||
interface PersonalTodoListProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const PersonalTodoList = ({ user }: PersonalTodoListProps) => {
|
||||
const {
|
||||
data: todos,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetTodosByUserQuery(user.id, { skip: !user.id });
|
||||
|
||||
return (
|
||||
<View className="bg-lightGray rounded-md">
|
||||
<Text className="font-medium my-4 text-gray-600">To-Do List:</Text>
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : error ? (
|
||||
<NoData
|
||||
onRetry={refetch}
|
||||
message="Failed to load todos"
|
||||
isRetrying={isFetching}
|
||||
/>
|
||||
) : todos && todos.length > 0 ? (
|
||||
<View style={{ gap: 8 }}>
|
||||
{todos.map((todo) => (
|
||||
<View
|
||||
key={todo.id}
|
||||
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
||||
>
|
||||
<View className="flex-row items-center justify-between mb-2">
|
||||
<Text
|
||||
className="text-base font-medium flex-1 mr-2"
|
||||
numberOfLines={10}
|
||||
>
|
||||
{todo.task}
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<StatusTableItem todoStatus={todo.status} />
|
||||
<PriorityIndicator
|
||||
priority={todo.priority}
|
||||
size={SizeEnum.Small}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="font-medium text-black">
|
||||
{user.role === "USER"
|
||||
? "No todos assigned for this user"
|
||||
: "No todos assigned"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user