Files
app-padel/apps/web/app/api/clients/[id]/route.ts
Ivan 88c6a7084a feat(clients): add clients management page
Add comprehensive clients management interface including:
- Client table with search, filtering, and pagination
- Client form for creating and editing clients
- Client detail dialog showing profile, membership, and stats
- API endpoint for individual client operations (GET, PUT, DELETE)
- Stats cards showing total clients, with membership, and new this month
- Integration with membership assignment dialog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 07:38:40 +00:00

378 lines
9.0 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db';
import { z } from 'zod';
interface RouteContext {
params: Promise<{ id: string }>;
}
// Validation schema for updating client
const updateClientSchema = z.object({
firstName: z.string().min(1, 'El nombre es requerido').optional(),
lastName: z.string().min(1, 'El apellido es requerido').optional(),
email: z.string().email('Email invalido').nullable().optional(),
phone: z.string().nullable().optional(),
avatar: z.string().url('URL invalida').nullable().optional(),
dateOfBirth: z.string().nullable().optional(),
address: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
level: z.string().nullable().optional(),
tags: z.array(z.string()).optional(),
});
// GET /api/clients/[id] - Get a single client with details
export async function GET(
request: NextRequest,
context: RouteContext
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ error: 'No autorizado' },
{ status: 401 }
);
}
const { id } = await context.params;
const client = await db.client.findFirst({
where: {
id,
organizationId: session.user.organizationId,
},
include: {
memberships: {
where: {
status: 'ACTIVE',
endDate: {
gte: new Date(),
},
},
include: {
plan: {
select: {
id: true,
name: true,
price: true,
durationMonths: true,
courtHours: true,
discountPercent: true,
},
},
},
orderBy: {
endDate: 'desc',
},
take: 1,
},
_count: {
select: {
bookings: true,
},
},
},
});
if (!client) {
return NextResponse.json(
{ error: 'Cliente no encontrado' },
{ status: 404 }
);
}
// Calculate total spent from payments
const totalSpentResult = await db.payment.aggregate({
where: {
clientId: client.id,
},
_sum: {
amount: true,
},
});
// Calculate total from sales
const totalSalesResult = await db.sale.aggregate({
where: {
clientId: client.id,
},
_sum: {
total: true,
},
});
const totalSpent =
Number(totalSpentResult._sum.amount || 0) +
Number(totalSalesResult._sum.total || 0);
// For now, balance is set to 0 (can be extended with a balance field in the future)
const balance = 0;
return NextResponse.json({
...client,
stats: {
totalBookings: client._count.bookings,
totalSpent,
balance,
},
});
} catch (error) {
console.error('Error fetching client:', error);
return NextResponse.json(
{ error: 'Error al obtener el cliente' },
{ status: 500 }
);
}
}
// PUT /api/clients/[id] - Update a client
export async function PUT(
request: NextRequest,
context: RouteContext
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ error: 'No autorizado' },
{ status: 401 }
);
}
const { id } = await context.params;
// Verify client exists and belongs to user's organization
const existingClient = await db.client.findFirst({
where: {
id,
organizationId: session.user.organizationId,
},
});
if (!existingClient) {
return NextResponse.json(
{ error: 'Cliente no encontrado' },
{ status: 404 }
);
}
const body = await request.json();
// Validate input
const validationResult = updateClientSchema.safeParse(body);
if (!validationResult.success) {
return NextResponse.json(
{
error: 'Datos de actualizacion invalidos',
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 }
);
}
const {
firstName,
lastName,
email,
phone,
avatar,
dateOfBirth,
address,
notes,
level,
tags,
} = validationResult.data;
// Check for email uniqueness if email is being changed
if (email && email !== existingClient.email) {
const emailExists = await db.client.findFirst({
where: {
organizationId: session.user.organizationId,
email,
id: {
not: id,
},
},
});
if (emailExists) {
return NextResponse.json(
{ error: 'Ya existe un cliente con este email' },
{ status: 409 }
);
}
}
// Build update data
const updateData: Record<string, unknown> = {};
if (firstName !== undefined) updateData.firstName = firstName;
if (lastName !== undefined) updateData.lastName = lastName;
if (email !== undefined) updateData.email = email;
if (phone !== undefined) updateData.phone = phone;
if (avatar !== undefined) updateData.avatar = avatar;
if (dateOfBirth !== undefined) {
updateData.dateOfBirth = dateOfBirth ? new Date(dateOfBirth) : null;
}
if (address !== undefined) updateData.address = address;
if (notes !== undefined) updateData.notes = notes;
if (level !== undefined) updateData.level = level;
if (tags !== undefined) updateData.tags = tags;
const client = await db.client.update({
where: { id },
data: updateData,
include: {
memberships: {
where: {
status: 'ACTIVE',
endDate: {
gte: new Date(),
},
},
include: {
plan: {
select: {
id: true,
name: true,
price: true,
durationMonths: true,
courtHours: true,
},
},
},
take: 1,
},
_count: {
select: {
bookings: true,
},
},
},
});
return NextResponse.json(client);
} catch (error) {
console.error('Error updating client:', error);
// Check for unique constraint violation
if (error instanceof Error && error.message.includes('Unique constraint')) {
return NextResponse.json(
{ error: 'Ya existe un cliente con este email o DNI' },
{ status: 409 }
);
}
return NextResponse.json(
{ error: 'Error al actualizar el cliente' },
{ status: 500 }
);
}
}
// DELETE /api/clients/[id] - Soft delete a client (set isActive = false)
export async function DELETE(
request: NextRequest,
context: RouteContext
) {
try {
const session = await getServerSession(authOptions);
if (!session?.user) {
return NextResponse.json(
{ error: 'No autorizado' },
{ status: 401 }
);
}
const { id } = await context.params;
// Verify client exists and belongs to user's organization
const existingClient = await db.client.findFirst({
where: {
id,
organizationId: session.user.organizationId,
},
include: {
memberships: {
where: {
status: 'ACTIVE',
},
},
bookings: {
where: {
status: {
in: ['PENDING', 'CONFIRMED'],
},
startTime: {
gte: new Date(),
},
},
},
},
});
if (!existingClient) {
return NextResponse.json(
{ error: 'Cliente no encontrado' },
{ status: 404 }
);
}
// Check for active memberships
if (existingClient.memberships.length > 0) {
return NextResponse.json(
{
error: 'No se puede desactivar un cliente con membresia activa',
details: {
activeMemberships: existingClient.memberships.length,
},
},
{ status: 400 }
);
}
// Check for pending/future bookings
if (existingClient.bookings.length > 0) {
return NextResponse.json(
{
error: 'No se puede desactivar un cliente con reservas pendientes',
details: {
pendingBookings: existingClient.bookings.length,
},
},
{ status: 400 }
);
}
// Soft delete by setting isActive to false
const client = await db.client.update({
where: { id },
data: {
isActive: false,
},
select: {
id: true,
firstName: true,
lastName: true,
isActive: true,
},
});
return NextResponse.json({
message: 'Cliente desactivado exitosamente',
client,
});
} catch (error) {
console.error('Error deleting client:', error);
return NextResponse.json(
{ error: 'Error al desactivar el cliente' },
{ status: 500 }
);
}
}