265 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|