Files
aeroresolve_frontend/src/app/users/components/UserModal.tsx
T

265 lines
7.4 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { UsersApi, type UserItem } from '../UsersApi';
import { RolesApi, type RoleItem } from '../../roles/RolesApi';
import {
CustomModal,
CustomInput,
CustomDropdown,
CustomSwitch,
CustomButton,
CustomAlertBanner,
} from '../../../components/custom';
import {
UserIcon,
EnvelopeSimpleIcon,
LockKeyIcon,
ShieldCheckIcon,
FloppyDiskIcon,
} from '@phosphor-icons/react';
interface UserModalProps {
userToEdit?: UserItem | null;
isOpen: boolean;
onClose: () => void;
onSaved: () => void;
}
export default function UserModal({
userToEdit,
isOpen,
onClose,
onSaved,
}: UserModalProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [roleId, setRoleId] = useState('');
const [isActive, setIsActive] = useState(true);
const [roles, setRoles] = useState<RoleItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
loadRoles();
if (userToEdit) {
setEmail(userToEdit.email);
setFirstName(userToEdit.firstName);
setLastName(userToEdit.lastName || '');
setRoleId(userToEdit.roleId);
setIsActive(userToEdit.isActive);
setPassword('');
} else {
setEmail('');
setPassword('');
setFirstName('');
setLastName('');
setRoleId('');
setIsActive(true);
}
setError(null);
}
}, [isOpen, userToEdit]);
const loadRoles = async () => {
try {
const data = await RolesApi.getRoles();
setRoles(data);
if (!userToEdit && data.length > 0 && !roleId) {
setRoleId(data[0].id);
}
} catch (e) {
console.error('Failed to load roles in user modal:', e);
}
};
const isSystemAdmin = userToEdit?.isSystem ?? false;
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!firstName.trim()) {
setError('First name is required.');
return;
}
if (!userToEdit && !email.trim()) {
setError('Email address is required.');
return;
}
if (!userToEdit && (!password || password.length < 8)) {
setError('Password must be at least 8 characters long.');
return;
}
if (!roleId) {
setError('Please assign an operational role.');
return;
}
setLoading(true);
try {
if (userToEdit) {
await UsersApi.updateUser(userToEdit.id, {
firstName: firstName.trim(),
lastName: lastName.trim() || undefined,
roleId,
isActive,
});
} else {
await UsersApi.createUser({
email: email.trim().toLowerCase(),
password,
firstName: firstName.trim(),
lastName: lastName.trim() || undefined,
roleId,
isActive,
});
}
onSaved();
} catch (err: any) {
setError(err?.response?.data?.message || 'Failed to save user account.');
} finally {
setLoading(false);
}
};
const roleOptions = roles.map((r) => ({
label: `${r.name}${r.isSystem ? ' (System Default)' : ''}`,
value: r.id,
}));
return (
<CustomModal
isOpen={isOpen}
onClose={onClose}
title={userToEdit ? 'Edit User Profile' : 'Provision New User'}
description={
userToEdit
? 'Update account information and assigned operational clearance.'
: 'Provision a new team member with specific role credentials.'
}
size="md"
footer={
<div className="flex items-center justify-end gap-3 w-full">
<CustomButton
variant="outlined"
size="md"
onClick={onClose}
className="!rounded-[10px] !h-[40px]"
>
Cancel
</CustomButton>
<CustomButton
variant="primary"
size="md"
leftIcon={<FloppyDiskIcon size={16} />}
loading={loading}
disabled={loading}
onClick={handleSubmit}
className="!rounded-[10px] !h-[40px] !bg-[#1B9869] hover:!bg-[#14704E]"
>
{userToEdit ? 'Save Changes' : 'Provision User'}
</CustomButton>
</div>
}
>
<div className="space-y-4 py-1">
{error && (
<CustomAlertBanner
message={error}
type="error"
onClose={() => setError(null)}
/>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<CustomInput
label="First Name"
required
placeholder="e.g. John"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
leftIcon={<UserIcon size={18} />}
className="!h-[42px] !rounded-[10px]"
/>
<CustomInput
label="Last Name"
placeholder="e.g. Doe"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
leftIcon={<UserIcon size={18} />}
className="!h-[42px] !rounded-[10px]"
/>
</div>
<CustomInput
label="Email Address"
required
type="email"
disabled={!!userToEdit}
placeholder="e.g. john.doe@aeroresolve.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
leftIcon={<EnvelopeSimpleIcon size={18} />}
className="!h-[42px] !rounded-[10px]"
/>
{!userToEdit && (
<CustomInput
label="Temporary Password"
required
type="password"
placeholder="At least 8 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
leftIcon={<LockKeyIcon size={18} />}
className="!h-[42px] !rounded-[10px]"
/>
)}
<div className="space-y-1">
<CustomDropdown
label="Operational Role"
required
disabled={isSystemAdmin}
options={roleOptions}
value={roleId}
onChange={(val) => setRoleId(val)}
placeholder="Select Operational Role..."
leftIcon={<ShieldCheckIcon size={18} />}
size="md"
className="!h-[42px] !rounded-[10px]"
/>
</div>
<div className="flex items-center justify-between p-3.5 bg-gray-50/70 border border-gray-100 rounded-[12px] mt-2">
<div>
<span className="text-[13px] font-semibold text-gray-900 block">
Account Status
</span>
<span className="text-[12px] text-gray-500 font-medium">
{isActive
? 'User can authenticate and access authorized workflows.'
: 'Account is deactivated and prohibited from signing in.'}
</span>
</div>
<CustomSwitch
disabled={isSystemAdmin}
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
/>
</div>
</form>
</div>
</CustomModal>
);
}