Files
cotizador/prisma/schema.prisma
T
urieljarethandClaude Opus 4.8 8b1e420d4a Recuperar feature de registro de horas / cobro por tiempo (RegistroHoras)
La cotización UJ2606AG777 (aprobada, "Agentes IA") se cobra por hora bajo
demanda, pero la rama desplegada (main) no incluía la feature de registro de
horas que sí existía en desarrollo (rama master): el cargo de horas de junio no
se veía en la web y los proyectos por tiempo no mostraban sus cobros en el panel
individual.

Se porta la feature de forma quirúrgica (sin arrastrar cambios no relacionados):

- Modelo RegistroHoras + columnas Cotizacion.incluirIva, Cliente.rfc,
  ServicioCotizado.beneficios (migración idempotente para la web).
- calculators.ts: modelo de cobro "demanda", helpers de horas
  (calcularHorasRango, agrupación por día/semana/mes, notas de pago).
- RegistroHorasPanel: panel para registrar/editar/borrar horas y previsualizar
  la nota de pago con branding. Solo aparece en cotizaciones aprobadas con cobro
  por tiempo (horas/retainer/demanda).
- PreciosEditables: los servicios "demanda" muestran la tarifa/hr en vez de $0.
- API /api/cotizaciones/[id]/horas (+ /[registroId]) para el CRUD de registros.
- Handlers de cotizaciones: persisten incluirIva/rfc/beneficios, "demanda" nunca
  suma al total, y fast-path para el cambio de estado (arregla CambiarEstado que
  fallaba la validación al enviar solo { estado }).
- .megaignore para evitar que MegaSync corrompa node_modules/.next.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-08 02:11:57 -06:00

234 lines
6.8 KiB
Plaintext

generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(cuid())
email String @unique
password String
name String
role String @default("asesor")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cotizaciones Cotizacion[] @relation("AsesorCotizaciones")
}
model Cliente {
id String @id @default(cuid())
nombre String
empresa String?
email String?
telefono String?
rfc String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cotizaciones Cotizacion[]
@@index([nombre, empresa])
}
model Cotizacion {
id String @id @default(cuid())
numero String @unique
fecha DateTime @default(now())
vigencia DateTime
moneda String @default("MXN")
tipoCambio String @default("NA")
proyecto String @default("MKT Digital")
esquemaPago String @default("Pago Unico/Mensual")
estado String @default("borrador")
incluirBonos Boolean @default(false)
incluirFinanciamiento Boolean @default(false)
incluirIva Boolean @default(true)
esDoble Boolean @default(false)
opcionesMetadata Json?
observaciones String?
clienteId String
asesorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cliente Cliente @relation(fields: [clienteId], references: [id])
asesor User @relation("AsesorCotizaciones", fields: [asesorId], references: [id])
servicios ServicioCotizado[]
planBucefalo PlanBucefaloCotizacion?
registrosHoras RegistroHoras[]
@@index([clienteId])
@@index([asesorId])
@@index([estado])
@@index([createdAt])
}
model Categoria {
id String @id @default(cuid())
nombre String @unique
descripcion String?
color String @default("#6b7280")
activo Boolean @default(true)
orden Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
servicios ServicioCatalogo[]
}
model Paquete {
id String @id @default(cuid())
nombre String
descripcion String?
activo Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
fases FasePaquete[]
}
model FasePaquete {
id String @id @default(cuid())
paqueteId String
nombre String
orden Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
paquete Paquete @relation(fields: [paqueteId], references: [id], onDelete: Cascade)
servicios ServicioPaquete[]
@@index([paqueteId])
}
model ServicioCatalogo {
id String @id @default(cuid())
nombre String
descripcion String?
fase Int @default(1)
tipoPago String @default("unico")
precioBase Float
tiempoEntrega String @default("4 - 10 dias")
entregablesDefault Json @default("[]")
categoriaId String?
variante String?
nivel String?
activo Boolean @default(true)
orden Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
servicios ServicioCotizado[]
categoriaRel Categoria? @relation(fields: [categoriaId], references: [id])
paquetes ServicioPaquete[]
@@index([categoriaId])
@@index([activo, fase, orden])
}
model ServicioPaquete {
id String @id @default(cuid())
servicioCatalogoId String
fasePaqueteId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
servicio ServicioCatalogo @relation(fields: [servicioCatalogoId], references: [id], onDelete: Cascade)
fasePaquete FasePaquete @relation(fields: [fasePaqueteId], references: [id], onDelete: Cascade)
@@unique([servicioCatalogoId, fasePaqueteId])
@@index([fasePaqueteId])
}
model ServicioCotizado {
id String @id @default(cuid())
cotizacionId String
servicioCatalogoId String?
nombre String?
esPersonalizado Boolean @default(false)
horas Float?
tarifaHora Float?
modeloCobro String @default("fijo")
montoMinimo Float?
horasIncluidas Float?
opcion String?
fase Int
tipoPago String
precio Float
tiempoEntrega String
entregables Json @default("[]")
beneficios Json @default("[]")
notas String?
seleccionado Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
servicioCatalogo ServicioCatalogo? @relation(fields: [servicioCatalogoId], references: [id])
@@index([cotizacionId])
@@index([servicioCatalogoId])
}
model PlanBucefaloCotizacion {
id String @id @default(cuid())
cotizacionId String @unique
nivel String @default("basico")
precio Float
seleccionado Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
}
// Registro de horas trabajadas contra una cotizacion aprobada con cobro por tiempo
// (modeloCobro horas/retainer/demanda). Se usa para emitir notas de pago al cliente:
// cada entrada es un dia con un rango horario, una descripcion breve y una tarifa
// (snapshot). `horas` se calcula del rango (calcularHorasRango) y se persiste.
model RegistroHoras {
id String @id @default(cuid())
cotizacionId String
fecha DateTime
horaInicio String
horaFin String
horas Float
tarifaHora Float @default(0)
descripcion String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
@@index([cotizacionId])
@@index([fecha])
}
model Configuracion {
id String @id @default(cuid())
clave String @unique
valor String
updatedAt DateTime @updatedAt
}
model Bono {
id String @id @default(cuid())
numero Int
titulo String
descripcion String
activo Boolean @default(true)
}
model FinanciamientoPlan {
id String @id @default(cuid())
meses Int @unique
tasa Float
comision Float
montoMinimo Float
iva Float @default(0.16)
}