feat(config): alta de usuarios desde Configuraciones (POST /config/usuarios, solo admin)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState, type FormEvent } from 'react'
|
||||
import {
|
||||
crearUsuarioConfig,
|
||||
getUsuariosConfig,
|
||||
patchUsuarioConfig,
|
||||
type UsuarioConfig,
|
||||
@@ -49,6 +50,16 @@ export function Usuarios() {
|
||||
// feedback por usuario: { [usuarioId]: 'ok' | 'error' | null }
|
||||
const [feedback, setFeedback] = useState<Record<string, string>>({})
|
||||
|
||||
// ─── Alta de usuario ─────────────────────────────────────────────
|
||||
const [formAbierto, setFormAbierto] = useState(false)
|
||||
const [nombreNuevo, setNombreNuevo] = useState('')
|
||||
const [rolNuevo, setRolNuevo] = useState<string>('vendedor')
|
||||
const [asignacionNueva, setAsignacionNueva] = useState<string>('')
|
||||
const [passwordNuevo, setPasswordNuevo] = useState('')
|
||||
const [guardandoAlta, setGuardandoAlta] = useState(false)
|
||||
const [errorAlta, setErrorAlta] = useState('')
|
||||
const [exitoAlta, setExitoAlta] = useState('')
|
||||
|
||||
const cargar = useCallback(async () => {
|
||||
setCargando(true)
|
||||
setError('')
|
||||
@@ -118,6 +129,39 @@ export function Usuarios() {
|
||||
)
|
||||
})
|
||||
|
||||
const enviarAlta = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setErrorAlta('')
|
||||
setExitoAlta('')
|
||||
if (nombreNuevo.trim().length < 3) {
|
||||
setErrorAlta('Escribe el nombre completo del usuario.')
|
||||
return
|
||||
}
|
||||
if (passwordNuevo.length < 8) {
|
||||
setErrorAlta('La contraseña necesita al menos 8 caracteres.')
|
||||
return
|
||||
}
|
||||
setGuardandoAlta(true)
|
||||
try {
|
||||
const creado = await crearUsuarioConfig({
|
||||
nombre: nombreNuevo.trim(),
|
||||
rol: rolNuevo,
|
||||
asignacion: asignacionNueva || null,
|
||||
password: passwordNuevo,
|
||||
})
|
||||
setExitoAlta(`Usuario "${creado.username}" creado con rol ${ROL_LABELS[creado.rol] ?? creado.rol}.`)
|
||||
setNombreNuevo('')
|
||||
setRolNuevo('vendedor')
|
||||
setAsignacionNueva('')
|
||||
setPasswordNuevo('')
|
||||
await cargar()
|
||||
} catch (err) {
|
||||
setErrorAlta(err instanceof Error && err.message ? err.message : 'No se pudo crear el usuario.')
|
||||
} finally {
|
||||
setGuardandoAlta(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (cargando) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
@@ -132,6 +176,109 @@ export function Usuarios() {
|
||||
<p role="alert" className="text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Alta de usuario */}
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-[#111827] dark:text-[#E2E8F0]">
|
||||
Dar de alta usuario
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFormAbierto((v) => !v)
|
||||
setErrorAlta('')
|
||||
setExitoAlta('')
|
||||
}}
|
||||
className="rounded-lg bg-accent px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||
>
|
||||
{formAbierto ? 'Cerrar' : '+ Nuevo usuario'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{formAbierto && (
|
||||
<form onSubmit={enviarAlta} className="mt-4 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label htmlFor="alta-nombre" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
Nombre completo *
|
||||
</label>
|
||||
<input
|
||||
id="alta-nombre"
|
||||
type="text"
|
||||
value={nombreNuevo}
|
||||
onChange={(e) => setNombreNuevo(e.target.value)}
|
||||
placeholder="P. ej. María López García"
|
||||
className="mt-1.5 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||
/>
|
||||
<p className="mt-1 text-[11px] text-[#64748B] dark:text-[#94A3B8]">
|
||||
El usuario de acceso se genera del nombre (minúsculas, sin acentos).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="alta-password" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
Contraseña *
|
||||
</label>
|
||||
<input
|
||||
id="alta-password"
|
||||
type="password"
|
||||
value={passwordNuevo}
|
||||
onChange={(e) => setPasswordNuevo(e.target.value)}
|
||||
placeholder="Mínimo 8 caracteres"
|
||||
className="mt-1.5 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="alta-rol" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
Rol *
|
||||
</label>
|
||||
<select
|
||||
id="alta-rol"
|
||||
value={rolNuevo}
|
||||
onChange={(e) => setRolNuevo(e.target.value)}
|
||||
className="mt-1.5 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>{ROL_LABELS[r]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="alta-asignacion" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
Asignación
|
||||
</label>
|
||||
<select
|
||||
id="alta-asignacion"
|
||||
value={asignacionNueva}
|
||||
onChange={(e) => setAsignacionNueva(e.target.value)}
|
||||
className="mt-1.5 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||
>
|
||||
{ASIGNACIONES.map((a) => (
|
||||
<option key={a} value={a}>{ASIGNACION_LABELS[a]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={guardandoAlta}
|
||||
className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||
>
|
||||
{guardandoAlta ? 'Creando…' : 'Crear usuario'}
|
||||
</button>
|
||||
{exitoAlta && (
|
||||
<p role="status" className="text-sm text-green-600">{exitoAlta}</p>
|
||||
)}
|
||||
{errorAlta && (
|
||||
<p role="alert" className="text-sm text-red-600">{errorAlta}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Búsqueda */}
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
<label htmlFor="buscar-usuario" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
|
||||
@@ -108,6 +108,29 @@ export async function patchUsuarioConfig(
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
}
|
||||
|
||||
/** POST /config/usuarios — dar de alta un usuario (solo admin) */
|
||||
export async function crearUsuarioConfig(data: {
|
||||
nombre: string
|
||||
rol: string
|
||||
asignacion: string | null
|
||||
password: string
|
||||
}): Promise<UsuarioConfig> {
|
||||
const resp = await fetch(
|
||||
`${API_URL}/config/usuarios`,
|
||||
conAuth({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
)
|
||||
manejar401(resp)
|
||||
if (!resp.ok) {
|
||||
const cuerpo = (await resp.json().catch(() => null)) as { error?: string } | null
|
||||
throw new Error(cuerpo?.error ?? `HTTP ${resp.status}`)
|
||||
}
|
||||
return (await resp.json()) as UsuarioConfig
|
||||
}
|
||||
|
||||
// ─── Auditoría ─────────────────────────────────────────────────
|
||||
|
||||
export interface AuditoriaItem {
|
||||
|
||||
Reference in New Issue
Block a user