From 7329bac898c78a96833605a2d8599cf85163e221 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:49:16 -0500 Subject: [PATCH] up --- pkg/models/ai_config.go | 8 ++ pkg/models/contrato.go | 32 ++++++- pkg/models/conx_db.go | 12 +++ resources/views/renovaciones/contratos.html | 94 +++++++++++++++++++-- rest/controllers/ai_config_controller.go | 9 ++ rest/controllers/contrato_controller.go | 28 ++++-- rest/controllers/conx_db_controller.go | 9 ++ rest/routes/user.go | 2 + 8 files changed, 176 insertions(+), 18 deletions(-) diff --git a/pkg/models/ai_config.go b/pkg/models/ai_config.go index 3ebe9ed..502d8e6 100644 --- a/pkg/models/ai_config.go +++ b/pkg/models/ai_config.go @@ -95,6 +95,14 @@ func JoinModulos(modules []string) string { return strings.Join(clean, ",") } +func GetAiConfigSelect() ([]AiConfig, error) { + var items []AiConfig + if err := app.Http.Database.DB.Model(&AiConfig{}).Select("id, nombre, provider").Order("nombre ASC").Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} + // GetAiConfigForService retorna la config activa asignada al módulo indicado. // Lógica de prioridad: // 1. Config activa con modulo conteniendo service (puede ser comma-separated) diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go index 0e24d99..ad03489 100644 --- a/pkg/models/contrato.go +++ b/pkg/models/contrato.go @@ -20,6 +20,13 @@ type Contrato struct { Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"` Notas string `json:"notas" gorm:"column:notas"` + // Infraestructura asociada (opcional) + ServidorID *uint `json:"servidor_id" gorm:"column:servidor_id"` + Servidor Servidor `json:"servidor" gorm:"foreignKey:ServidorID"` + ConxDbID *uint `json:"conx_db_id" gorm:"column:conx_db_id"` + ConxDb ConxDb `json:"conx_db" gorm:"foreignKey:ConxDbID"` + AiConfigID *uint `json:"ai_config_id" gorm:"column:ai_config_id"` + AiConfig AiConfig `json:"ai_config" gorm:"foreignKey:AiConfigID"` // Enlace de pago Bold: único por ciclo de pago. // Se reutiliza en múltiples notificaciones del mismo ciclo. // Se anula (vacía) cuando SALE_APPROVED llega y se registra el pago. @@ -36,7 +43,8 @@ func GetAllContratos(limit, offset int, search, estado string) ([]Contrato, int6 var items []Contrato var total int64 db := app.Http.Database.DB.Model(&Contrato{}). - Preload("Cliente").Preload("Servicios") + Preload("Cliente").Preload("Servicios"). + Preload("Servidor").Preload("ConxDb").Preload("AiConfig") if search != "" { db = db.Joins("JOIN clientes ON clientes.id = contratos.cliente_id"). Where("clientes.nombre ILIKE ? OR clientes.empresa ILIKE ?", @@ -56,7 +64,7 @@ func GetAllContratos(limit, offset int, search, estado string) ([]Contrato, int6 func GetContratoByID(id uint) (*Contrato, error) { var item Contrato - if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").First(&item, id).Error; err != nil { + if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").Preload("Servidor").Preload("ConxDb").Preload("AiConfig").First(&item, id).Error; err != nil { return nil, err } return &item, nil @@ -156,7 +164,7 @@ func CreateContrato(c Contrato, servicioIDs []uint) error { func UpdateContrato(c Contrato, servicioIDs []uint) error { db := app.Http.Database.DB - if err := db.Model(&Contrato{}).Where("id = ?", c.ID).Updates(map[string]interface{}{ + updates := map[string]interface{}{ "cliente_id": c.ClienteID, "fecha_inicio": c.FechaInicio, "fecha_vencimiento": c.FechaVencimiento, @@ -165,7 +173,23 @@ func UpdateContrato(c Contrato, servicioIDs []uint) error { "estado": c.Estado, "auto_renovar": c.AutoRenovar, "notas": c.Notas, - }).Error; err != nil { + } + if c.ServidorID != nil { + updates["servidor_id"] = *c.ServidorID + } else { + updates["servidor_id"] = nil + } + if c.ConxDbID != nil { + updates["conx_db_id"] = *c.ConxDbID + } else { + updates["conx_db_id"] = nil + } + if c.AiConfigID != nil { + updates["ai_config_id"] = *c.AiConfigID + } else { + updates["ai_config_id"] = nil + } + if err := db.Model(&Contrato{}).Where("id = ?", c.ID).Updates(updates).Error; err != nil { return err } if servicioIDs != nil { diff --git a/pkg/models/conx_db.go b/pkg/models/conx_db.go index bf01e27..4eaa4fe 100755 --- a/pkg/models/conx_db.go +++ b/pkg/models/conx_db.go @@ -88,3 +88,15 @@ func DeleteConxDb(conxDb ConxDb) error { } return nil } + +func GetConxDbSelect(servidorID string) ([]ConxDb, error) { + var items []ConxDb + db := app.Http.Database.DB.Model(&ConxDb{}).Select("id, nombre, servidor_id").Order("nombre ASC") + if servidorID != "" { + db = db.Where("servidor_id = ?", servidorID) + } + if err := db.Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} diff --git a/resources/views/renovaciones/contratos.html b/resources/views/renovaciones/contratos.html index 56da402..8e6ee48 100644 --- a/resources/views/renovaciones/contratos.html +++ b/resources/views/renovaciones/contratos.html @@ -29,6 +29,7 @@ Cliente Servicios + Infra Vencimiento Días Precio @@ -49,6 +50,29 @@ + +
+ + + + +
+ - Sin registros + Sin registros @@ -186,6 +210,37 @@ +
+

