initial commit

This commit is contained in:
Sone
2026-05-19 18:46:16 +02:00
parent a92d70ca74
commit 7e67fa8ea0
33 changed files with 6316 additions and 3952 deletions
+5 -5
View File
@@ -2,9 +2,9 @@ import { useEffect, useState, useCallback, useMemo } from "react";
import { View, Text, FlatList } from "react-native"; import { View, Text, FlatList } from "react-native";
import { useLocalSearchParams, router } from "expo-router"; import { useLocalSearchParams, router } from "expo-router";
import { ActivityIndicator } from "react-native-paper"; import { ActivityIndicator } from "react-native-paper";
import { useAppSelector } from "@/store/hooks"; import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice"; import { selectIsMobile } from "@/store/screen/screenSizeSlice";
import { SearchResult, useGlobalSearchQuery } from "@/store/api/searchApi"; import { SearchResult, useGlobalSearchQuery } from "@/store/api/searchApi";
import { SearchBar } from "@/components/ui/SearchBar"; import { SearchBar } from "@/components/ui/SearchBar";
import { SizeEnum } from "@/constants/sizeEnum"; import { SizeEnum } from "@/constants/sizeEnum";
@@ -31,7 +31,7 @@ const SearchPage = () => {
skip: !debouncedQ || debouncedQ.trim().length < 2, skip: !debouncedQ || debouncedQ.trim().length < 2,
refetchOnMountOrArgChange: true, refetchOnMountOrArgChange: true,
refetchOnFocus: true, refetchOnFocus: true,
} },
); );
const results = useMemo(() => data?.results ?? [], [data?.results]); const results = useMemo(() => data?.results ?? [], [data?.results]);
@@ -42,7 +42,7 @@ const SearchPage = () => {
}, []); }, []);
const isExpanded = useCallback( const isExpanded = useCallback(
(id: string) => !!expandedIds[id], (id: string) => !!expandedIds[id],
[expandedIds] [expandedIds],
); );
// Reset expansions when new query results arrive // Reset expansions when new query results arrive
@@ -68,7 +68,7 @@ const SearchPage = () => {
? new Date(item.createdAt).toLocaleString() ? new Date(item.createdAt).toLocaleString()
: null, : null,
})), })),
[results] [results],
); );
const renderResult = useCallback( const renderResult = useCallback(
@@ -79,7 +79,7 @@ const SearchPage = () => {
onToggle={toggleExpanded} onToggle={toggleExpanded}
/> />
), ),
[isExpanded, toggleExpanded] [isExpanded, toggleExpanded],
); );
return ( return (
+25 -3
View File
@@ -1,3 +1,25 @@
@tailwind base; @import "tailwindcss/theme.css" layer(theme);
@tailwind components; @import "tailwindcss/preflight.css" layer(base);
@tailwind utilities; @import "tailwindcss/utilities.css";
@import "nativewind/theme";
@theme {
--font-inter: "Inter", sans-serif;
--color-primaryPurple: #49385d;
--color-primaryPurpleLight: #a78fa1;
--color-primaryPurpleDark: #6750a4;
--color-darkGray: #49454f;
--color-darkMediumGray: #7a757c;
--color-mediumGray: #c4bebe;
--color-lightGray: #d9d9d9;
--color-secondary: #b768ff;
--color-tertiary: #2e1155;
--color-accent: #ff00ff;
--color-background: #1e0933;
--color-highRisk: #f83434;
--color-mediumRisk: #f8f834;
--color-lowRisk: #22c927;
--color-orange: #ff9800;
--color-blue: #56b3fa;
}
+1 -4
View File
@@ -1,9 +1,6 @@
module.exports = function (api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: [ presets: ["babel-preset-expo"],
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
}; };
}; };
+2 -2
View File
@@ -17,8 +17,8 @@ export const UserButton = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const initials = useMemo( const initials = useMemo(
() => getInitials(user?.firstName, user?.lastName), () => getInitials(user?.firstName!, user?.lastName!),
[user?.firstName, user?.lastName] [user?.firstName, user?.lastName],
); );
if (!user) return null; if (!user) return null;
+12 -16
View File
@@ -14,32 +14,28 @@ interface AccountDetailsProps {
isLoading: boolean; isLoading: boolean;
} }
const backgroundStyle = {
height: 6,
backgroundColor: "#C4BEBE",
};
export const AccountDetails = ({ user, isLoading }: AccountDetailsProps) => { export const AccountDetails = ({ user, isLoading }: AccountDetailsProps) => {
const isMobile = useAppSelector(selectIsMobile); const isMobile = useAppSelector(selectIsMobile);
return ( return (
<> <>
<PersonalActions user={user} isLoading={isLoading} /> <PersonalActions user={user} isLoading={isLoading} />
<Divider <Divider style={backgroundStyle} className={isMobile ? "my-1" : "my-4"} />
style={{ height: 6, backgroundColor: "#C4BEBE" }}
className={isMobile ? "my-1" : "my-4"}
/>
<PersonalInfo user={user} manager={user.manager} /> <PersonalInfo user={user} manager={user.manager} />
<Divider <Divider style={backgroundStyle} className={isMobile ? "my-1" : "my-4"} />
style={{ height: 6, backgroundColor: "#C4BEBE" }}
className={isMobile ? "my-1" : "my-4"}
/>
<PersonalMetrics user={user} /> <PersonalMetrics user={user} />
<Divider <Divider style={backgroundStyle} className={isMobile ? "my-1" : "my-4"} />
style={{ height: 6, backgroundColor: "#C4BEBE" }}
className={isMobile ? "my-1" : "my-4"}
/>
<PersonalTodoList user={user} /> <PersonalTodoList user={user} />
<Divider <Divider style={backgroundStyle} className={isMobile ? "my-1" : "my-4"} />
style={{ height: 6, backgroundColor: "#C4BEBE" }}
className={isMobile ? "my-1" : "my-4"}
/>
<PersonalLoginList user={user} /> <PersonalLoginList user={user} />
</> </>
); );
+3 -3
View File
@@ -43,11 +43,11 @@ export const AdminModal = ({
roleType: "admin", roleType: "admin",
limit: 1000, limit: 1000,
}, },
{ skip: !visible } { skip: !visible },
); );
const managerOptions = useMemo( const managerOptions = useMemo(
() => (adminsData?.items || []).filter((u) => u.role === RoleEnum.MANAGER), () => (adminsData?.items || []).filter((u) => u.role === RoleEnum.MANAGER),
[adminsData] [adminsData],
); );
const adminRef = useRef<User | null>(admin); const adminRef = useRef<User | null>(admin);
@@ -329,7 +329,7 @@ export const AdminModal = ({
<Button <Button
onPress={handleSubmit(submit)} onPress={handleSubmit(submit)}
disabled={isLoading || !isValid || (isEdit && !isDirty)} disabled={isLoading || !isValid || (isEdit && !isDirty)}
className="!ml-2" className="ml-2!"
> >
{submitText} {submitText}
</Button> </Button>
@@ -52,7 +52,7 @@ export const DesktopAdminAccountsTable = ({
{admins.map((admin) => ( {admins.map((admin) => (
<DataTable.Row key={admin.id}> <DataTable.Row key={admin.id}>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
{admin.imageUrl ? ( {admin.imageUrl ? (
<Avatar.Image <Avatar.Image
source={{ source={{
@@ -68,11 +68,11 @@ export const DesktopAdminAccountsTable = ({
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
/> />
)} )}
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text <Text
onPress={() => openDetails(admin)} onPress={() => openDetails(admin)}
numberOfLines={10} numberOfLines={10}
className="flex-shrink" className="shrink"
> >
{admin.firstName} {admin.lastName} {admin.firstName} {admin.lastName}
</Text> </Text>
@@ -93,7 +93,7 @@ export const DesktopAdminAccountsTable = ({
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
<Text numberOfLines={2}>{admin.email}</Text> <Text numberOfLines={2}>{admin.email}</Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
@@ -102,7 +102,7 @@ export const DesktopAdminAccountsTable = ({
justifyContent: "center", justifyContent: "center",
}} }}
> >
<View className="flex-row items-center flex-shrink"> <View className="flex-row items-center shrink">
<Text numberOfLines={2}> <Text numberOfLines={2}>
{admin.role === RoleEnum.ADMIN ? "Admin" : "Manager"} {admin.role === RoleEnum.ADMIN ? "Admin" : "Manager"}
</Text> </Text>
+4 -1
View File
@@ -1,6 +1,7 @@
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import { Button } from "react-native-paper"; import { Button } from "react-native-paper";
import { router } from "expo-router"; import { router } from "expo-router";
import { useAppSelector } from "@/store/hooks"; import { useAppSelector } from "@/store/hooks";
import { selectIsMobile } from "@/store/screen/screenSizeSlice"; import { selectIsMobile } from "@/store/screen/screenSizeSlice";
@@ -10,7 +11,9 @@ export const AccessDenied = () => {
return ( return (
<View className="flex-1 bg-primaryPurpleLight"> <View className="flex-1 bg-primaryPurpleLight">
<View className={`${isMobile ? "p-1" : "p-5"} flex-1`}> <View className={`${isMobile ? "p-1" : "p-5"} flex-1`}>
<View className={`${isMobile ? "p-1" : "p-5"} bg-lightGray rounded-lg flex-1`}> <View
className={`${isMobile ? "p-1" : "p-5"} bg-lightGray rounded-lg flex-1`}
>
<View className="flex-1 justify-center items-center p-4"> <View className="flex-1 justify-center items-center p-4">
<Text className="text-2xl font-bold mb-4 text-highRisk"> <Text className="text-2xl font-bold mb-4 text-highRisk">
Access Denied Access Denied
@@ -31,27 +31,30 @@ export const DesktopLogsTable = ({
{logs.map((log) => ( {logs.map((log) => (
<DataTable.Row key={log.id}> <DataTable.Row key={log.id}>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
<Text numberOfLines={1}>{log.name}</Text> <Text numberOfLines={1}>{log.name}</Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
<Text numberOfLines={1}>{log.email}</Text> <Text numberOfLines={1}>{log.email}</Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
<Text numberOfLines={1}> <Text numberOfLines={1}>
{formatDate(log.lastLoginAt)} {formatTimeWithSeconds(log.lastLoginAt)} {formatDate(log.lastLoginAt)}{" "}
{formatTimeWithSeconds(log.lastLoginAt)}
</Text> </Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink pr-2"> <View className="flex-row items-center shrink pr-2">
<Text <Text
numberOfLines={1} numberOfLines={1}
style={{ color: log.status === "FAILURE" ? "#F83434" : "#22C927" }} style={{
color: log.status === "FAILURE" ? "#F83434" : "#22C927",
}}
> >
{log.activity} {log.activity}
{log.status === "FAILURE" && reasonLabel(log.failureReason) {log.status === "FAILURE" && reasonLabel(log.failureReason)
+7 -4
View File
@@ -5,7 +5,7 @@ import { formatDate, formatTimeWithSeconds } from "@/utils/dateUtils";
import { reasonLabel } from "@/utils/loginUtils"; import { reasonLabel } from "@/utils/loginUtils";
interface MobileLogsTableProps { interface MobileLogsTableProps {
logs: UserLoginLog[]; logs: UserLoginLog[] | null | undefined;
} }
export const MobileLogsTable = ({ logs }: MobileLogsTableProps) => { export const MobileLogsTable = ({ logs }: MobileLogsTableProps) => {
@@ -15,18 +15,21 @@ export const MobileLogsTable = ({ logs }: MobileLogsTableProps) => {
className="flex-1" className="flex-1"
contentContainerStyle={{ paddingBottom: 16 }} contentContainerStyle={{ paddingBottom: 16 }}
> >
{logs.map((log) => ( {logs?.map((log) => (
<View key={log.id} className="mb-3 p-4 bg-white rounded-md shadow-sm"> <View key={log.id} className="mb-3 p-4 bg-white rounded-md shadow-sm">
<Text className="text-base font-semibold"> <Text className="text-base font-semibold">
{log.name}{" "} {log.name}{" "}
<Text className="font-normal text-gray-600">({log.email})</Text> <Text className="font-normal text-gray-600">({log.email})</Text>
</Text> </Text>
<Text className="text-sm text-gray-700 mt-1"> <Text className="text-sm text-gray-700 mt-1">
Time: {formatDate(log.lastLoginAt)} {formatTimeWithSeconds(log.lastLoginAt)} Time: {formatDate(log.lastLoginAt)}{" "}
{formatTimeWithSeconds(log.lastLoginAt)}
</Text> </Text>
<Text <Text
className="text-sm mt-1" className="text-sm mt-1"
style={{ color: log.status === "FAILURE" ? "#F83434" : "#22C927" }} style={{
color: log.status === "FAILURE" ? "#F83434" : "#22C927",
}}
> >
Login activity: {log.activity} Login activity: {log.activity}
{log.status === "FAILURE" && reasonLabel(log.failureReason) {log.status === "FAILURE" && reasonLabel(log.failureReason)
+3 -2
View File
@@ -1,9 +1,10 @@
import { SearchResult } from "@/store/api/searchApi";
import { normalizeEnumLike } from "@/utils/formattingUtils";
import { memo } from "react"; import { memo } from "react";
import { Text, View, TouchableOpacity } from "react-native"; import { Text, View, TouchableOpacity } from "react-native";
import { Card, Divider } from "react-native-paper"; import { Card, Divider } from "react-native-paper";
import { SearchResult } from "@/store/api/searchApi";
import { normalizeEnumLike } from "@/utils/formattingUtils";
interface ResultRowProps { interface ResultRowProps {
item: SearchResult; item: SearchResult;
expanded: boolean; expanded: boolean;
@@ -68,7 +68,7 @@ export const DeleteTodoModal = ({
{error && <Text className="text-highRisk mt-2">{error}</Text>} {error && <Text className="text-highRisk mt-2">{error}</Text>}
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button className="!ml-2" onPress={handleDelete} disabled={isLoading}> <Button className="ml-2!" onPress={handleDelete} disabled={isLoading}>
Delete Delete
</Button> </Button>
</Dialog.Actions> </Dialog.Actions>
@@ -48,14 +48,14 @@ export const DesktopAdminTodosTable = ({
{todos.map((todo) => ( {todos.map((todo) => (
<DataTable.Row key={todo.id}> <DataTable.Row key={todo.id}>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text onPress={() => openDetails(todo)} numberOfLines={10}> <Text onPress={() => openDetails(todo)} numberOfLines={10}>
{todo.title} {todo.title}
</Text> </Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text numberOfLines={10}> <Text numberOfLines={10}>
{todo.task && todo.task.length > 50 {todo.task && todo.task.length > 50
? `${todo.task.slice(0, 50)}...` ? `${todo.task.slice(0, 50)}...`
@@ -79,7 +79,7 @@ export const DesktopAdminTodosTable = ({
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell style={{ justifyContent: "center" }}> <DataTable.Cell style={{ justifyContent: "center" }}>
<Text className="px-1 py-1">{`${formatDateMDY( <Text className="px-1 py-1">{`${formatDateMDY(
todo.createdAt todo.createdAt,
)} ${formatTime(todo.createdAt)}`}</Text> )} ${formatTime(todo.createdAt)}`}</Text>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell style={{ justifyContent: "center" }}> <DataTable.Cell style={{ justifyContent: "center" }}>
+15 -9
View File
@@ -32,7 +32,7 @@ interface TodoModalProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
onSubmit: ( onSubmit: (
todo: Partial<Todo> & { assigneeIds?: string[]; userId?: string } todo: Partial<Todo> & { assigneeIds?: string[]; userId?: string },
) => void; ) => void;
isLoading: boolean; isLoading: boolean;
todo?: Todo | null; todo?: Todo | null;
@@ -51,9 +51,11 @@ export const TodoModal = ({
todoRef.current = todo; todoRef.current = todo;
// Helpers to extract existing selections from the todo // Helpers to extract existing selections from the todo
const existingSupervisors = (todo?.assignees ?? []).filter( const existingSupervisors = (todo?.assignees ?? []).filter(
(u) => u.role === RoleEnum.ADMIN || u.role === RoleEnum.MANAGER (u) => u.role === RoleEnum.ADMIN || u.role === RoleEnum.MANAGER,
);
const existingUser = (todo?.assignees ?? []).find(
(u) => u.role === RoleEnum.USER,
); );
const existingUser = (todo?.assignees ?? []).find((u) => u.role === RoleEnum.USER);
const defaultValues: TodoFormValues = { const defaultValues: TodoFormValues = {
id: todo?.id, id: todo?.id,
title: todo?.title ?? "", title: todo?.title ?? "",
@@ -126,10 +128,12 @@ export const TodoModal = ({
sortDir: "asc" as any, sortDir: "asc" as any,
search: adminSearchQuery.trim(), search: adminSearchQuery.trim(),
}, },
{ skip: !visible || adminSearchQuery.trim().length < 2 } { skip: !visible || adminSearchQuery.trim().length < 2 },
); );
const adminUsers = adminUsersPage?.items ?? []; const adminUsers = adminUsersPage?.items ?? [];
const preselectedSupervisorItems = existingSupervisors.map(mapUserToDropdownItem); const preselectedSupervisorItems = existingSupervisors.map(
mapUserToDropdownItem,
);
const adminDropdownItems = uniqueById([ const adminDropdownItems = uniqueById([
...preselectedSupervisorItems, ...preselectedSupervisorItems,
...adminUsers.map(mapUserToDropdownItem), ...adminUsers.map(mapUserToDropdownItem),
@@ -146,10 +150,12 @@ export const TodoModal = ({
sortDir: "asc" as any, sortDir: "asc" as any,
search: userSearchQuery.trim(), search: userSearchQuery.trim(),
}, },
{ skip: !visible || userSearchQuery.trim().length < 2 } { skip: !visible || userSearchQuery.trim().length < 2 },
); );
const regularUsers = regularUsersPage?.items ?? []; const regularUsers = regularUsersPage?.items ?? [];
const preselectedUserItems = existingUser ? [mapUserToDropdownItem(existingUser)] : []; const preselectedUserItems = existingUser
? [mapUserToDropdownItem(existingUser)]
: [];
const regularUserDropdownItems = uniqueById([ const regularUserDropdownItems = uniqueById([
...preselectedUserItems, ...preselectedUserItems,
...regularUsers.map(mapUserToDropdownItem), ...regularUsers.map(mapUserToDropdownItem),
@@ -173,7 +179,7 @@ export const TodoModal = ({
if (!visible) return; if (!visible) return;
const t = todoRef.current; const t = todoRef.current;
const admins = (t?.assignees ?? []).filter( const admins = (t?.assignees ?? []).filter(
(u) => u.role === RoleEnum.ADMIN || u.role === RoleEnum.MANAGER (u) => u.role === RoleEnum.ADMIN || u.role === RoleEnum.MANAGER,
); );
const user = (t?.assignees ?? []).find((u) => u.role === RoleEnum.USER); const user = (t?.assignees ?? []).find((u) => u.role === RoleEnum.USER);
const nextDefaults: TodoFormValues = { const nextDefaults: TodoFormValues = {
@@ -452,7 +458,7 @@ export const TodoModal = ({
<Button <Button
onPress={handleSubmit(submit)} onPress={handleSubmit(submit)}
disabled={isLoading || !isValid || !isDirty} disabled={isLoading || !isValid || !isDirty}
className="!ml-2" className="ml-2!"
> >
{mode === OperationMode.ADD ? "Add" : "Edit"} {mode === OperationMode.ADD ? "Add" : "Edit"}
</Button> </Button>
@@ -83,7 +83,7 @@ export const BulkDeleteUserModal = ({
{error && <Text className="text-highRisk mt-2">{error}</Text>} {error && <Text className="text-highRisk mt-2">{error}</Text>}
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button className="!ml-2" onPress={handleDelete} disabled={isLoading}> <Button className="ml-2!" onPress={handleDelete} disabled={isLoading}>
{users.length === 1 ? "Delete" : "Delete All"} {users.length === 1 ? "Delete" : "Delete All"}
</Button> </Button>
</Dialog.Actions> </Dialog.Actions>
@@ -63,7 +63,7 @@ export const DeleteUserModal = ({
{error && <Text className="text-highRisk mt-2">{error}</Text>} {error && <Text className="text-highRisk mt-2">{error}</Text>}
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button className="!ml-2" onPress={handleDelete} disabled={isLoading}> <Button className="ml-2!" onPress={handleDelete} disabled={isLoading}>
Delete Delete
</Button> </Button>
</Dialog.Actions> </Dialog.Actions>
@@ -65,7 +65,7 @@ export const DesktopUserAccountsTable = ({
style={{ marginRight: 10, marginLeft: 10 }} style={{ marginRight: 10, marginLeft: 10 }}
/> />
)} )}
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text <Text
numberOfLines={10} numberOfLines={10}
onPress={() => router.push(`/accounts/users/${user.id}`)} onPress={() => router.push(`/accounts/users/${user.id}`)}
@@ -79,12 +79,12 @@ export const DesktopUserAccountsTable = ({
<RiskIndicator risk={user.riskStatus} size={SizeEnum.Moderate} /> <RiskIndicator risk={user.riskStatus} size={SizeEnum.Moderate} />
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text numberOfLines={10}>{user.email}</Text> <Text numberOfLines={10}>{user.email}</Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
<DataTable.Cell> <DataTable.Cell>
<View className="flex-row items-center flex-shrink px-1 py-1"> <View className="flex-row items-center shrink px-1 py-1">
<Text numberOfLines={10}>{user.title}</Text> <Text numberOfLines={10}>{user.title}</Text>
</View> </View>
</DataTable.Cell> </DataTable.Cell>
+1 -1
View File
@@ -66,7 +66,7 @@ export const LockUserModal = ({
{error && <Text className="text-highRisk mt-2">{error}</Text>} {error && <Text className="text-highRisk mt-2">{error}</Text>}
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button className="!ml-2" onPress={handleLock} disabled={isLoading}> <Button className="ml-2!" onPress={handleLock} disabled={isLoading}>
{actionTitle} {actionTitle}
</Button> </Button>
</Dialog.Actions> </Dialog.Actions>
+2 -2
View File
@@ -87,7 +87,7 @@ export const UserModal = ({
email: vals.email.trim().toLowerCase(), email: vals.email.trim().toLowerCase(),
title: vals.title.trim(), title: vals.title.trim(),
riskStatus: vals.riskStatus, riskStatus: vals.riskStatus,
role: isEdit ? user?.role ?? RoleEnum.USER : RoleEnum.USER, role: isEdit ? (user?.role ?? RoleEnum.USER) : RoleEnum.USER,
}; };
return onSubmit(payload); return onSubmit(payload);
}; };
@@ -266,7 +266,7 @@ export const UserModal = ({
<Button <Button
onPress={handleSubmit(submit)} onPress={handleSubmit(submit)}
disabled={isLoading || (isEdit && !isDirty)} disabled={isLoading || (isEdit && !isDirty)}
className="!ml-2" className="ml-2!"
> >
{submitText} {submitText}
</Button> </Button>
+6
View File
@@ -3,3 +3,9 @@ export enum OperationMode {
EDIT, EDIT,
DELETE, DELETE,
} }
export const OperationModeLabels: Record<OperationMode, string> = {
[OperationMode.ADD]: "Add",
[OperationMode.EDIT]: "Edit",
[OperationMode.DELETE]: "Delete",
};
+6
View File
@@ -3,3 +3,9 @@ export enum RoleEnum {
ADMIN = "ADMIN", ADMIN = "ADMIN",
USER = "USER", USER = "USER",
} }
export const RoleLabels: Record<RoleEnum, string> = {
[RoleEnum.MANAGER]: "Manager",
[RoleEnum.ADMIN]: "Admin",
[RoleEnum.USER]: "User",
};
+7
View File
@@ -4,3 +4,10 @@ export enum SizeEnum {
Large = "LARGE", Large = "LARGE",
Regular = "REGULAR", Regular = "REGULAR",
} }
export const SizeLabels: Record<SizeEnum, string> = {
[SizeEnum.Small]: "Small",
[SizeEnum.Moderate]: "Moderate",
[SizeEnum.Large]: "Large",
[SizeEnum.Regular]: "Regular",
};
+5
View File
@@ -2,3 +2,8 @@ export enum TableSortDirection {
ASCENDING = "ASCENDING", ASCENDING = "ASCENDING",
DESCENDING = "DESCENDING", DESCENDING = "DESCENDING",
} }
export const TableSortDirectionLabels: Record<TableSortDirection, string> = {
[TableSortDirection.ASCENDING]: "Ascending",
[TableSortDirection.DESCENDING]: "Descending",
};
+2 -6
View File
@@ -1,16 +1,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { debounce } from "lodash";
export const useDebounce = <T>(value: T, delay = 300): T => { export const useDebounce = <T>(value: T, delay = 300): T => {
const [debouncedValue, setDebouncedValue] = useState(value); const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { useEffect(() => {
const handler = debounce(() => setDebouncedValue(value), delay); const handler = setTimeout(() => setDebouncedValue(value), delay);
handler(); return () => clearTimeout(handler);
return () => {
handler.cancel();
};
}, [value, delay]); }, [value, delay]);
return debouncedValue; return debouncedValue;
+1
View File
@@ -17,6 +17,7 @@ export const useRoleAccess = () => {
isManager, isManager,
hasAccess: (level: RoleEnum) => { hasAccess: (level: RoleEnum) => {
const hierarchy = [RoleEnum.USER, RoleEnum.ADMIN, RoleEnum.MANAGER]; const hierarchy = [RoleEnum.USER, RoleEnum.ADMIN, RoleEnum.MANAGER];
return ( return (
currentRole !== undefined && currentRole !== undefined &&
hierarchy.indexOf(currentRole) >= hierarchy.indexOf(level) hierarchy.indexOf(currentRole) >= hierarchy.indexOf(level)
+1 -1
View File
@@ -3,4 +3,4 @@ const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname); const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./app/globals.css" }); module.exports = withNativeWind(config);
+6089 -3802
View File
File diff suppressed because it is too large Load Diff
+60 -54
View File
@@ -12,64 +12,70 @@
"lint": "expo lint" "lint": "expo lint"
}, },
"dependencies": { "dependencies": {
"@clerk/clerk-expo": "^2.14.12", "@clerk/clerk-expo": "^2.19.31",
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^15.1.1",
"@hookform/resolvers": "^5.2.1", "@hookform/resolvers": "^5.2.2",
"@react-native-async-storage/async-storage": "2.1.2", "@react-native-async-storage/async-storage": "3.0.2",
"@react-native-picker/picker": "^2.11.1", "@react-native-picker/picker": "^2.11.4",
"@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/bottom-tabs": "^7.16.1",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.9.18",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.2.4",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.12.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "~53.0.23", "expo": "~55.0.24",
"expo-auth-session": "~6.2.1", "expo-auth-session": "~55.0.16",
"expo-blur": "~14.1.5", "expo-blur": "~55.0.14",
"expo-constants": "~17.1.7", "expo-constants": "~55.0.16",
"expo-font": "~13.3.2", "expo-font": "~55.0.7",
"expo-haptics": "~14.1.4", "expo-haptics": "~55.0.14",
"expo-image": "~2.4.0", "expo-image": "~55.0.10",
"expo-linear-gradient": "^14.1.5", "expo-linear-gradient": "^55.0.14",
"expo-linking": "~7.1.7", "expo-linking": "~55.0.15",
"expo-router": "~5.1.7", "expo-router": "~55.0.14",
"expo-secure-store": "~14.2.4", "expo-secure-store": "~55.0.14",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~55.0.21",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~55.0.6",
"expo-symbols": "~0.4.5", "expo-symbols": "~55.0.8",
"expo-system-ui": "~5.0.11", "expo-system-ui": "~55.0.18",
"expo-web-browser": "~14.2.0", "expo-web-browser": "~55.0.16",
"lodash": "^4.17.21", "lodash": "^4.18.1",
"lucide-react": "^0.515.0", "lucide-react": "^1.16.0",
"lucide-react-native": "^0.515.0", "lucide-react-native": "^1.16.0",
"nativewind": "^4.1.23", "nativewind": "^5.0.0-preview.4",
"react": "^19.2.3", "react": "^19.2.6",
"react-dom": "^19.2.3", "react-dom": "^19.2.6",
"react-hook-form": "^7.62.0", "react-hook-form": "^7.76.0",
"react-native": "0.79.5", "react-native": "0.85.3",
"react-native-gesture-handler": "~2.24.0", "react-native-css": "^3.0.7",
"react-native-paper": "^5.14.5", "react-native-gesture-handler": "~2.31.2",
"react-native-paper-dates": "^0.22.49", "react-native-paper": "^5.15.2",
"react-native-pie-chart": "^4.0.1", "react-native-paper-dates": "^0.23.8",
"react-native-reanimated": "~3.17.4", "react-native-pie-chart": "^4.0.2",
"react-native-safe-area-context": "^5.4.0", "react-native-reanimated": "4.2.1",
"react-native-screens": "~4.11.1", "react-native-safe-area-context": "~5.6.2",
"react-native-svg": "^15.11.2", "react-native-screens": "~4.25.0",
"react-native-web": "~0.20.0", "react-native-svg": "^15.15.5",
"react-native-webview": "13.13.5", "react-native-web": "~0.21.2",
"react-redux": "^9.2.0", "react-native-webview": "13.16.1",
"tailwindcss": "^3.4.17", "react-redux": "^9.3.0",
"zod": "^4.0.17" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.29.0",
"@types/lodash": "^4.17.20", "@tailwindcss/postcss": "^4.3.0",
"@types/react": "~19.0.10", "@types/lodash": "^4.17.24",
"eslint": "^9.25.0", "@types/react": "~19.2.14",
"eslint-config-expo": "~9.2.0", "eslint": "^10.4.0",
"json-server": "^1.0.0-beta.3", "eslint-config-expo": "~55.0.1",
"react-native-worklets": "^0.5.2", "json-server": "^1.0.0-beta.15",
"typescript": "~5.8.3" "postcss": "^8.5.14",
"react-native-worklets": "^0.8.3",
"tailwindcss": "^4.3.0",
"typescript": "~6.0.3"
},
"overrides": {
"lightningcss": "1.30.1"
}, },
"private": true "private": true
} }
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
+2 -3
View File
@@ -34,8 +34,7 @@ export type UserLoginLog = {
| "ACCOUNT_LOCKED" | "ACCOUNT_LOCKED"
| "NOT_ACTIVATED" | "NOT_ACTIVATED"
| "NOT_ALLOWED" | "NOT_ALLOWED"
| "UNKNOWN" | "UNKNOWN";
| null;
}; };
const normalizeSortDir = (dir?: TableSortDirection) => { const normalizeSortDir = (dir?: TableSortDirection) => {
@@ -114,7 +113,7 @@ export const usersApi = baseApi.injectEndpoints({
providesTags: (result) => [ providesTags: (result) => [
"Users", "Users",
...(result?.items ?? []).map( ...(result?.items ?? []).map(
({ id }) => ({ type: "Users", id } as const) ({ id }) => ({ type: "Users", id }) as const,
), ),
], ],
}), }),
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"module": "es2022", "module": "esnext",
"strict": true, "strict": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
+20 -5
View File
@@ -1,22 +1,33 @@
import { format, isSameWeek } from "date-fns"; import { format, isSameWeek } from "date-fns";
/** Formats a date input to a time string in "HH:mm" format. Returns an empty string if input is null or undefined. */
export const formatTime = (dateInput: string | Date | null | undefined) => { export const formatTime = (dateInput: string | Date | null | undefined) => {
if (!dateInput) return ""; if (!dateInput) return "";
const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
return format(date, "HH:mm"); return format(date, "HH:mm");
}; };
/** Formats a date input to a time string in "HH:mm:ss" format. Returns an empty string if input is null or undefined. */
export const formatTimeWithSeconds = ( export const formatTimeWithSeconds = (
dateInput: string | Date | null | undefined dateInput: string | Date | null | undefined,
) => { ) => {
if (!dateInput) return ""; if (!dateInput) return "";
const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
return format(date, "HH:mm:ss"); return format(date, "HH:mm:ss");
}; };
/** Formats a date input to a human-readable date string.
* Returns "Today" or "Tomorrow" for those respective days,
* the weekday name (e.g. "Monday") if the date falls within the current week,
* or "MM/dd" otherwise. Returns an empty string if input is null or undefined. */
export const formatDate = (dateInput: string | Date | null | undefined) => { export const formatDate = (dateInput: string | Date | null | undefined) => {
if (!dateInput) return ""; if (!dateInput) return "";
const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
const now = new Date(); const now = new Date();
if ( if (
@@ -40,11 +51,15 @@ export const formatDate = (dateInput: string | Date | null | undefined) => {
if (isSameWeek(date, now, { weekStartsOn: 1 })) { if (isSameWeek(date, now, { weekStartsOn: 1 })) {
return format(date, "EEEE"); return format(date, "EEEE");
} }
return format(date, "MM/dd"); return format(date, "MM/dd");
}; };
/** Formats a date input to a "MM/dd/yyyy" string. Returns an empty string if input is null or undefined. */
export const formatDateMDY = (dateInput: string | Date | null | undefined) => { export const formatDateMDY = (dateInput: string | Date | null | undefined) => {
if (!dateInput) return ""; if (!dateInput) return "";
const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput;
const date = typeof dateInput === "string" ? new Date(dateInput) : dateInput;
return format(date, "MM/dd/yyyy"); return format(date, "MM/dd/yyyy");
}; };
+8 -8
View File
@@ -8,21 +8,21 @@ export enum LoginFailureReason {
UNKNOWN = "UNKNOWN", UNKNOWN = "UNKNOWN",
} }
export const reasonLabel = (reason?: LoginFailureReason | string | null) => { export const reasonLabel = (reason?: LoginFailureReason | string) => {
switch (reason) { switch (reason) {
case "MISSING_CREDENTIALS": case LoginFailureReason.MISSING_CREDENTIALS:
return "Missing credentials"; return "Missing credentials";
case "INVALID_CREDENTIALS": case LoginFailureReason.INVALID_CREDENTIALS:
return "Invalid credentials"; return "Invalid credentials";
case "WRONG_PASSWORD": case LoginFailureReason.WRONG_PASSWORD:
return "Wrong password"; return "Wrong password";
case "ACCOUNT_LOCKED": case LoginFailureReason.ACCOUNT_LOCKED:
return "Account locked"; return "Account locked";
case "NOT_ACTIVATED": case LoginFailureReason.NOT_ACTIVATED:
return "Account not activated"; return "Account not activated";
case "NOT_ALLOWED": case LoginFailureReason.NOT_ALLOWED:
return "Not allowed"; return "Not allowed";
case "UNKNOWN": case LoginFailureReason.UNKNOWN:
return "Unknown"; return "Unknown";
default: default:
return null; return null;