This commit is contained in:
Lizandro Guarnizo
2026-06-10 00:18:20 -05:00
parent d701534537
commit 478c2bde00
3 changed files with 156 additions and 114 deletions
+57 -33
View File
@@ -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
}
+74 -59
View File
@@ -52,25 +52,25 @@
</td>
<td class="py-2 px-3">
<div class="flex gap-1 flex-wrap">
<template x-if="d.servidor">
<span class="inline-flex items-center gap-0.5 text-xs bg-blue-50 text-blue-700 rounded px-1.5 py-0.5" title="Servidor: " + d.servidor.nombre>
<template x-for="s in (d.servidores||[])" :key="'sv-'+s.ID">
<span class="inline-flex items-center gap-0.5 text-xs bg-blue-50 text-blue-700 rounded px-1.5 py-0.5" :title="'Servidor: '+s.nombre">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.75L4.5 7.5l.75-1.5m-2.25 6.75h16.5"/></svg>
<span x-text="d.servidor.nombre"></span>
<span x-text="s.nombre"></span>
</span>
</template>
<template x-if="d.conx_db">
<span class="inline-flex items-center gap-0.5 text-xs bg-green-50 text-green-700 rounded px-1.5 py-0.5" title="BD: " + d.conx_db.nombre>
<template x-for="d in (d.conx_dbs||[])" :key="'db-'+d.ID">
<span class="inline-flex items-center gap-0.5 text-xs bg-green-50 text-green-700 rounded px-1.5 py-0.5" :title="'BD: '+d.nombre">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"/></svg>
<span x-text="d.conx_db.nombre"></span>
<span x-text="d.nombre"></span>
</span>
</template>
<template x-if="d.ai_config">
<span class="inline-flex items-center gap-0.5 text-xs bg-purple-50 text-purple-700 rounded px-1.5 py-0.5" title="IA: " + d.ai_config.nombre>
<template x-for="a in (d.ai_configs||[])" :key="'ai-'+a.ID">
<span class="inline-flex items-center gap-0.5 text-xs bg-purple-50 text-purple-700 rounded px-1.5 py-0.5" :title="'IA: '+a.nombre">
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 0 0-2.455 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"/></svg>
<span x-text="d.ai_config.nombre"></span>
<span x-text="a.nombre"></span>
</span>
</template>
<span x-show="!d.servidor && !d.conx_db && !d.ai_config" class="text-gray-300"></span>
<span x-show="!(d.servidores||[]).length && !(d.conx_dbs||[]).length && !(d.ai_configs||[]).length" class="text-gray-300"></span>
</div>
</td>
<td class="py-2 px-3" x-text="fmtDate(d.fecha_vencimiento)"></td>
@@ -211,35 +211,51 @@
<textarea x-model="form.notas" rows="2" class="mt-1 w-full border rounded px-3 py-2 text-sm"></textarea>
</div>
<div class="col-span-2 border-t border-gray-100 pt-3 mt-1">
<p class="text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wider">Infraestructura <span class="font-normal normal-case text-gray-400">(opcional)</span></p>
</div>
<div class="col-span-2 sm:col-span-1">
<label class="text-xs font-medium text-gray-600">Servidor</label>
<select x-model="form.servidor_id" @change="onServidorChange()" class="mt-1 w-full border rounded px-3 py-2 text-sm">
<option value="">— Sin servidor —</option>
<template x-for="s in servidoresList" :key="s.ID">
<option :value="s.ID" x-text="s.nombre + ' — ' + (s.ip_servidor||'')"></option>
</template>
</select>
</div>
<div class="col-span-2 sm:col-span-1">
<label class="text-xs font-medium text-gray-600">Base de datos</label>
<select x-model="form.conx_db_id" class="mt-1 w-full border rounded px-3 py-2 text-sm">
<option value="">— Sin BD —</option>
<template x-for="db in conxDbList" :key="db.ID">
<option :value="db.ID" x-text="db.nombre"></option>
</template>
</select>
<p x-show="form.servidor_id && conxDbList.length===0" class="text-xs text-gray-400 mt-0.5">Sin BD para este servidor</p>
<p class="text-xs font-semibold text-gray-500 mb-2 uppercase tracking-wider">Infraestructura <span class="font-normal normal-case text-gray-400">(opcional, múltiple)</span></p>
</div>
<div class="col-span-2">
<label class="text-xs font-medium text-gray-600">IA / AI Config</label>
<select x-model="form.ai_config_id" class="mt-1 w-full border rounded px-3 py-2 text-sm">
<option value="">— Sin IA —</option>
<template x-for="ai in aiConfigList" :key="ai.ID">
<option :value="ai.ID" x-text="ai.nombre + ' (' + ai.provider + ')'"></option>
<label class="text-xs font-medium text-gray-600 mb-1 block">Servidores</label>
<div class="border rounded max-h-32 overflow-y-auto divide-y text-sm">
<template x-for="s in servidoresList" :key="s.ID">
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" :value="s.ID"
:checked="form.servidor_ids.includes(s.ID)"
@change="toggleServidor(s.ID)" />
<span x-text="s.nombre + ' — ' + (s.ip_servidor||'')"></span>
</label>
</template>
</select>
<div x-show="servidoresList.length===0" class="text-xs text-gray-400 px-3 py-2">Sin servidores</div>
</div>
</div>
<div class="col-span-2">
<label class="text-xs font-medium text-gray-600 mb-1 block">Bases de datos <span class="font-normal text-gray-400">(filtradas por servidor seleccionado)</span></label>
<div class="border rounded max-h-32 overflow-y-auto divide-y text-sm">
<template x-for="db in conxDbList" :key="db.ID">
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" :value="db.ID"
:checked="form.conx_db_ids.includes(db.ID)"
@change="toggleConxDb(db.ID)" />
<span x-text="db.nombre"></span>
</label>
</template>
<div x-show="conxDbList.length===0" class="text-xs text-gray-400 px-3 py-2">
<span x-text="form.servidor_ids.length > 0 ? 'Sin BD para los servidores seleccionados' : 'Selecciona un servidor primero'"></span>
</div>
</div>
</div>
<div class="col-span-2">
<label class="text-xs font-medium text-gray-600 mb-1 block">IA / AI Configs</label>
<div class="border rounded max-h-32 overflow-y-auto divide-y text-sm">
<template x-for="ai in aiConfigList" :key="ai.ID">
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 cursor-pointer">
<input type="checkbox" :value="ai.ID"
:checked="form.ai_config_ids.includes(ai.ID)"
@change="toggleAiConfig(ai.ID)" />
<span x-text="ai.nombre + ' (' + ai.provider + ')'"></span>
</label>
</template>
<div x-show="aiConfigList.length===0" class="text-xs text-gray-400 px-3 py-2">Sin configs de IA</div>
</div>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" x-model="form.auto_renovar" id="auto_ren" />
@@ -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);
+25 -22
View File
@@ -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})
}