diff --git a/package.json b/package.json index 2ebb792..9d83741 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "postinstall": "prisma generate", "dev": "next dev --turbopack", - "build": "next build --turbopack", + "build": "prisma db push && next build --turbopack", "start": "next start", "test": "vitest", "test:ui": "vitest --ui", @@ -59,4 +59,4 @@ "vite-tsconfig-paths": "^6.1.1", "vitest": "^4.0.18" } -} +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7243efd..083e1e0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model Usuario { ultimoAcceso DateTime? @map("ultimo_acceso") intentosFallidos Int @default(0) @map("intentos_fallidos") bloqueadoHasta DateTime? @map("bloqueado_hasta") + motivoInactivo String? @map("motivo_inactivo") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") permisos Permiso[] diff --git a/src/app/api/usuarios/[id]/route.ts b/src/app/api/usuarios/[id]/route.ts index 6908fae..234daf4 100644 --- a/src/app/api/usuarios/[id]/route.ts +++ b/src/app/api/usuarios/[id]/route.ts @@ -69,9 +69,30 @@ export async function DELETE( message: "Usuario eliminado permanentemente", }); } else { + let motivo = ""; + try { + const body = await request.json(); + if (body.motivoInactivo !== undefined && typeof body.motivoInactivo !== "string") { + return NextResponse.json( + { success: false, message: "motivoInactivo debe ser una cadena de texto" }, + { status: 400 } + ); + } + motivo = body.motivoInactivo || ""; + } catch (e) { + // Ignorar si no hay body + } + + if (!motivo || motivo.trim() === '') { + return NextResponse.json( + { success: false, message: "El motivo de inactivación es requerido" }, + { status: 400 } + ); + } + await prisma.usuario.update({ where: { id }, - data: { estado: 0 }, + data: { estado: 0, motivoInactivo: motivo.trim() }, }); return NextResponse.json({ success: true, @@ -86,3 +107,119 @@ export async function DELETE( ); } } + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const caller = getAuthenticatedUser(request); + if (!caller) { + return NextResponse.json( + { success: false, message: "No autenticado" }, + { status: 401 } + ); + } + + if (caller.rol !== "admin") { + return NextResponse.json( + { success: false, message: "No tienes permisos" }, + { status: 403 } + ); + } + + const { id: idParam } = await params; + const id = parseInt(idParam, 10); + + if (isNaN(id)) { + return NextResponse.json( + { success: false, message: "ID de usuario inválido" }, + { status: 400 } + ); + } + + const body = await request.json(); + const { estado, motivoInactivo } = body; + + if (estado !== undefined && typeof estado !== "number") { + return NextResponse.json( + { success: false, message: "El estado debe ser un número (0 o 1)" }, + { status: 400 } + ); + } + + if (estado !== undefined && estado !== 0 && estado !== 1) { + return NextResponse.json( + { success: false, message: "Estado no soportado" }, + { status: 400 } + ); + } + + if (motivoInactivo !== undefined && typeof motivoInactivo !== "string") { + return NextResponse.json( + { success: false, message: "motivoInactivo debe ser una cadena de texto" }, + { status: 400 } + ); + } + + const dataToUpdate: any = {}; + if (estado !== undefined) { + dataToUpdate.estado = estado; + if (estado === 0) { + if (!motivoInactivo || motivoInactivo.trim() === "") { + return NextResponse.json( + { success: false, message: "El motivo de inactivación es requerido" }, + { status: 400 } + ); + } + } + } + + if (motivoInactivo !== undefined) { + dataToUpdate.motivoInactivo = motivoInactivo.trim(); + } + + if (estado === 1) { + dataToUpdate.motivoInactivo = null; + } + + const usuario = await prisma.usuario.update({ + where: { id }, + data: dataToUpdate, + select: { + id: true, + nombreCompleto: true, + username: true, + email: true, + rol: true, + estado: true, + ultimoAcceso: true, + intentosFallidos: true, + bloqueadoHasta: true, + motivoInactivo: true, + createdAt: true, + updatedAt: true, + } + }); + + return NextResponse.json({ + success: true, + message: "Usuario actualizado correctamente", + data: usuario, + }); + } catch (error: any) { + console.error("Error al actualizar usuario:", error); + + if (error?.code === 'P2025') { + return NextResponse.json( + { success: false, message: "Usuario no encontrado" }, + { status: 404 } + ); + } + + return NextResponse.json( + { success: false, message: "Error interno del servidor" }, + { status: 500 } + ); + } +} diff --git a/src/app/gestion-usuarios/gestion-usuarios.tsx b/src/app/gestion-usuarios/gestion-usuarios.tsx index 0ec91e1..6a1958d 100644 --- a/src/app/gestion-usuarios/gestion-usuarios.tsx +++ b/src/app/gestion-usuarios/gestion-usuarios.tsx @@ -16,6 +16,7 @@ type UsuarioRow = { email: string; rol: string; estado: number; + motivoInactivo?: string | null; }; type RolDef = { key: string; label: string; esBase: boolean }; @@ -201,10 +202,9 @@ export default function GestionUsuariosPage() { denyButtonText: 'Desactivar (Soft Delete)', cancelButtonText: 'Cancelar' }).then(async (result) => { - if (result.isConfirmed || result.isDenied) { - const isHardDelete = result.isConfirmed; + if (result.isConfirmed) { try { - const res = await fetch(`/api/usuarios/${id}?hardDelete=${isHardDelete}`, { + const res = await fetch(`/api/usuarios/${id}?hardDelete=true`, { method: "DELETE" }); const json = await res.json(); @@ -217,6 +217,70 @@ export default function GestionUsuariosPage() { } catch (error) { Swal.fire('Error', 'No se pudo conectar con el servidor', 'error'); } + } else if (result.isDenied) { + const { value: motivo } = await Swal.fire({ + title: 'Motivo de inactivación', + input: 'text', + inputLabel: 'Ingresa el motivo (ej. Ya no pertenece a la iglesia)', + inputPlaceholder: 'Escribe el motivo aquí...', + showCancelButton: true, + inputValidator: (value) => { + if (!value || !value.trim()) { + return '¡Debes escribir un motivo!'; + } + } + }); + + if (motivo) { + try { + const res = await fetch(`/api/usuarios/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ estado: 0, motivoInactivo: motivo }) + }); + const json = await res.json(); + if (res.ok && json.success) { + Swal.fire('¡Desactivado!', 'El usuario ha sido desactivado correctamente.', 'success'); + cargarUsuarios(); + } else { + Swal.fire('Error', json.message || 'Error al desactivar el usuario', 'error'); + } + } catch (error) { + Swal.fire('Error', 'No se pudo conectar con el servidor', 'error'); + } + } + } + }); + }; + + const handleReactivar = (id: number, nombre: string) => { + Swal.fire({ + title: 'Reactivar Usuario', + text: `¿Estás seguro de reactivar a ${nombre}?`, + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#16a34a', + cancelButtonColor: '#6b7280', + confirmButtonText: 'Sí, reactivar', + cancelButtonText: 'Cancelar' + }).then(async (result) => { + if (result.isConfirmed) { + try { + const res = await fetch(`/api/usuarios/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ estado: 1 }) + }); + const json = await res.json(); + if (res.ok && json.success) { + Swal.fire('¡Activado!', 'El usuario ha sido reactivado correctamente.', 'success'); + cargarUsuarios(); + } else { + Swal.fire('Error', json.message || 'Error al reactivar el usuario', 'error'); + } + } catch (error) { + Swal.fire('Error', 'No se pudo conectar con el servidor', 'error'); + } } }); }; @@ -439,15 +503,33 @@ export default function GestionUsuariosPage() { {u.estado === 1 ? "Activo" : "Inactivo"} + {u.estado === 0 && u.motivoInactivo && ( +
+ {u.motivoInactivo} +
+ )}{u.email}
+{u.email}
+ {u.estado === 0 && u.motivoInactivo && ( +Motivo: {u.motivoInactivo}
+ )} {roles.find(r => r.key === u.rol)?.label ?? u.rol} diff --git a/src/dao/usuario.dao.ts b/src/dao/usuario.dao.ts index 4435cf0..ecbf731 100644 --- a/src/dao/usuario.dao.ts +++ b/src/dao/usuario.dao.ts @@ -13,6 +13,7 @@ function mapToUsuario(row: any): Usuario { ultimoAcceso: row.ultimoAcceso, intentosFallidos: row.intentosFallidos, bloqueadoHasta: row.bloqueadoHasta, + motivoInactivo: row.motivoInactivo, createdAt: row.createdAt, updatedAt: row.updatedAt, }; @@ -31,6 +32,7 @@ export class UsuarioDAO { ultimoAcceso: true, intentosFallidos: true, bloqueadoHasta: true, + motivoInactivo: true, createdAt: true, updatedAt: true, }, diff --git a/src/models/Usuario.ts b/src/models/Usuario.ts index 3b8286f..6ae61c3 100644 --- a/src/models/Usuario.ts +++ b/src/models/Usuario.ts @@ -10,6 +10,7 @@ export interface Usuario { ultimoAcceso: Date | null; intentosFallidos: number; bloqueadoHasta: Date | null; + motivoInactivo?: string | null; createdAt: Date; updatedAt: Date; }