287 lines
9.3 KiB
TypeScript
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>
|
|
);
|
|
};
|