Files
AthenaFE/components/features/users/UserModal.tsx
T
2025-11-02 10:08:56 +01:00

287 lines
9.3 KiB
TypeScript

import { useEffect, 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 { DialogContainer } from "@/components/ui/DialogContainer";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { User } from "@/store/models/User.model";
import { RiskEnum, RiskLabels } from "@/constants/riskEnum";
import { RoleEnum } from "@/constants/roleEnum";
import { OperationMode } from "@/constants/operationModeEnum";
import { CustomPicker, PickerItem } from "@/components/ui/CustomPicker";
import {
userSchemaWithId,
UserFormValues,
} from "@/components/features/users/schemas/user.schema";
interface UserModalProps {
visible: boolean;
mode?: OperationMode;
user?: User | null;
isLoading?: boolean;
onClose: () => void;
onSubmit: (payload: Partial<User>) => void | Promise<void>;
}
export const UserModal = ({
visible,
mode = OperationMode.ADD,
user = null,
isLoading = false,
onClose,
onSubmit,
}: UserModalProps) => {
const isEdit = mode === OperationMode.EDIT;
const defaultValues: UserFormValues = {
id: user?.id,
firstName: user?.firstName ?? "",
lastName: user?.lastName ?? "",
email: user?.email ?? "",
title: user?.title ?? "",
riskStatus: user?.riskStatus ?? RiskEnum.LOW_RISK,
};
const {
control,
handleSubmit,
reset,
formState: { errors, isDirty },
} = useForm<UserFormValues>({
resolver: zodResolver(userSchemaWithId),
defaultValues,
mode: "onChange",
shouldUnregister: false,
});
const userRef = useRef<User | null>(user);
userRef.current = user;
useEffect(() => {
if (!visible) return;
const nextDefaults: UserFormValues = {
id: userRef.current?.id,
firstName: userRef.current?.firstName ?? "",
lastName: userRef.current?.lastName ?? "",
email: userRef.current?.email ?? "",
title: userRef.current?.title ?? "",
riskStatus: userRef.current?.riskStatus ?? RiskEnum.LOW_RISK,
};
reset(nextDefaults, { keepDefaultValues: false });
}, [visible, reset, user?.id]);
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 submit = (vals: UserFormValues) => {
const payload: Partial<User> = {
id: vals.id,
firstName: vals.firstName.trim(),
lastName: vals.lastName.trim(),
email: vals.email.trim().toLowerCase(),
title: vals.title.trim(),
riskStatus: vals.riskStatus,
role: isEdit ? user?.role ?? RoleEnum.USER : RoleEnum.USER,
};
return onSubmit(payload);
};
const titleText = isEdit ? "Edit User" : "Add User";
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 }}
showsVerticalScrollIndicator
>
<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"
placeholderTextColor="#d9d9d9"
disabled={isLoading}
/>
{errors.firstName ? (
<Text className="text-highRisk text-xs mt-1">
{errors.firstName.message}
</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"
placeholderTextColor="#d9d9d9"
disabled={isLoading}
/>
{errors.lastName ? (
<Text className="text-highRisk text-xs mt-1">
{errors.lastName.message}
</Text>
) : null}
</>
)}
/>
</View>
<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"
keyboardType="email-address"
placeholderTextColor="#d9d9d9"
autoCapitalize="none"
disabled={isLoading}
/>
{errors.email ? (
<Text className="text-highRisk text-xs mt-1">
{errors.email.message}
</Text>
) : null}
</>
)}
/>
</View>
<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 title"
placeholderTextColor="#d9d9d9"
disabled={isLoading}
/>
{errors.title ? (
<Text className="text-highRisk text-xs mt-1">
{errors.title.message}
</Text>
) : null}
</>
)}
/>
</View>
<View className="mb-4">
<Text className="text-sm mb-1 font-medium">Risk Status</Text>
<Controller
control={control}
name="riskStatus"
render={({ field: { onChange, value } }) => (
<View className="border border-gray-300 rounded overflow-hidden">
<CustomPicker
selectedValue={value}
onValueChange={(val) => onChange(val)}
className="h-10"
enabled={!isLoading}
>
{Object.values(RiskEnum).map((v) => (
<PickerItem key={v} label={RiskLabels[v]} value={v} />
))}
</CustomPicker>
</View>
)}
/>
</View>
</ScrollView>
</Dialog.Content>
<Dialog.Actions>
<Button
onPress={handleSubmit(submit)}
disabled={isLoading || (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>
);
};