project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
+349
View File
@@ -0,0 +1,349 @@
import { useEffect, useMemo, useRef } from "react";
import { View, Text, ScrollView, Dimensions } from "react-native";
import { Dialog, TextInput, IconButton } from "react-native-paper";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { User } from "@/store/models/User.model";
import { RoleEnum } from "@/constants/roleEnum";
import { RiskEnum } from "@/constants/riskEnum";
import { OperationMode } from "@/constants/operationModeEnum";
import { DialogContainer } from "@/components/ui/DialogContainer";
import { useAuthContext } from "@/auth/AuthContext";
import { useGetUsersQuery } from "@/store/api/usersApi";
import { CustomPicker, PickerItem } from "@/components/ui/CustomPicker";
import {
adminSchema,
AdminFormValues,
} from "@/components/features/admins/schemas/admin.schema";
interface AdminModalProps {
visible: boolean;
mode?: OperationMode;
admin?: User | null;
isLoading: boolean;
onClose: () => void;
onSubmit: (payload: Partial<User>) => void;
}
export const AdminModal = ({
visible,
mode = OperationMode.ADD,
admin = null,
isLoading,
onClose,
onSubmit,
}: AdminModalProps) => {
const isEdit = mode === OperationMode.EDIT;
const { user: currentUser } = useAuthContext();
const { data: adminsData } = useGetUsersQuery(
{
roleType: "admin",
limit: 1000,
},
{ skip: !visible }
);
const managerOptions = useMemo(
() => (adminsData?.items || []).filter((u) => u.role === RoleEnum.MANAGER),
[adminsData]
);
const adminRef = useRef<User | null>(admin);
adminRef.current = admin;
const currentUserRef = useRef(currentUser);
currentUserRef.current = currentUser;
const defaultValues: AdminFormValues = {
id: admin?.id,
email: admin?.email ?? "",
firstName: admin?.firstName ?? "",
lastName: admin?.lastName ?? "",
title: admin?.title ?? "",
role: admin?.role ?? RoleEnum.ADMIN,
riskStatus: admin?.riskStatus ?? RiskEnum.LOW_RISK,
managerId:
admin?.managerId ??
(currentUser?.role === RoleEnum.MANAGER ? currentUser.id : undefined),
};
const {
control,
handleSubmit,
reset,
watch,
formState: { errors, isDirty, isValid },
} = useForm<AdminFormValues>({
resolver: zodResolver(adminSchema),
defaultValues,
mode: "onChange",
shouldUnregister: false,
});
useEffect(() => {
if (!visible) return;
const nextDefaults: AdminFormValues = {
id: adminRef.current?.id,
email: adminRef.current?.email ?? "",
firstName: adminRef.current?.firstName ?? "",
lastName: adminRef.current?.lastName ?? "",
title: adminRef.current?.title ?? "",
role: adminRef.current?.role ?? RoleEnum.ADMIN,
riskStatus: adminRef.current?.riskStatus ?? RiskEnum.LOW_RISK,
managerId:
adminRef.current?.managerId ??
(currentUserRef.current?.role === RoleEnum.MANAGER
? currentUserRef.current?.id
: undefined),
};
reset(nextDefaults, { keepDefaultValues: false });
}, [visible, reset, admin?.id, currentUser?.id, currentUser?.role]);
const windowHeight = Dimensions.get("window").height;
const dialogVerticalMargin = 24;
const dialogMaxHeight = windowHeight - dialogVerticalMargin - 60;
const approxChrome = 140;
const maxDialogContentHeight = Math.max(200, dialogMaxHeight - approxChrome);
const selectedRole = watch("role");
const submit = (vals: AdminFormValues) => {
const payload: Partial<User> = {
id: vals.id,
email: vals.email.trim().toLowerCase(),
firstName: vals.firstName.trim(),
lastName: vals.lastName.trim(),
title: vals.title.trim(),
role: vals.role || RoleEnum.ADMIN,
riskStatus: vals.riskStatus || RiskEnum.LOW_RISK,
// Only Managers can assign Admins to a Manager (Managers themselves cannot be reassigned)
...(currentUser?.role === RoleEnum.MANAGER && vals.role === RoleEnum.ADMIN
? { managerId: vals.managerId ?? undefined }
: {}),
};
onSubmit(payload);
};
const titleText = isEdit ? "Edit Admin" : "Add Admin";
const submitText = isEdit ? "Edit" : "Add";
return (
<DialogContainer
visible={visible}
onDismiss={onClose}
dialogStyle={{
alignSelf: "center",
marginVertical: dialogVerticalMargin,
maxHeight: dialogMaxHeight,
}}
>
<Dialog.Title>
<View className="w-full flex-row items-center justify-between">
<View className="flex-1">
<Text className="pr-3">{titleText}</Text>
</View>
<View style={{ alignSelf: "flex-start" }}>
<IconButton
icon="close"
size={20}
onPress={onClose}
disabled={isLoading}
accessibilityLabel="Close dialog"
style={{ margin: 0 }}
/>
</View>
</View>
</Dialog.Title>
<Dialog.Content>
<ScrollView
style={{ maxHeight: maxDialogContentHeight }}
contentContainerStyle={{ paddingBottom: 16 }}
>
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">Email</Text>
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<>
<TextInput
className={`border ${
errors.email ? "border-highRisk" : "border-gray-300"
} rounded p-2 text-base bg-white`}
value={value}
onChangeText={onChange}
placeholder="Enter email address"
keyboardType="email-address"
autoCapitalize="none"
placeholderTextColor="#d9d9d9"
/>
{errors.email ? (
<Text className="text-highRisk text-xs mt-1">
{errors.email.message as string}
</Text>
) : null}
</>
)}
/>
</View>
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">First Name</Text>
<Controller
control={control}
name="firstName"
render={({ field: { onChange, value } }) => (
<>
<TextInput
className={`border ${
errors.firstName ? "border-highRisk" : "border-gray-300"
} rounded p-2 text-base bg-white`}
value={value}
onChangeText={onChange}
placeholder="Enter first name"
autoCapitalize="words"
placeholderTextColor="#d9d9d9"
/>
{errors.firstName ? (
<Text className="text-highRisk text-xs mt-1">
{errors.firstName.message as string}
</Text>
) : null}
</>
)}
/>
</View>
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">Last Name</Text>
<Controller
control={control}
name="lastName"
render={({ field: { onChange, value } }) => (
<>
<TextInput
className={`border ${
errors.lastName ? "border-highRisk" : "border-gray-300"
} rounded p-2 text-base bg-white`}
value={value}
onChangeText={onChange}
placeholder="Enter last name"
autoCapitalize="words"
placeholderTextColor="#d9d9d9"
/>
{errors.lastName ? (
<Text className="text-highRisk text-xs mt-1">
{errors.lastName.message as string}
</Text>
) : null}
</>
)}
/>
</View>
{/* Password field removed for invitation-only flow */}
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">Title</Text>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<>
<TextInput
className={`border ${
errors.title ? "border-highRisk" : "border-gray-300"
} rounded p-2 text-base bg-white`}
value={value}
onChangeText={onChange}
placeholder="Enter job title"
autoCapitalize="words"
placeholderTextColor="#d9d9d9"
/>
{errors.title ? (
<Text className="text-highRisk text-xs mt-1">
{errors.title.message as string}
</Text>
) : null}
</>
)}
/>
</View>
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">Role</Text>
<View className="border border-gray-300 rounded overflow-hidden">
<Controller
control={control}
name="role"
render={({ field: { onChange, value } }) => (
<CustomPicker
selectedValue={value}
onValueChange={(v) => onChange(v)}
>
<PickerItem label="Admin" value={RoleEnum.ADMIN} />
<PickerItem label="Manager" value={RoleEnum.MANAGER} />
</CustomPicker>
)}
/>
</View>
</View>
{currentUser?.role === RoleEnum.MANAGER &&
selectedRole === RoleEnum.ADMIN ? (
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">
Assign to Manager
</Text>
<View className="border border-gray-300 rounded overflow-hidden">
<Controller
control={control}
name="managerId"
render={({ field: { onChange, value } }) => (
<CustomPicker
selectedValue={value ?? undefined}
onValueChange={(v) => onChange(v)}
>
{managerOptions.map((m) => (
<PickerItem
key={m.id}
label={
`${m.firstName} ${m.lastName}`.trim() || m.email
}
value={m.id}
/>
))}
</CustomPicker>
)}
/>
</View>
</View>
) : null}
</ScrollView>
</Dialog.Content>
<Dialog.Actions>
<Button
onPress={handleSubmit(submit)}
disabled={isLoading || !isValid || (isEdit && !isDirty)}
className="!ml-2"
>
{submitText}
</Button>
</Dialog.Actions>
{isLoading && (
<View
className="absolute inset-0 bg-gray-500/30 flex items-center justify-center"
pointerEvents="auto"
style={{ zIndex: 1000, top: 0, right: 0, bottom: 0, left: 0 }}
>
<LoadingSpinner size={16} />
</View>
)}
</DialogContainer>
);
};