From 478c2bde0024c922c10c915ecbeba14022022847 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Wed, 10 Jun 2026 00:18:20 -0500
Subject: [PATCH] up
---
pkg/models/contrato.go | 90 ++++++++-----
resources/views/renovaciones/contratos.html | 133 +++++++++++---------
rest/controllers/contrato_controller.go | 47 +++----
3 files changed, 156 insertions(+), 114 deletions(-)
diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go
index ad03489..9bf0d9b 100644
--- a/pkg/models/contrato.go
+++ b/pkg/models/contrato.go
@@ -20,13 +20,10 @@ 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"`
+ // Infraestructura asociada (opcional, múltiple)
+ Servidores []Servidor `json:"servidores" gorm:"many2many:contrato_servidores"`
+ ConxDBs []ConxDb `json:"conx_dbs" gorm:"many2many:contrato_conx_dbs"`
+ AiConfigs []AiConfig `json:"ai_configs" gorm:"many2many:contrato_ai_configs"`
// 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.
@@ -44,7 +41,7 @@ func GetAllContratos(limit, offset int, search, estado string) ([]Contrato, int6
var total int64
db := app.Http.Database.DB.Model(&Contrato{}).
Preload("Cliente").Preload("Servicios").
- Preload("Servidor").Preload("ConxDb").Preload("AiConfig")
+ Preload("Servidores").Preload("ConxDBs").Preload("AiConfigs")
if search != "" {
db = db.Joins("JOIN clientes ON clientes.id = contratos.cliente_id").
Where("clientes.nombre ILIKE ? OR clientes.empresa ILIKE ?",
@@ -64,7 +61,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").Preload("Servidor").Preload("ConxDb").Preload("AiConfig").First(&item, id).Error; err != nil {
+ if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").Preload("Servidores").Preload("ConxDBs").Preload("AiConfigs").First(&item, id).Error; err != nil {
return nil, err
}
return &item, nil
@@ -154,17 +151,40 @@ func syncContratoServicios(contratoID uint, servicioIDs []uint) error {
return nil
}
-func CreateContrato(c Contrato, servicioIDs []uint) error {
- db := app.Http.Database.DB
- if err := db.Omit("Servicios.*").Create(&c).Error; err != nil {
+func syncContratoJoin(db *gorm.DB, table string, contratoID uint, ids []uint) error {
+ if err := db.Exec("DELETE FROM "+table+" WHERE contrato_id = ?", contratoID).Error; err != nil {
return err
}
- return syncContratoServicios(c.ID, servicioIDs)
+ for _, id := range ids {
+ if err := db.Exec("INSERT INTO "+table+" (contrato_id, "+table[:len(table)-1]+"_id) VALUES (?, ?) ON CONFLICT DO NOTHING",
+ contratoID, id,
+ ).Error; err != nil {
+ return err
+ }
+ }
+ return nil
}
-func UpdateContrato(c Contrato, servicioIDs []uint) error {
+func CreateContrato(c Contrato, servicioIDs, servidorIDs, conxDBIDs, aiConfigIDs []uint) error {
db := app.Http.Database.DB
- updates := map[string]interface{}{
+ if err := db.Omit("Servicios.*", "Servidores.*", "ConxDBs.*", "AiConfigs.*").Create(&c).Error; err != nil {
+ return err
+ }
+ if err := syncContratoServicios(c.ID, servicioIDs); err != nil {
+ return err
+ }
+ if err := syncContratoJoin(db, "contrato_servidores", c.ID, servidorIDs); err != nil {
+ return err
+ }
+ if err := syncContratoJoin(db, "contrato_conx_dbs", c.ID, conxDBIDs); err != nil {
+ return err
+ }
+ return syncContratoJoin(db, "contrato_ai_configs", c.ID, aiConfigIDs)
+}
+
+func UpdateContrato(c Contrato, servicioIDs, servidorIDs, conxDBIDs, aiConfigIDs []uint) error {
+ db := app.Http.Database.DB
+ if err := db.Model(&Contrato{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
"cliente_id": c.ClienteID,
"fecha_inicio": c.FechaInicio,
"fecha_vencimiento": c.FechaVencimiento,
@@ -173,27 +193,28 @@ func UpdateContrato(c Contrato, servicioIDs []uint) error {
"estado": c.Estado,
"auto_renovar": c.AutoRenovar,
"notas": c.Notas,
- }
- 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 {
+ }).Error; err != nil {
return err
}
if servicioIDs != nil {
- return syncContratoServicios(c.ID, servicioIDs)
+ if err := syncContratoServicios(c.ID, servicioIDs); err != nil {
+ return err
+ }
+ }
+ if servidorIDs != nil {
+ if err := syncContratoJoin(db, "contrato_servidores", c.ID, servidorIDs); err != nil {
+ return err
+ }
+ }
+ if conxDBIDs != nil {
+ if err := syncContratoJoin(db, "contrato_conx_dbs", c.ID, conxDBIDs); err != nil {
+ return err
+ }
+ }
+ if aiConfigIDs != nil {
+ if err := syncContratoJoin(db, "contrato_ai_configs", c.ID, aiConfigIDs); err != nil {
+ return err
+ }
}
return nil
}
@@ -205,6 +226,9 @@ func DeleteContrato(id uint) error {
return err
}
db.Exec("DELETE FROM contrato_servicios WHERE contrato_id = ?", id)
+ db.Exec("DELETE FROM contrato_servidores WHERE contrato_id = ?", id)
+ db.Exec("DELETE FROM contrato_conx_dbs WHERE contrato_id = ?", id)
+ db.Exec("DELETE FROM contrato_ai_configs WHERE contrato_id = ?", id)
return db.Delete(&c).Error
}
diff --git a/resources/views/renovaciones/contratos.html b/resources/views/renovaciones/contratos.html
index d025362..a222270 100644
--- a/resources/views/renovaciones/contratos.html
+++ b/resources/views/renovaciones/contratos.html
@@ -52,25 +52,25 @@
-
-
+
+
-
+
-
-
+
+
-
+
-
-
+
+
-
+
- —
+ —
|
|
@@ -211,35 +211,51 @@
-
Infraestructura (opcional)
-
-
-
-
-
-
-
-
-
Sin BD para este servidor
+
Infraestructura (opcional, múltiple)
-
-
+
+
+
+
+
+
+
+
+
+
+
Sin configs de IA
+
@@ -407,7 +423,7 @@ document.addEventListener('alpine:init', () => {
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:'', servidor_id:'', conx_db_id:'', ai_config_id:'' },
+ form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'', servidor_ids:[], conx_db_ids:[], ai_config_ids:[] },
toast: { show: false, msg: '', type: 'ok' },
async init() {
@@ -425,12 +441,13 @@ document.addEventListener('alpine:init', () => {
async loadSelects() {
try {
- const [c, s, r, sv, ai] = await Promise.all([
+ const [c, s, r, sv, ai, db] = 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'),
+ axios.get('/app/api/conxdb/select'),
]);
this.clientes = Array.isArray(c.data) ? c.data : (c.data.registros || []);
this.servicios = Array.isArray(s.data) ? s.data : (s.data.registros || []);
@@ -440,20 +457,23 @@ document.addEventListener('alpine:init', () => {
if (this.reglasBienvenida.length > 0) this.bienvenidaReglaId = this.reglasBienvenida[0].ID;
this.servidoresList = sv.data.registros || [];
this.aiConfigList = ai.data.registros || [];
+ this.conxDbList = db.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 = [];
- }
+ toggleServidor(id) {
+ const idx = (this.form.servidor_ids || []).indexOf(id);
+ if (idx === -1) { this.form.servidor_ids.push(id); } else { this.form.servidor_ids.splice(idx, 1); }
+ },
+ toggleConxDb(id) {
+ const idx = (this.form.conx_db_ids || []).indexOf(id);
+ if (idx === -1) { this.form.conx_db_ids.push(id); } else { this.form.conx_db_ids.splice(idx, 1); }
+ },
+ toggleAiConfig(id) {
+ const idx = (this.form.ai_config_ids || []).indexOf(id);
+ if (idx === -1) { this.form.ai_config_ids.push(id); } else { this.form.ai_config_ids.splice(idx, 1); }
},
get precioSugerido() {
@@ -482,23 +502,18 @@ document.addEventListener('alpine:init', () => {
estado: d.estado,
auto_renovar: d.auto_renovar,
notas: d.notas,
- servidor_id: d.servidor_id || '',
- conx_db_id: d.conx_db_id || '',
- ai_config_id: d.ai_config_id || ''
+ servidor_ids: (d.servidores || []).map(s => s.ID),
+ conx_db_ids: (d.conx_dbs || []).map(s => s.ID),
+ ai_config_ids: (d.ai_configs || []).map(s => s.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.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:'' };
+ this.form = { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, moneda:'COP', estado:'activo', auto_renovar:false, notas:'', servidor_ids:[], conx_db_ids:[], ai_config_ids:[] };
},
async save() {
@@ -509,9 +524,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,
+ servidor_ids: this.form.servidor_ids || [],
+ conx_db_ids: this.form.conx_db_ids || [],
+ ai_config_ids: this.form.ai_config_ids || [],
};
if (this.editModal) {
await axios.put(`/app/api/contratos/${this.selectedId}`, payload);
diff --git a/rest/controllers/contrato_controller.go b/rest/controllers/contrato_controller.go
index ea05070..eebf9c9 100644
--- a/rest/controllers/contrato_controller.go
+++ b/rest/controllers/contrato_controller.go
@@ -62,16 +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"`
- ServidorID *uint `json:"servidor_id"`
- ConxDbID *uint `json:"conx_db_id"`
- AiConfigID *uint `json:"ai_config_id"`
+ AutoRenovar bool `json:"auto_renovar"`
+ Notas string `json:"notas"`
+ ServidorIDs []uint `json:"servidor_ids"`
+ ConxDbIDs []uint `json:"conx_db_ids"`
+ AiConfigIDs []uint `json:"ai_config_ids"`
}
var inp Input
if err := c.BodyParser(&inp); err != nil {
@@ -93,11 +93,17 @@ func CreateContrato(c *fiber.Ctx) error {
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 {
+ if inp.ServidorIDs == nil {
+ inp.ServidorIDs = []uint{}
+ }
+ if inp.ConxDbIDs == nil {
+ inp.ConxDbIDs = []uint{}
+ }
+ if inp.AiConfigIDs == nil {
+ inp.AiConfigIDs = []uint{}
+ }
+ if err := models.CreateContrato(m, inp.ServicioIDs, inp.ServidorIDs, inp.ConxDbIDs, inp.AiConfigIDs); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Devolver el ID del contrato creado para que el frontend pueda enviar bienvenida
@@ -121,9 +127,9 @@ 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"`
+ ServidorIDs []uint `json:"servidor_ids"`
+ ConxDbIDs []uint `json:"conx_db_ids"`
+ AiConfigIDs []uint `json:"ai_config_ids"`
}
var inp Input
if err := c.BodyParser(&inp); err != nil {
@@ -145,10 +151,7 @@ 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 {
+ if err := models.UpdateContrato(*existing, inp.ServicioIDs, inp.ServidorIDs, inp.ConxDbIDs, inp.AiConfigIDs); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
@@ -185,12 +188,12 @@ func RenovarContrato(c *fiber.Ctx) error {
Estado: "activo",
Notas: "Renovación automática desde contrato #" + strconv.Itoa(int(existing.ID)),
}
- if err := models.CreateContrato(nuevo, servicioIDs); err != nil {
+ if err := models.CreateContrato(nuevo, servicioIDs, nil, nil, nil); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Marcar anterior como renovado
existing.Estado = "renovado"
- models.UpdateContrato(*existing, nil)
+ models.UpdateContrato(*existing, nil, nil, nil, nil)
return c.JSON(fiber.Map{"message": "Renovado", "ok": true})
}