feat(whatsapp): schema del bot de reservas + panel de trabajos por jefe (F1+F3)
FASE 1 — schema:
- Cliente.email opcional; nuevo telefonoNorm (E.164, único) para matchear por WhatsApp
- TipoServicio.jefeMecanicoId opcional (trabajos per-jefe; globales legacy quedan null)
- Turno.autoId/kilometros opcionales (reservas por WhatsApp sin auto/km)
- Nuevo modelo ConversacionWhatsapp (estado del bot por teléfono)
- admin/turnos/[id]: no permite COMPLETADO sin auto+km; Sheets tolera email nulo
FASE 3 — panel de trabajos del jefe:
- API jefe/servicios (GET/POST) y jefe/servicios/[id] (PATCH/DELETE), scoped al jefe
- DELETE desactiva si el trabajo ya tiene turnos asociados
- Página /jefe/servicios (alta/listado/activar/editar/eliminar) + nav Turnos|Trabajos
- Dashboard del jefe resiliente a turnos sin auto/km ("a completar")
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
// Edita un trabajo propio del jefe (nombre, duración, activo). Solo si le pertenece.
|
||||
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const propio = await prisma.tipoServicio.findFirst({
|
||||
where: { id, jefeMecanicoId: sesion.sub },
|
||||
});
|
||||
if (!propio) return NextResponse.json({ error: "Trabajo no encontrado" }, { status: 404 });
|
||||
|
||||
const data = await req.json();
|
||||
|
||||
if (data.duracionHoras !== undefined) {
|
||||
const dur = Number(data.duracionHoras);
|
||||
if (!Number.isFinite(dur) || dur <= 0 || dur > 24)
|
||||
return NextResponse.json(
|
||||
{ error: "La duración debe ser un número de horas entre 0 y 24" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (data.nombre !== undefined && (typeof data.nombre !== "string" || !data.nombre.trim()))
|
||||
return NextResponse.json({ error: "El nombre del trabajo es obligatorio" }, { status: 400 });
|
||||
|
||||
const servicio = await prisma.tipoServicio.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(data.nombre !== undefined && { nombre: data.nombre.trim() }),
|
||||
...(data.duracionHoras !== undefined && { duracionHoras: Number(data.duracionHoras) }),
|
||||
...(data.activo !== undefined && { activo: Boolean(data.activo) }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(servicio);
|
||||
}
|
||||
|
||||
// Elimina un trabajo propio. Si ya tiene turnos asociados, lo desactiva en vez de borrarlo
|
||||
// (para no romper la integridad referencial de los turnos históricos).
|
||||
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const propio = await prisma.tipoServicio.findFirst({
|
||||
where: { id, jefeMecanicoId: sesion.sub },
|
||||
});
|
||||
if (!propio) return NextResponse.json({ error: "Trabajo no encontrado" }, { status: 404 });
|
||||
|
||||
const enUso = await prisma.turno.count({ where: { tipoServicioId: id } });
|
||||
if (enUso > 0) {
|
||||
const servicio = await prisma.tipoServicio.update({
|
||||
where: { id },
|
||||
data: { activo: false },
|
||||
});
|
||||
return NextResponse.json({ ...servicio, desactivado: true });
|
||||
}
|
||||
|
||||
await prisma.tipoServicio.delete({ where: { id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { verificarSesion } from "@/lib/auth-admin";
|
||||
|
||||
// Lista los trabajos cargados por el jefe logueado (los suyos, no los globales).
|
||||
export async function GET() {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const servicios = await prisma.tipoServicio.findMany({
|
||||
where: { jefeMecanicoId: sesion.sub },
|
||||
orderBy: { nombre: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(servicios);
|
||||
}
|
||||
|
||||
// Crea un trabajo propio del jefe (nombre + duración en horas).
|
||||
export async function POST(req: NextRequest) {
|
||||
const sesion = await verificarSesion("jefe_session");
|
||||
if (!sesion || sesion.role !== "jefe")
|
||||
return NextResponse.json({ error: "No autorizado" }, { status: 401 });
|
||||
|
||||
const { nombre, duracionHoras } = await req.json();
|
||||
const dur = Number(duracionHoras);
|
||||
if (!nombre || typeof nombre !== "string" || !nombre.trim())
|
||||
return NextResponse.json({ error: "El nombre del trabajo es obligatorio" }, { status: 400 });
|
||||
if (!Number.isFinite(dur) || dur <= 0 || dur > 24)
|
||||
return NextResponse.json(
|
||||
{ error: "La duración debe ser un número de horas entre 0 y 24" },
|
||||
{ status: 400 }
|
||||
);
|
||||
|
||||
const servicio = await prisma.tipoServicio.create({
|
||||
data: { nombre: nombre.trim(), duracionHoras: dur, jefeMecanicoId: sesion.sub },
|
||||
});
|
||||
|
||||
return NextResponse.json(servicio, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user