initial commit
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
Generated
+6089
-3802
File diff suppressed because it is too large
Load Diff
+60
-54
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user