Infraestructura (opcional)

+
+
+ + +
+
+ + +

Sin BD para este servidor

+
+
+ + +
@@ -349,9 +404,10 @@ document.addEventListener('alpine:init', () => { bienvenidaModal: false, bienvenidaReglaId: '', bienvenidaContratoId: null, historialModal: false, historialTitulo: '', historialTimeline: [], historialLoading: false, clientes: [], servicios: [], reglas: [], reglasBienvenida: [], + servidoresList: [], conxDbList: [], aiConfigList: [], selectedId: null, verificandoID: null, - form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'' }, + form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'', servidor_id:'', conx_db_id:'', ai_config_id:'' }, toast: { show: false, msg: '', type: 'ok' }, async init() { @@ -369,10 +425,12 @@ document.addEventListener('alpine:init', () => { async loadSelects() { try { - const [c, s, r] = await Promise.all([ + const [c, s, r, sv, ai] = await Promise.all([ axios.get('/app/api/clientes/select'), axios.get('/app/api/servicios/select'), axios.get('/app/api/reglas-notificacion'), + axios.get('/app/loadservidorselect'), + axios.get('/app/api/ai-config/select'), ]); this.clientes = Array.isArray(c.data) ? c.data : (c.data.registros || []); this.servicios = Array.isArray(s.data) ? s.data : (s.data.registros || []); @@ -380,11 +438,24 @@ document.addEventListener('alpine:init', () => { this.reglas = todasReglas.filter(x => x.activo); this.reglasBienvenida = todasReglas.filter(x => x.activo && x.tipo_evento === 'bienvenida'); if (this.reglasBienvenida.length > 0) this.bienvenidaReglaId = this.reglasBienvenida[0].ID; + this.servidoresList = sv.data.registros || []; + this.aiConfigList = ai.data.registros || []; } catch(e) { this.showToast('Error cargando listas: ' + (e.response?.data?.error || e.message), 'error'); } }, + async onServidorChange() { + this.form.conx_db_id = ''; + if (!this.form.servidor_id) { this.conxDbList = []; return; } + try { + const res = await axios.get('/app/api/conxdb/select', { params: { servidor_id: this.form.servidor_id } }); + this.conxDbList = res.data.registros || []; + } catch(e) { + this.conxDbList = []; + } + }, + get precioSugerido() { return this.servicios .filter(s => (this.form.servicio_ids || []).includes(s.ID)) @@ -400,7 +471,7 @@ document.addEventListener('alpine:init', () => { this.form.precio_acordado = this.precioSugerido; }, - openEdit(d) { + async openEdit(d) { this.form = { cliente_id: d.cliente_id, servicio_ids: (d.servicios || []).map(s => s.ID), @@ -410,16 +481,24 @@ document.addEventListener('alpine:init', () => { moneda: d.moneda || 'COP', estado: d.estado, auto_renovar: d.auto_renovar, - notas: d.notas + notas: d.notas, + servidor_id: d.servidor_id || '', + conx_db_id: d.conx_db_id || '', + ai_config_id: d.ai_config_id || '' }; this.selectedId = d.ID; this.editModal = true; + // Cargar BD del servidor seleccionado + if (d.servidor_id) { + await this.onServidorChange(); + } }, openDelete(d) { this.selectedId = d.ID; this.deleteModal = true; }, closeModals() { this.addModal = this.editModal = this.deleteModal = false; this.selectedId = null; - this.form = { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'' }; + this.conxDbList = []; + this.form = { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'', servidor_id:'', conx_db_id:'', ai_config_id:'' }; }, async save() { @@ -430,6 +509,9 @@ document.addEventListener('alpine:init', () => { cliente_id: parseInt(this.form.cliente_id) || 0, servicio_id: parseInt(this.form.servicio_id) || 0, precio_acordado: parseFloat(this.form.precio_acordado) || 0, + servidor_id: this.form.servidor_id ? parseInt(this.form.servidor_id) : null, + conx_db_id: this.form.conx_db_id ? parseInt(this.form.conx_db_id) : null, + ai_config_id: this.form.ai_config_id ? parseInt(this.form.ai_config_id) : null, }; if (this.editModal) { await axios.put(`/app/api/contratos/${this.selectedId}`, payload); diff --git a/rest/controllers/ai_config_controller.go b/rest/controllers/ai_config_controller.go index 3d23495..36e0cc0 100644 --- a/rest/controllers/ai_config_controller.go +++ b/rest/controllers/ai_config_controller.go @@ -155,6 +155,15 @@ func UpdateAiConfigHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"ok": true}) } +// GetAiConfigSelect devuelve lista simple para selects +func GetAiConfigSelect(c *fiber.Ctx) error { + items, err := models.GetAiConfigSelect() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"registros": items}) +} + // DeleteAiConfigHandler elimina una configuración de IA. func DeleteAiConfigHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) diff --git a/rest/controllers/contrato_controller.go b/rest/controllers/contrato_controller.go index 0483b77..ea05070 100644 --- a/rest/controllers/contrato_controller.go +++ b/rest/controllers/contrato_controller.go @@ -62,13 +62,16 @@ func GetContratos(c *fiber.Ctx) error { func CreateContrato(c *fiber.Ctx) error { type Input struct { - ClienteID uint `json:"cliente_id"` - ServicioIDs []uint `json:"servicio_ids"` - FechaInicio string `json:"fecha_inicio"` - FechaVencimiento string `json:"fecha_vencimiento"` + ClienteID uint `json:"cliente_id"` + ServicioIDs []uint `json:"servicio_ids"` + FechaInicio string `json:"fecha_inicio"` + FechaVencimiento string `json:"fecha_vencimiento"` PrecioAcordado float64 `json:"precio_acordado"` - AutoRenovar bool `json:"auto_renovar"` - Notas string `json:"notas"` + AutoRenovar bool `json:"auto_renovar"` + Notas string `json:"notas"` + ServidorID *uint `json:"servidor_id"` + ConxDbID *uint `json:"conx_db_id"` + AiConfigID *uint `json:"ai_config_id"` } var inp Input if err := c.BodyParser(&inp); err != nil { @@ -86,10 +89,13 @@ func CreateContrato(c *fiber.Ctx) error { ClienteID: inp.ClienteID, FechaInicio: fi, FechaVencimiento: fv, - PrecioAcordado: inp.PrecioAcordado, + PrecioAcordado: float64(inp.PrecioAcordado), AutoRenovar: inp.AutoRenovar, Notas: inp.Notas, Estado: "activo", + ServidorID: inp.ServidorID, + ConxDbID: inp.ConxDbID, + AiConfigID: inp.AiConfigID, } if err := models.CreateContrato(m, inp.ServicioIDs); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) @@ -115,10 +121,13 @@ func UpdateContrato(c *fiber.Ctx) error { PrecioAcordado float64 `json:"precio_acordado"` AutoRenovar bool `json:"auto_renovar"` Notas string `json:"notas"` + ServidorID *uint `json:"servidor_id"` + ConxDbID *uint `json:"conx_db_id"` + AiConfigID *uint `json:"ai_config_id"` } var inp Input if err := c.BodyParser(&inp); err != nil { - return c.Status(400).JSON(fiber.Map{"error": err.Error()}) + return c.Status(400).JSON(fiber.Map{"error": "ID inválido"}) } existing, err := models.GetContratoByID(uint(id)) if err != nil { @@ -136,6 +145,9 @@ func UpdateContrato(c *fiber.Ctx) error { existing.PrecioAcordado = inp.PrecioAcordado existing.AutoRenovar = inp.AutoRenovar existing.Notas = inp.Notas + existing.ServidorID = inp.ServidorID + existing.ConxDbID = inp.ConxDbID + existing.AiConfigID = inp.AiConfigID if err := models.UpdateContrato(*existing, inp.ServicioIDs); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } diff --git a/rest/controllers/conx_db_controller.go b/rest/controllers/conx_db_controller.go index 1910ddd..816d0f6 100755 --- a/rest/controllers/conx_db_controller.go +++ b/rest/controllers/conx_db_controller.go @@ -125,3 +125,12 @@ func DeleteConxDb(c *fiber.Ctx) error { "message": "conx ssh eliminado exitosamente", }) } + +func GetConxDbSelect(c *fiber.Ctx) error { + servidorID := c.Query("servidor_id") + items, err := models.GetConxDbSelect(servidorID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"registros": items}) +} diff --git a/rest/routes/user.go b/rest/routes/user.go index 03954de..2f77cbe 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -65,6 +65,7 @@ func UserRoutes(app fiber.Router) { // Rutas de conexiones db protected.Get("/conexion_db", middlewares.MenuMiddleware, controllers.ConxDb) // Renderizar la vista protected.Get("/loadconexiondb", controllers.GetConxDb) // Obtener + protected.Get("/api/conxdb/select", controllers.GetConxDbSelect) // Select (filtrado por ?servidor_id=) protected.Post("/conexiondb", controllers.CreateConxDb) // Crear protected.Put("/conexiondb/:id", controllers.UpdateConxDb) // Actualizar protected.Delete("/conexiondb/:id", controllers.DeleteConxDb) // Eliminar @@ -284,6 +285,7 @@ func UserRoutes(app fiber.Router) { // ─── Configuraciones de IA (Qwen, OpenAI, etc.) ───────────────────────── protected.Get("/ai-config", middlewares.MenuMiddleware, controllers.AiConfigIndex) protected.Get("/ai-config/list", controllers.GetAiConfigs) + protected.Get("/api/ai-config/select", controllers.GetAiConfigSelect) protected.Post("/ai-config", controllers.CreateAiConfigHandler) protected.Put("/ai-config/:id", controllers.UpdateAiConfigHandler) protected.Delete("/ai-config/:id", controllers.DeleteAiConfigHandler)