92 lines
6.1 KiB
TypeScript
92 lines
6.1 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import type { FormEvent } from 'react';
|
|
import { Link, useNavigate } from 'react-router';
|
|
import { Alert, LoadingBlock, errorMessage } from '../components/Feedback';
|
|
import { Icon } from '../components/Icon';
|
|
import { listRoles } from '../lib/api';
|
|
import type { AdministrativeRole } from '../lib/api';
|
|
import { createUserProfile } from '../lib/userProfileApi';
|
|
|
|
export function NewUserPage() {
|
|
const navigate = useNavigate();
|
|
const [roles, setRoles] = useState<AdministrativeRole[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [roleIds, setRoleIds] = useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
listRoles().then(setRoles).catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const inspectorSelected = useMemo(
|
|
() => roles.some((role) => role.code === 'inspector' && roleIds.includes(role.id)),
|
|
[roles, roleIds],
|
|
);
|
|
const toggleRole = (id: string) => setRoleIds((current) => current.includes(id) ? current.filter((value) => value !== id) : [...current, id]);
|
|
|
|
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
setError('');
|
|
const data = new FormData(event.currentTarget);
|
|
const email = String(data.get('email') ?? '').trim();
|
|
if (inspectorSelected && !email) {
|
|
setError('El email es obligatorio para usuarios con rol Inspector porque allí recibirán la documentación de sus inspecciones.');
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
try {
|
|
const created = await createUserProfile({
|
|
username: String(data.get('username')),
|
|
email: email || null,
|
|
dni: String(data.get('dni') ?? '').trim() || null,
|
|
phone: String(data.get('phone') ?? '').trim() || null,
|
|
jobTitle: String(data.get('jobTitle') ?? '').trim() || null,
|
|
employeeNumber: String(data.get('employeeNumber') ?? '').trim() || null,
|
|
firstName: String(data.get('firstName')),
|
|
lastName: String(data.get('lastName')),
|
|
password: String(data.get('password')),
|
|
mustChangePassword: data.get('mustChangePassword') === 'on',
|
|
roleIds,
|
|
});
|
|
navigate(`/admin/users/${created.id}`, { replace: true, state: { created: true } });
|
|
} catch (requestError) {
|
|
setError(errorMessage(requestError));
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return <section className="narrow-section">
|
|
<div className="breadcrumb"><Link to="/admin/users">Usuarios</Link><span>/</span><strong>Nuevo usuario</strong></div>
|
|
<div className="page-heading"><div><span className="eyebrow">NUEVO ACCESO</span><h1>Crear usuario</h1><p>Además del acceso, registrá los datos personales que identifican al agente y permiten las comunicaciones oficiales.</p></div></div>
|
|
{error && <Alert>{error}</Alert>}
|
|
{loading ? <LoadingBlock /> : <form className="panel form-panel" onSubmit={submit}>
|
|
<div className="form-section">
|
|
<div><h2>Datos personales</h2><p className="section-copy">Nombre y apellido identifican al agente. DNI, contacto y función quedan asociados a su historial operativo.</p></div>
|
|
<div className="form-grid">
|
|
<label className="field"><span>Nombre <em>obligatorio</em></span><input name="firstName" required maxLength={120} /></label>
|
|
<label className="field"><span>Apellido <em>obligatorio</em></span><input name="lastName" required maxLength={120} /></label>
|
|
<label className="field"><span>DNI</span><input name="dni" inputMode="numeric" maxLength={15} placeholder="Sin puntos" /></label>
|
|
<label className="field"><span>Teléfono</span><input name="phone" type="tel" maxLength={40} placeholder="Ej.: +54 261 ..." /></label>
|
|
<label className="field"><span>Cargo / función</span><input name="jobTitle" maxLength={160} placeholder="Ej.: Inspector de Hidrocarburos" /></label>
|
|
<label className="field"><span>Legajo / matrícula</span><input name="employeeNumber" maxLength={80} /></label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="form-section">
|
|
<div><h2>Acceso y contacto</h2><p className="section-copy">El email del Inspector se utiliza también como destinatario de la documentación al cerrar la inspección.</p></div>
|
|
<div className="form-grid">
|
|
<label className="field"><span>Usuario <em>obligatorio</em></span><input name="username" required minLength={3} maxLength={80} pattern="[a-zA-Z][a-zA-Z0-9._-]+" /></label>
|
|
<label className="field"><span>Email {inspectorSelected ? <em>obligatorio para Inspector</em> : <em>recomendado</em>}</span><input name="email" type="email" maxLength={320} required={inspectorSelected} /></label>
|
|
</div>
|
|
{inspectorSelected && <div className="temporal-notice"><Icon name="mail" /><p><strong>Inspector:</strong> este email recibirá copia de las Actas/Informe correspondientes al cierre de la inspección.</p></div>}
|
|
</div>
|
|
|
|
<div className="form-section"><h2>Seguridad</h2><label className="field"><span>Contraseña temporal</span><input name="password" type="password" minLength={12} maxLength={128} required autoComplete="new-password" /><small>Mínimo 12 caracteres.</small></label><label className="check-row"><input name="mustChangePassword" type="checkbox" defaultChecked /><span><strong>Exigir cambio de contraseña</strong><small>El usuario no podrá acceder a otros módulos hasta actualizarla.</small></span></label></div>
|
|
<div className="form-section"><h2>Roles</h2><div className="choice-grid">{roles.map((role) => <label className={`choice-card ${roleIds.includes(role.id) ? 'selected' : ''}`} key={role.id}><input type="checkbox" checked={roleIds.includes(role.id)} onChange={() => toggleRole(role.id)} /><span><strong>{role.name}</strong><small>{role.description}</small></span><Icon name="check" /></label>)}</div></div>
|
|
<div className="form-actions"><Link className="button secondary" to="/admin/users">Cancelar</Link><button className="button primary" disabled={submitting}>{submitting ? 'Creando…' : 'Crear usuario'}</button></div>
|
|
</form>}
|
|
</section>;
|
|
}
|