feat: Add major features - Mejoras 5-10

- Mejora 5: Órdenes de Compra integration in obra detail
- Mejora 6: Portal de Cliente with JWT auth for clients
- Mejora 7: Diagrama de Gantt for project visualization
- Mejora 8: Push Notifications with service worker
- Mejora 9: Activity Log system with templates
- Mejora 10: PWA support with offline capabilities

New features include:
- Fotos gallery with upload/delete
- Bitácora de obra with daily logs
- PDF export for reports, gastos, presupuestos
- Control de asistencia for employees
- Client portal with granular permissions
- Gantt chart with task visualization
- Push notification system
- Activity timeline component
- PWA manifest, icons, and install prompt

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mexus
2026-01-19 03:09:38 +00:00
parent 86bfbd2039
commit a08e7057e8
69 changed files with 12435 additions and 26 deletions

View File

@@ -106,6 +106,14 @@ model User {
tareasAsignadas TareaObra[]
obrasSupervision Obra[] @relation("ObraSupervisor")
registrosAvance RegistroAvance[]
fotosSubidas FotoAvance[]
bitacorasRegistradas BitacoraObra[]
asistenciasRegistradas Asistencia[]
ordenesCreadas OrdenCompra[] @relation("OrdenCreador")
ordenesAprobadas OrdenCompra[] @relation("OrdenAprobador")
pushSubscriptions PushSubscription[]
notificaciones Notificacion[]
actividades ActividadLog[]
@@index([empresaId])
@@index([email])
@@ -145,10 +153,40 @@ model Cliente {
// Relations
obras Obra[]
accesos ClienteAcceso[]
@@index([empresaId])
}
// ============== PORTAL DE CLIENTES ==============
model ClienteAcceso {
id String @id @default(cuid())
email String @unique
password String? // Hash de contraseña (opcional si usa token)
token String? @unique // Token de acceso único
tokenExpira DateTime? // Expiración del token
activo Boolean @default(true)
ultimoAcceso DateTime?
// Permisos
verFotos Boolean @default(true)
verAvances Boolean @default(true)
verGastos Boolean @default(false)
verDocumentos Boolean @default(true)
descargarPDF Boolean @default(true)
// Relaciones
clienteId String
cliente Cliente @relation(fields: [clienteId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([clienteId])
@@index([token])
}
model Obra {
id String @id @default(cuid())
nombre String
@@ -180,6 +218,11 @@ model Obra {
asignaciones AsignacionEmpleado[]
contratos ContratoSubcontratista[]
registrosAvance RegistroAvance[]
fotos FotoAvance[]
bitacoras BitacoraObra[]
asistencias Asistencia[]
ordenesCompra OrdenCompra[]
actividadesLog ActividadLog[]
@@index([empresaId])
@@index([estado])
@@ -201,6 +244,7 @@ model FaseObra {
// Relations
tareas TareaObra[]
fotos FotoAvance[]
@@index([obraId])
}
@@ -350,6 +394,7 @@ model Material {
// Relations
movimientos MovimientoInventario[]
itemsOrden ItemOrdenCompra[]
@@unique([codigo, empresaId])
@@index([empresaId])
@@ -392,6 +437,7 @@ model Empleado {
// Relations
asignaciones AsignacionEmpleado[]
jornadas JornadaTrabajo[]
asistencias Asistencia[]
@@index([empresaId])
}
@@ -464,3 +510,388 @@ model ContratoSubcontratista {
@@index([subcontratistaId])
@@index([obraId])
}
// ============== ÓRDENES DE COMPRA ==============
enum EstadoOrdenCompra {
BORRADOR
PENDIENTE
APROBADA
ENVIADA
RECIBIDA_PARCIAL
RECIBIDA
CANCELADA
}
enum PrioridadOrden {
BAJA
NORMAL
ALTA
URGENTE
}
model OrdenCompra {
id String @id @default(cuid())
numero String // Número de orden (OC-001, etc.)
// Estado y prioridad
estado EstadoOrdenCompra @default(BORRADOR)
prioridad PrioridadOrden @default(NORMAL)
// Fechas
fechaEmision DateTime @default(now())
fechaRequerida DateTime? // Fecha en que se necesitan los materiales
fechaAprobacion DateTime?
fechaEnvio DateTime?
fechaRecepcion DateTime?
// Proveedor
proveedorNombre String
proveedorRfc String?
proveedorContacto String?
proveedorTelefono String?
proveedorEmail String?
proveedorDireccion String?
// Totales
subtotal Float @default(0)
descuento Float @default(0)
iva Float @default(0)
total Float @default(0)
// Condiciones
condicionesPago String? // Ej: "Contado", "Crédito 30 días"
tiempoEntrega String? // Ej: "3-5 días hábiles"
lugarEntrega String? // Dirección de entrega
// Notas
notas String? @db.Text
notasInternas String? @db.Text
// Relaciones
obraId String
obra Obra @relation(fields: [obraId], references: [id], onDelete: Cascade)
creadoPorId String
creadoPor User @relation("OrdenCreador", fields: [creadoPorId], references: [id])
aprobadoPorId String?
aprobadoPor User? @relation("OrdenAprobador", fields: [aprobadoPorId], references: [id])
// Items
items ItemOrdenCompra[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([numero, obraId])
@@index([obraId])
@@index([estado])
@@index([fechaEmision])
}
model ItemOrdenCompra {
id String @id @default(cuid())
// Descripción del item
codigo String? // Código del material
descripcion String
unidad UnidadMedida
// Cantidades
cantidad Float
cantidadRecibida Float @default(0)
// Precios
precioUnitario Float
descuento Float @default(0)
subtotal Float
// Relaciones
ordenId String
orden OrdenCompra @relation(fields: [ordenId], references: [id], onDelete: Cascade)
materialId String? // Opcional: vincular con catálogo de materiales
material Material? @relation(fields: [materialId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([ordenId])
@@index([materialId])
}
// ============== CONTROL DE ASISTENCIA ==============
enum TipoAsistencia {
PRESENTE
AUSENTE
RETARDO
PERMISO
INCAPACIDAD
VACACIONES
}
model Asistencia {
id String @id @default(cuid())
fecha DateTime @db.Date
// Estado de asistencia
tipo TipoAsistencia @default(PRESENTE)
// Registro de entrada
horaEntrada DateTime?
latitudEntrada Float?
longitudEntrada Float?
// Registro de salida
horaSalida DateTime?
latitudSalida Float?
longitudSalida Float?
// Horas trabajadas (calculadas)
horasTrabajadas Float?
horasExtra Float @default(0)
// Notas y observaciones
notas String?
motivoAusencia String?
// Relaciones
empleadoId String
empleado Empleado @relation(fields: [empleadoId], references: [id], onDelete: Cascade)
obraId String
obra Obra @relation(fields: [obraId], references: [id], onDelete: Cascade)
registradoPorId String
registradoPor User @relation(fields: [registradoPorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([empleadoId, obraId, fecha])
@@index([empleadoId])
@@index([obraId])
@@index([fecha])
@@index([tipo])
}
// ============== BITÁCORA DE OBRA ==============
enum CondicionClima {
SOLEADO
NUBLADO
PARCIALMENTE_NUBLADO
LLUVIA_LIGERA
LLUVIA_FUERTE
TORMENTA
VIENTO_FUERTE
FRIO_EXTREMO
CALOR_EXTREMO
}
model BitacoraObra {
id String @id @default(cuid())
fecha DateTime @db.Date
// Condiciones climáticas
clima CondicionClima
temperaturaMin Float?
temperaturaMax Float?
condicionesExtra String? // Notas adicionales del clima
// Personal en obra
personalPropio Int @default(0)
personalSubcontrato Int @default(0)
personalDetalle String? // Descripción del personal
// Actividades del día
actividadesRealizadas String @db.Text
actividadesPendientes String? @db.Text
// Materiales
materialesUtilizados String? @db.Text
materialesRecibidos String? @db.Text
// Equipo y maquinaria
equipoUtilizado String? @db.Text
// Incidentes y observaciones
incidentes String? @db.Text
observaciones String? @db.Text
// Seguridad
incidentesSeguridad String? @db.Text
platicaSeguridad Boolean @default(false)
temaSeguridad String?
// Visitas
visitasInspeccion String? @db.Text
// Relaciones
obraId String
obra Obra @relation(fields: [obraId], references: [id], onDelete: Cascade)
registradoPorId String
registradoPor User @relation(fields: [registradoPorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([obraId, fecha])
@@index([obraId])
@@index([fecha])
}
// ============== FOTOS DE AVANCE ==============
model FotoAvance {
id String @id @default(cuid())
url String // Ruta del archivo
thumbnail String? // Ruta de la miniatura
titulo String?
descripcion String?
fechaCaptura DateTime @default(now())
// Geolocalización
latitud Float?
longitud Float?
direccionGeo String? // Dirección obtenida por geocoding
// Metadatos
tamanio Int? // Tamaño en bytes
tipo String? // MIME type (image/jpeg, etc.)
ancho Int? // Width en pixels
alto Int? // Height en pixels
// Relaciones
obraId String
obra Obra @relation(fields: [obraId], references: [id], onDelete: Cascade)
faseId String?
fase FaseObra? @relation(fields: [faseId], references: [id])
subidoPorId String
subidoPor User @relation(fields: [subidoPorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([obraId])
@@index([faseId])
@@index([fechaCaptura])
}
// ============== NOTIFICACIONES PUSH ==============
enum TipoNotificacion {
TAREA_ASIGNADA
TAREA_COMPLETADA
GASTO_PENDIENTE
GASTO_APROBADO
ORDEN_APROBADA
AVANCE_REGISTRADO
RECORDATORIO
ALERTA_INVENTARIO
GENERAL
}
model PushSubscription {
id String @id @default(cuid())
endpoint String @unique
p256dh String
auth String
activo Boolean @default(true)
// Preferencias de notificación
notifyTareas Boolean @default(true)
notifyGastos Boolean @default(true)
notifyOrdenes Boolean @default(true)
notifyAvances Boolean @default(true)
notifyAlertas Boolean @default(true)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
model Notificacion {
id String @id @default(cuid())
tipo TipoNotificacion
titulo String
mensaje String
url String? // URL para navegar al hacer clic
leida Boolean @default(false)
enviada Boolean @default(false)
// Datos adicionales en JSON
metadata String? @db.Text
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([userId])
@@index([leida])
@@index([createdAt])
}
// ============== LOG DE ACTIVIDADES ==============
enum TipoActividad {
OBRA_CREADA
OBRA_ACTUALIZADA
OBRA_ESTADO_CAMBIADO
FASE_CREADA
TAREA_CREADA
TAREA_ASIGNADA
TAREA_COMPLETADA
TAREA_ESTADO_CAMBIADO
GASTO_CREADO
GASTO_APROBADO
GASTO_RECHAZADO
ORDEN_CREADA
ORDEN_APROBADA
ORDEN_ENVIADA
ORDEN_RECIBIDA
AVANCE_REGISTRADO
FOTO_SUBIDA
BITACORA_REGISTRADA
MATERIAL_MOVIMIENTO
USUARIO_ASIGNADO
COMENTARIO_AGREGADO
DOCUMENTO_SUBIDO
}
model ActividadLog {
id String @id @default(cuid())
tipo TipoActividad
descripcion String
detalles String? @db.Text // JSON con datos adicionales
// Entidad afectada
entidadTipo String? // "obra", "tarea", "gasto", etc.
entidadId String?
entidadNombre String?
// Contexto
obraId String?
obra Obra? @relation(fields: [obraId], references: [id], onDelete: SetNull)
// Usuario que realizó la acción
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
// Empresa para filtrar
empresaId String
// Metadatos de IP/dispositivo (opcional)
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([obraId])
@@index([userId])
@@index([empresaId])
@@index([tipo])
@@index([createdAt])
}