From c1f81171ddaf34064b65137ede743058508ca98b Mon Sep 17 00:00:00 2001 From: IsaacAburto1548 Date: Sat, 27 Jun 2026 12:04:32 -0600 Subject: [PATCH 1/3] activar usuario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ahora, al darle al botón de eliminar y seleccionar "Desactivar (Soft Delete)" en el sistema de gestión de usuarios, pedirá de forma obligatoria el motivo (ej. "ya no pertenece a la iglesia"). El motivo aparecerá visualmente bajo la etiqueta "Inactivo" en la lista de usuarios. Aparecerá un nuevo botón verde (✓) para los usuarios inactivos que permite reactivarlos instantáneamente. --- package.json | 4 +- prisma/schema.prisma | 1 + src/app/api/usuarios/[id]/route.ts | 58 ++++++++ src/app/gestion-usuarios/gestion-usuarios.tsx | 130 +++++++++++++++--- src/dao/usuario.dao.ts | 2 + src/models/Usuario.ts | 1 + 6 files changed, 176 insertions(+), 20 deletions(-) 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..2f1cc19 100644 --- a/src/app/api/usuarios/[id]/route.ts +++ b/src/app/api/usuarios/[id]/route.ts @@ -86,3 +86,61 @@ 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; + + const dataToUpdate: any = {}; + if (estado !== undefined) dataToUpdate.estado = estado; + if (motivoInactivo !== undefined) dataToUpdate.motivoInactivo = motivoInactivo; + + if (estado === 1) dataToUpdate.motivoInactivo = null; + + const usuario = await prisma.usuario.update({ + where: { id }, + data: dataToUpdate, + }); + + return NextResponse.json({ + success: true, + message: "Usuario actualizado correctamente", + data: usuario, + }); + } catch (error) { + console.error("Error al actualizar usuario:", error); + 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..2182797 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) { + 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,31 @@ export default function GestionUsuariosPage() { {u.estado === 1 ? "Activo" : "Inactivo"} + {u.estado === 0 && u.motivoInactivo && ( +

+ {u.motivoInactivo} +

+ )} - +
+ {u.estado === 0 && ( + + )} + +
))} @@ -468,16 +548,30 @@ export default function GestionUsuariosPage() { {u.estado === 1 ? "Activo" : "Inactivo"} - +
+ {u.estado === 0 && ( + + )} + +
-

{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; } From 43c55ac2c72c6ccbea3fc94f208c4325ceedc62d Mon Sep 17 00:00:00 2001 From: IsaacAburto1548 Date: Sat, 27 Jun 2026 13:00:17 -0600 Subject: [PATCH 2/3] fix: corregir tipado de prisma y aplicar cambios de coderabbit --- src/app/api/usuarios/[id]/route.ts | 52 +++++++++++++++++-- src/app/gestion-usuarios/gestion-usuarios.tsx | 6 ++- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/app/api/usuarios/[id]/route.ts b/src/app/api/usuarios/[id]/route.ts index 2f1cc19..7dfb6d2 100644 --- a/src/app/api/usuarios/[id]/route.ts +++ b/src/app/api/usuarios/[id]/route.ts @@ -69,9 +69,24 @@ export async function DELETE( message: "Usuario eliminado permanentemente", }); } else { + let motivo = ""; + try { + const body = await request.json(); + 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, @@ -121,14 +136,43 @@ export async function PUT( const { estado, motivoInactivo } = body; const dataToUpdate: any = {}; - if (estado !== undefined) dataToUpdate.estado = estado; - if (motivoInactivo !== undefined) dataToUpdate.motivoInactivo = motivoInactivo; + 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 (estado === 1) dataToUpdate.motivoInactivo = null; + 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({ diff --git a/src/app/gestion-usuarios/gestion-usuarios.tsx b/src/app/gestion-usuarios/gestion-usuarios.tsx index 2182797..6a1958d 100644 --- a/src/app/gestion-usuarios/gestion-usuarios.tsx +++ b/src/app/gestion-usuarios/gestion-usuarios.tsx @@ -225,7 +225,7 @@ export default function GestionUsuariosPage() { inputPlaceholder: 'Escribe el motivo aquí...', showCancelButton: true, inputValidator: (value) => { - if (!value) { + if (!value || !value.trim()) { return '¡Debes escribir un motivo!'; } } @@ -516,6 +516,7 @@ export default function GestionUsuariosPage() { onClick={() => handleReactivar(u.id, u.nombreCompleto)} className="inline-flex items-center justify-center w-8 h-8 bg-green-50 text-green-600 hover:bg-green-600 hover:text-white border border-green-200 hover:border-green-600 rounded-lg transition-colors shadow-sm" title="Activar usuario" + aria-label={`Activar usuario ${u.nombreCompleto}`} > @@ -524,6 +525,7 @@ export default function GestionUsuariosPage() { onClick={() => handleEliminar(u.id, u.nombreCompleto)} className="inline-flex items-center justify-center w-8 h-8 bg-red-50 text-red-600 hover:bg-red-600 hover:text-white border border-red-200 hover:border-red-600 rounded-lg transition-colors shadow-sm" title={u.estado === 0 ? "Eliminar permanentemente" : "Eliminar o Desactivar"} + aria-label={u.estado === 0 ? `Eliminar permanentemente usuario ${u.nombreCompleto}` : `Eliminar o desactivar usuario ${u.nombreCompleto}`} > @@ -554,6 +556,7 @@ export default function GestionUsuariosPage() { onClick={() => handleReactivar(u.id, u.nombreCompleto)} className="inline-flex items-center justify-center w-8 h-8 bg-green-50 text-green-600 hover:bg-green-600 hover:text-white border border-green-200 hover:border-green-600 rounded-lg transition-colors shadow-sm" title="Activar usuario" + aria-label={`Activar usuario ${u.nombreCompleto}`} > @@ -562,6 +565,7 @@ export default function GestionUsuariosPage() { onClick={() => handleEliminar(u.id, u.nombreCompleto)} className="inline-flex items-center justify-center w-8 h-8 bg-red-50 text-red-600 hover:bg-red-600 hover:text-white border border-red-200 hover:border-red-600 rounded-lg transition-colors shadow-sm" title={u.estado === 0 ? "Eliminar permanentemente" : "Eliminar o Desactivar"} + aria-label={u.estado === 0 ? `Eliminar permanentemente usuario ${u.nombreCompleto}` : `Eliminar o desactivar usuario ${u.nombreCompleto}`} > From 9b214bbe0fcf19ea10901d6c5e9d6b99f7b9e280 Mon Sep 17 00:00:00 2001 From: IsaacAburto1548 Date: Sat, 27 Jun 2026 13:11:32 -0600 Subject: [PATCH 3/3] Update route.ts --- src/app/api/usuarios/[id]/route.ts | 37 +++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/app/api/usuarios/[id]/route.ts b/src/app/api/usuarios/[id]/route.ts index 7dfb6d2..234daf4 100644 --- a/src/app/api/usuarios/[id]/route.ts +++ b/src/app/api/usuarios/[id]/route.ts @@ -72,6 +72,12 @@ export async function DELETE( 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 @@ -135,6 +141,27 @@ export async function PUT( 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; @@ -180,8 +207,16 @@ export async function PUT( message: "Usuario actualizado correctamente", data: usuario, }); - } catch (error) { + } 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 }