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]>
234 lines
6.8 KiB
Plaintext
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)
|
|
}
|