From a8842a2728c2e081da9b3da77e3f6a8116cb230f Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:55:16 -0500 Subject: [PATCH] feat(users): add block/unblock and delete from admin panel - Migration 0004: adds is_active column to users table - Backend: DELETE /users/:id endpoint + is_active field in UpdateUserDto - Admin UI: block/unblock toggle and delete with confirmation on user detail - Users list: shows "Bloqueado" badge for inactive users Co-Authored-By: Claude Sonnet 4.6 --- admin/src/app/users/[id]/page.tsx | 114 +++++++++++++++--- admin/src/app/users/page.tsx | 9 +- .../0004_add_user_is_active/migration.sql | 1 + backend/prisma/schema.prisma | 1 + backend/src/auth/dto/auth.dto.ts | 6 +- backend/src/users/users.controller.ts | 9 +- backend/src/users/users.service.ts | 4 + 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 backend/prisma/migrations/0004_add_user_is_active/migration.sql diff --git a/admin/src/app/users/[id]/page.tsx b/admin/src/app/users/[id]/page.tsx index 0dbc5eb..03eaa48 100644 --- a/admin/src/app/users/[id]/page.tsx +++ b/admin/src/app/users/[id]/page.tsx @@ -1,14 +1,14 @@ 'use client'; import { useEffect, useState, useCallback } from 'react'; -import { useParams } from 'next/navigation'; +import { useParams, useRouter } from 'next/navigation'; import Link from 'next/link'; import { api } from '@/lib/api'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { ArrowLeft, Pencil, X, Check } from 'lucide-react'; +import { ArrowLeft, Pencil, X, Check, ShieldOff, Shield, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; interface Professional { id: string; profession?: string; rate?: number; identification?: string; } @@ -16,7 +16,7 @@ interface Reputation { total: number; average: number; total_pro: number; averag interface UserDetail { id: string; name: string; email?: string; phone?: string; city?: string; gender?: string; birthday?: string; is_email_verified?: boolean; - is_phone_verified?: boolean; pro_state?: number; created_at: string; + is_phone_verified?: boolean; is_active?: boolean; pro_state?: number; created_at: string; professionals?: Professional | null; reputations?: Reputation | null; } @@ -29,12 +29,16 @@ interface Form { name: string; city: string; phone: string; gender: string; } export default function UserDetailPage() { const params = useParams<{ id: string }>(); + const router = useRouter(); const id = params?.id; const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [editing, setEditing] = useState(false); const [saving, setSaving] = useState(false); + const [toggling, setToggling] = useState(false); + const [deleting, setDeleting] = useState(false); + const [confirmDelete, setConfirmDelete] = useState(false); const [form, setForm] = useState
({ name: '', city: '', phone: '', gender: '' }); const load = useCallback(() => { @@ -71,6 +75,34 @@ export default function UserDetailPage() { } }; + const toggleBlock = async () => { + if (!user) return; + setToggling(true); + try { + const updated = await api.patch(`/users/${id}`, { is_active: !user.is_active }); + setUser(updated); + toast.success(updated.is_active ? 'Usuario desbloqueado' : 'Usuario bloqueado'); + } catch (e: any) { + toast.error(e?.message || 'Error al cambiar estado'); + } finally { + setToggling(false); + } + }; + + const deleteUser = async () => { + setDeleting(true); + try { + await api.delete(`/users/${id}`); + toast.success('Usuario eliminado'); + router.push('/users'); + } catch (e: any) { + toast.error(e?.message || 'No se puede eliminar: el usuario tiene datos asociados'); + setConfirmDelete(false); + } finally { + setDeleting(false); + } + }; + if (loading) return
Cargando...
; if (error) return (
@@ -81,32 +113,72 @@ export default function UserDetailPage() { if (!user) return null; const proState = user.pro_state ?? 0; + const isActive = user.is_active !== false; return (
{/* Header */} -
+
-

{user.name}

+
+

{user.name}

+ {!isActive && Bloqueado} +

ID: {user.id}

- {!editing ? ( - - ) : ( -
- - + +
+ )} + + {/* Editar */} + {!editing ? ( + -
- )} + ) : ( +
+ + +
+ )} +
@@ -170,6 +242,14 @@ export default function UserDetailPage() { Estado +
+ Cuenta +
+ + {isActive ? 'Activo' : 'Bloqueado'} + +
+
Estado profesional
@@ -201,7 +281,7 @@ export default function UserDetailPage() {
- {/* Perfil profesional (solo lectura, con link) */} + {/* Perfil profesional */} {user.professionals && ( diff --git a/admin/src/app/users/page.tsx b/admin/src/app/users/page.tsx index 4d1fc51..4ac5178 100644 --- a/admin/src/app/users/page.tsx +++ b/admin/src/app/users/page.tsx @@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Search, Eye } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; import Link from 'next/link'; interface User { @@ -15,6 +16,7 @@ interface User { email?: string; phone?: string; city?: string; + is_active?: boolean; pro_state?: number; created_at: string; } @@ -91,7 +93,12 @@ export default function UsersPage() { {u.email || '—'} {u.phone || '—'} {u.city || '—'} - {['Usuario', 'Solicitó', 'Profesional', 'Rechazado'][u.pro_state ?? 0] || '—'} + + {u.is_active === false + ? Bloqueado + : {['Usuario', 'Solicitó', 'Profesional', 'Rechazado'][u.pro_state ?? 0] || '—'} + } + {new Date(u.created_at).toLocaleDateString()} diff --git a/backend/prisma/migrations/0004_add_user_is_active/migration.sql b/backend/prisma/migrations/0004_add_user_is_active/migration.sql new file mode 100644 index 0000000..e2806f6 --- /dev/null +++ b/backend/prisma/migrations/0004_add_user_is_active/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "is_active" BOOLEAN NOT NULL DEFAULT true; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6bdefbd..d1020d4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -227,6 +227,7 @@ model users { fcm_token String? is_phone_verified Boolean? @default(false) is_email_verified Boolean? @default(false) + is_active Boolean @default(true) created_at DateTime @default(now()) @db.Timestamptz(6) updated_at DateTime @default(now()) @db.Timestamptz(6) chats_chats_professional_idTousers chats[] @relation("chats_professional_idTousers") diff --git a/backend/src/auth/dto/auth.dto.ts b/backend/src/auth/dto/auth.dto.ts index f9954fd..be72284 100644 --- a/backend/src/auth/dto/auth.dto.ts +++ b/backend/src/auth/dto/auth.dto.ts @@ -1,4 +1,4 @@ -import { IsEmail, IsString, MinLength, IsOptional, IsPhoneNumber, Matches } from 'class-validator'; +import { IsEmail, IsString, MinLength, IsOptional, IsPhoneNumber, Matches, IsBoolean } from 'class-validator'; export class RegisterDto { @IsEmail() @@ -54,6 +54,10 @@ export class UpdateUserDto { @Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'birthday must be YYYY-MM-DD' }) @IsOptional() birthday?: string; + + @IsOptional() + @IsBoolean() + is_active?: boolean; } export class FcmTokenDto { diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index b3ca423..4d9a74a 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Patch, Param, Body, UseGuards, Req } from '@nestjs/common'; +import { Controller, Get, Patch, Delete, Param, Body, UseGuards, Req } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; import { UsersService } from './users.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @@ -48,4 +48,11 @@ export class UsersController { findById(@Param('id') id: string) { return this.users.findById(id); } + + @Delete(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + deleteById(@Param('id') id: string) { + return this.users.deleteById(id); + } } diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 4d2c307..3960a7c 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -31,4 +31,8 @@ export class UsersService { updateFcmToken(id: string, fcm_token: string) { return this.prisma.users.update({ where: { id }, data: { fcm_token } }); } + + deleteById(id: string) { + return this.prisma.users.delete({ where: { id } }); + } }