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:
Beldor
2026-07-27 19:01:54 -03:00
parent dd7f73453e
commit c53e5bb5aa
7 changed files with 348 additions and 26 deletions
+38 -19
View File
@@ -7,16 +7,18 @@ datasource db {
}
model Cliente {
id String @id @default(cuid())
email String @unique
nombre String
apellido String
telefono String
createdAt DateTime @default(now())
id String @id @default(cuid())
email String? @unique
nombre String
apellido String
telefono String
telefonoNorm String? @unique // teléfono en formato E.164, usado para matchear al que escribe por WhatsApp
createdAt DateTime @default(now())
autos Auto[]
turnos Turno[]
magicTokens MagicToken[]
autos Auto[]
turnos Turno[]
magicTokens MagicToken[]
conversaciones ConversacionWhatsapp[]
}
model Auto {
@@ -35,30 +37,32 @@ model Auto {
}
model TipoServicio {
id String @id @default(cuid())
nombre String
duracionHoras Float
activo Boolean @default(true)
id String @id @default(cuid())
nombre String
duracionHoras Float
activo Boolean @default(true)
jefeMecanicoId String? // trabajo cargado por un jefe mecánico puntual; null = servicio global (legacy)
turnos Turno[]
historial HistorialServicio[]
jefeMecanico JefeMecanico? @relation(fields: [jefeMecanicoId], references: [id])
turnos Turno[]
historial HistorialServicio[]
}
model Turno {
id String @id @default(cuid())
clienteId String
autoId String
autoId String?
tipoServicioId String
jefeMecanicoId String
fechaInicio DateTime
fechaFin DateTime
kilometros Int
kilometros Int?
estado EstadoTurno @default(PENDIENTE)
comentario String?
createdAt DateTime @default(now())
cliente Cliente @relation(fields: [clienteId], references: [id])
auto Auto @relation(fields: [autoId], references: [id])
auto Auto? @relation(fields: [autoId], references: [id])
tipoServicio TipoServicio @relation(fields: [tipoServicioId], references: [id])
jefeMecanico JefeMecanico @relation(fields: [jefeMecanicoId], references: [id])
}
@@ -72,7 +76,8 @@ model JefeMecanico {
activo Boolean @default(true)
createdAt DateTime @default(now())
turnos Turno[]
turnos Turno[]
servicios TipoServicio[]
}
model HistorialServicio {
@@ -117,3 +122,17 @@ enum EstadoTurno {
CANCELADO
COMPLETADO
}
// Estado de la conversación del bot de WhatsApp, por número de teléfono.
// El webhook es stateless: acá se guarda en qué paso está y las selecciones parciales.
model ConversacionWhatsapp {
id String @id @default(cuid())
telefono String @unique // wa_id del cliente (E.164, sin '+')
paso String @default("INICIO") // paso de la máquina de estados
datos Json @default("{}") // selecciones parciales (mecánico, trabajo, slot, alta en curso, etc.)
clienteId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cliente Cliente? @relation(fields: [clienteId], references: [id])
}