feat: contrato soporta múltiples servicios (many2many) con precio sugerido

This commit is contained in:
Lizandro Guarnizo
2026-04-30 22:15:31 -05:00
parent 1b0374204c
commit e751a9f894
5 changed files with 157 additions and 76 deletions
+41 -20
View File
@@ -10,16 +10,15 @@ import (
type Contrato struct { type Contrato struct {
gorm.Model gorm.Model
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id"` ClienteID uint `json:"cliente_id" gorm:"column:cliente_id"`
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"` Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
ServicioID uint `json:"servicio_id" gorm:"column:servicio_id"` Servicios []Servicio `json:"servicios" gorm:"many2many:contrato_servicios"`
Servicio Servicio `json:"servicio" gorm:"foreignKey:ServicioID"` FechaInicio time.Time `json:"fecha_inicio" gorm:"column:fecha_inicio"`
FechaInicio time.Time `json:"fecha_inicio" gorm:"column:fecha_inicio"` FechaVencimiento time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
FechaVencimiento time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"` PrecioAcordado float64 `json:"precio_acordado" gorm:"column:precio_acordado"`
PrecioAcordado float64 `json:"precio_acordado" gorm:"column:precio_acordado"` Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado
Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"`
AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"` Notas string `json:"notas" gorm:"column:notas"`
Notas string `json:"notas" gorm:"column:notas"`
} }
func (Contrato) TableName() string { return "contratos" } func (Contrato) TableName() string { return "contratos" }
@@ -28,7 +27,7 @@ func GetAllContratos(limit, offset int, search, estado string) ([]Contrato, int6
var items []Contrato var items []Contrato
var total int64 var total int64
db := app.Http.Database.DB.Model(&Contrato{}). db := app.Http.Database.DB.Model(&Contrato{}).
Preload("Cliente").Preload("Servicio") Preload("Cliente").Preload("Servicios")
if search != "" { if search != "" {
db = db.Joins("JOIN clientes ON clientes.id = contratos.cliente_id"). db = db.Joins("JOIN clientes ON clientes.id = contratos.cliente_id").
Where("clientes.nombre ILIKE ? OR clientes.empresa ILIKE ?", Where("clientes.nombre ILIKE ? OR clientes.empresa ILIKE ?",
@@ -48,7 +47,7 @@ func GetAllContratos(limit, offset int, search, estado string) ([]Contrato, int6
func GetContratoByID(id uint) (*Contrato, error) { func GetContratoByID(id uint) (*Contrato, error) {
var item Contrato var item Contrato
if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicio").First(&item, id).Error; err != nil { if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").First(&item, id).Error; err != nil {
return nil, err return nil, err
} }
return &item, nil return &item, nil
@@ -57,7 +56,7 @@ func GetContratoByID(id uint) (*Contrato, error) {
// GetContratosByCliente devuelve contratos de un cliente específico // GetContratosByCliente devuelve contratos de un cliente específico
func GetContratosByCliente(clienteID uint) ([]Contrato, error) { func GetContratosByCliente(clienteID uint) ([]Contrato, error) {
var items []Contrato var items []Contrato
if err := app.Http.Database.DB.Preload("Servicio"). if err := app.Http.Database.DB.Preload("Servicios").
Where("cliente_id = ?", clienteID). Where("cliente_id = ?", clienteID).
Order("fecha_vencimiento ASC").Find(&items).Error; err != nil { Order("fecha_vencimiento ASC").Find(&items).Error; err != nil {
return nil, err return nil, err
@@ -72,7 +71,7 @@ func GetContratosProximosVencer(diasAntes int) ([]Contrato, error) {
startOfDay := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, time.UTC) startOfDay := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, time.UTC)
endOfDay := startOfDay.Add(24 * time.Hour) endOfDay := startOfDay.Add(24 * time.Hour)
if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicio"). if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").
Where("estado = 'activo' AND fecha_vencimiento >= ? AND fecha_vencimiento < ?", startOfDay, endOfDay). Where("estado = 'activo' AND fecha_vencimiento >= ? AND fecha_vencimiento < ?", startOfDay, endOfDay).
Find(&items).Error; err != nil { Find(&items).Error; err != nil {
log.Printf("Error getting contratos proximos: %v", err) log.Printf("Error getting contratos proximos: %v", err)
@@ -81,21 +80,42 @@ func GetContratosProximosVencer(diasAntes int) ([]Contrato, error) {
return items, nil return items, nil
} }
func CreateContrato(c Contrato) error { func CreateContrato(c Contrato, servicioIDs []uint) error {
return app.Http.Database.DB.Create(&c).Error db := app.Http.Database.DB
if err := db.Create(&c).Error; err != nil {
return err
}
if len(servicioIDs) > 0 {
var servicios []Servicio
if err := db.Find(&servicios, servicioIDs).Error; err != nil {
return err
}
return db.Model(&c).Association("Servicios").Replace(servicios)
}
return nil
} }
func UpdateContrato(c Contrato) error { func UpdateContrato(c Contrato, servicioIDs []uint) error {
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", c.ID).Updates(map[string]interface{}{ db := app.Http.Database.DB
if err := db.Model(&Contrato{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
"cliente_id": c.ClienteID, "cliente_id": c.ClienteID,
"servicio_id": c.ServicioID,
"fecha_inicio": c.FechaInicio, "fecha_inicio": c.FechaInicio,
"fecha_vencimiento": c.FechaVencimiento, "fecha_vencimiento": c.FechaVencimiento,
"precio_acordado": c.PrecioAcordado, "precio_acordado": c.PrecioAcordado,
"estado": c.Estado, "estado": c.Estado,
"auto_renovar": c.AutoRenovar, "auto_renovar": c.AutoRenovar,
"notas": c.Notas, "notas": c.Notas,
}).Error }).Error; err != nil {
return err
}
if servicioIDs != nil {
var servicios []Servicio
if err := db.Find(&servicios, servicioIDs).Error; err != nil {
return err
}
return db.Model(&Contrato{Model: gorm.Model{ID: c.ID}}).Association("Servicios").Replace(servicios)
}
return nil
} }
func DeleteContrato(id uint) error { func DeleteContrato(id uint) error {
@@ -103,5 +123,6 @@ func DeleteContrato(id uint) error {
if err := app.Http.Database.DB.First(&c, id).Error; err != nil { if err := app.Http.Database.DB.First(&c, id).Error; err != nil {
return err return err
} }
app.Http.Database.DB.Model(&c).Association("Servicios").Clear()
return app.Http.Database.DB.Delete(&c).Error return app.Http.Database.DB.Delete(&c).Error
} }
+10 -4
View File
@@ -56,12 +56,18 @@ func ProcesarVencimientos() {
for _, c := range contratos { for _, c := range contratos {
switch regla.AplicaA { switch regla.AplicaA {
case "renovable": case "renovable":
if c.Servicio.Tipo == "renovable" { for _, s := range c.Servicios {
filtrados = append(filtrados, c) if s.Tipo == "renovable" {
filtrados = append(filtrados, c)
break
}
} }
case "unico": case "unico":
if c.Servicio.Tipo == "unico" { for _, s := range c.Servicios {
filtrados = append(filtrados, c) if s.Tipo == "unico" {
filtrados = append(filtrados, c)
break
}
} }
default: default:
filtrados = append(filtrados, c) filtrados = append(filtrados, c)
+18 -12
View File
@@ -82,6 +82,15 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
return err return err
} }
dias := int(contrato.FechaVencimiento.Sub(time.Now()).Hours() / 24) dias := int(contrato.FechaVencimiento.Sub(time.Now()).Hours() / 24)
var items []ItemServicio
for _, s := range contrato.Servicios {
items = append(items, ItemServicio{
Nombre: s.Nombre,
Precio: s.Precio,
Moneda: s.Moneda,
FechaVenc: contrato.FechaVencimiento.Format("02/01/2006"),
})
}
datos := DatosPlantilla{ datos := DatosPlantilla{
ClienteNombre: contrato.Cliente.Nombre, ClienteNombre: contrato.Cliente.Nombre,
ClienteEmpresa: contrato.Cliente.Empresa, ClienteEmpresa: contrato.Cliente.Empresa,
@@ -89,12 +98,7 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
FechaVencimiento: contrato.FechaVencimiento.Format("02/01/2006"), FechaVencimiento: contrato.FechaVencimiento.Format("02/01/2006"),
DiasRestantes: dias, DiasRestantes: dias,
Total: contrato.PrecioAcordado, Total: contrato.PrecioAcordado,
Servicios: []ItemServicio{{ Servicios: items,
Nombre: contrato.Servicio.Nombre,
Precio: contrato.PrecioAcordado,
Moneda: contrato.Servicio.Moneda,
FechaVenc: contrato.FechaVencimiento.Format("02/01/2006"),
}},
} }
html, err := RenderPlantilla(p, datos) html, err := RenderPlantilla(p, datos)
if err != nil { if err != nil {
@@ -115,12 +119,14 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl
var fechaVenc time.Time var fechaVenc time.Time
for _, c := range contratos { for _, c := range contratos {
items = append(items, ItemServicio{ for _, s := range c.Servicios {
Nombre: c.Servicio.Nombre, items = append(items, ItemServicio{
Precio: c.PrecioAcordado, Nombre: s.Nombre,
Moneda: c.Servicio.Moneda, Precio: s.Precio,
FechaVenc: c.FechaVencimiento.Format("02/01/2006"), Moneda: s.Moneda,
}) FechaVenc: c.FechaVencimiento.Format("02/01/2006"),
})
}
total += c.PrecioAcordado total += c.PrecioAcordado
if fechaVenc.IsZero() || c.FechaVencimiento.Before(fechaVenc) { if fechaVenc.IsZero() || c.FechaVencimiento.Before(fechaVenc) {
fechaVenc = c.FechaVencimiento fechaVenc = c.FechaVencimiento
+52 -17
View File
@@ -28,7 +28,7 @@
<thead class="text-left border-b border-gray-200 bg-gray-50"> <thead class="text-left border-b border-gray-200 bg-gray-50">
<tr> <tr>
<th class="py-2 px-3">Cliente</th> <th class="py-2 px-3">Cliente</th>
<th class="py-2 px-3">Servicio</th> <th class="py-2 px-3">Servicios</th>
<th class="py-2 px-3">Vencimiento</th> <th class="py-2 px-3">Vencimiento</th>
<th class="py-2 px-3">Días</th> <th class="py-2 px-3">Días</th>
<th class="py-2 px-3">Precio</th> <th class="py-2 px-3">Precio</th>
@@ -43,14 +43,19 @@
<div class="font-medium" x-text="d.cliente?.nombre||'—'"></div> <div class="font-medium" x-text="d.cliente?.nombre||'—'"></div>
<div class="text-xs text-gray-400" x-text="d.cliente?.empresa||''"></div> <div class="text-xs text-gray-400" x-text="d.cliente?.empresa||''"></div>
</td> </td>
<td class="py-2 px-3" x-text="d.servicio?.nombre||'—'"></td> <td class="py-2 px-3">
<template x-for="s in (d.servicios||[])" :key="s.ID">
<span class="inline-block text-xs bg-gray-100 rounded px-1.5 py-0.5 mr-1 mb-0.5" x-text="s.nombre"></span>
</template>
<span x-show="!d.servicios||d.servicios.length===0" class="text-gray-400"></span>
</td>
<td class="py-2 px-3" x-text="fmtDate(d.fecha_vencimiento)"></td> <td class="py-2 px-3" x-text="fmtDate(d.fecha_vencimiento)"></td>
<td class="py-2 px-3"> <td class="py-2 px-3">
<span class="px-2 py-0.5 rounded text-xs font-semibold" <span class="px-2 py-0.5 rounded text-xs font-semibold"
:class="d.urgencia==='rojo' ? 'bg-red-100 text-red-700' : d.urgencia==='amarillo' ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'" :class="d.urgencia==='rojo' ? 'bg-red-100 text-red-700' : d.urgencia==='amarillo' ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'"
x-text="d.dias_restantes <= 0 ? 'Vencido' : d.dias_restantes+' días'"></span> x-text="d.dias_restantes <= 0 ? 'Vencido' : d.dias_restantes+' días'"></span>
</td> </td>
<td class="py-2 px-3" x-text="(d.servicio?.moneda||'') +' '+ Number(d.precio_acordado).toFixed(2)"></td> <td class="py-2 px-3" x-text="Number(d.precio_acordado).toFixed(2)"></td>
<td class="py-2 px-3"> <td class="py-2 px-3">
<span class="px-2 py-0.5 rounded text-xs font-medium" <span class="px-2 py-0.5 rounded text-xs font-medium"
:class="{ :class="{
@@ -111,14 +116,20 @@
</template> </template>
</select> </select>
</div> </div>
<div class="col-span-2" x-show="!editModal"> <div class="col-span-2">
<label class="text-xs font-medium text-gray-600">Servicio *</label> <label class="text-xs font-medium text-gray-600">Servicios <span class="font-normal text-gray-400">(uno o varios)</span></label>
<select x-model="form.servicio_id" required class="mt-1 w-full border rounded px-3 py-2 text-sm"> <div class="mt-1 border rounded max-h-36 overflow-y-auto divide-y text-sm">
<option value="">Seleccionar...</option>
<template x-for="s in servicios" :key="s.ID"> <template x-for="s in servicios" :key="s.ID">
<option :value="s.ID" x-text="s.nombre"></option> <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.servicio_ids.includes(s.ID)"
@change="toggleServicio(s.ID)" />
<span class="flex-1" x-text="s.nombre"></span>
<span class="text-xs text-gray-400" x-text="s.moneda+' '+Number(s.precio).toFixed(2)"></span>
</label>
</template> </template>
</select> <div x-show="servicios.length===0" class="text-xs text-gray-400 px-3 py-2">Sin servicios disponibles</div>
</div>
</div> </div>
<div> <div>
<label class="text-xs font-medium text-gray-600">Fecha inicio *</label> <label class="text-xs font-medium text-gray-600">Fecha inicio *</label>
@@ -131,6 +142,9 @@
<div> <div>
<label class="text-xs font-medium text-gray-600">Precio acordado</label> <label class="text-xs font-medium text-gray-600">Precio acordado</label>
<input x-model="form.precio_acordado" type="number" step="0.01" class="mt-1 w-full border rounded px-3 py-2 text-sm" /> <input x-model="form.precio_acordado" type="number" step="0.01" class="mt-1 w-full border rounded px-3 py-2 text-sm" />
<p class="text-xs text-gray-400 mt-0.5" x-show="precioSugerido > 0">
Sugerido: <span class="font-medium text-gray-600" x-text="precioSugerido.toFixed(2)"></span>
</p>
</div> </div>
<div x-show="editModal"> <div x-show="editModal">
<label class="text-xs font-medium text-gray-600">Estado</label> <label class="text-xs font-medium text-gray-600">Estado</label>
@@ -188,7 +202,7 @@ document.addEventListener('alpine:init', () => {
addModal: false, editModal: false, deleteModal: false, addModal: false, editModal: false, deleteModal: false,
clientes: [], servicios: [], clientes: [], servicios: [],
selectedId: null, selectedId: null,
form: { cliente_id:'', servicio_id:'', fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' }, form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' },
toast: { show: false, msg: '', type: 'ok' }, toast: { show: false, msg: '', type: 'ok' },
async init() { async init() {
@@ -205,16 +219,37 @@ document.addEventListener('alpine:init', () => {
}, },
async loadSelects() { async loadSelects() {
const [c, s] = await Promise.all([ try {
axios.get('/app/api/clientes/select'), const [c, s] = await Promise.all([
axios.get('/app/api/servicios/select'), axios.get('/app/api/clientes/select'),
]); axios.get('/app/api/servicios/select'),
this.clientes = c.data.registros || []; ]);
this.servicios = s.data.registros || []; this.clientes = Array.isArray(c.data) ? c.data : (c.data.registros || []);
this.servicios = Array.isArray(s.data) ? s.data : (s.data.registros || []);
} catch(e) {
this.showToast('Error cargando listas: ' + (e.response?.data?.error || e.message), 'error');
}
},
get precioSugerido() {
return this.servicios
.filter(s => (this.form.servicio_ids || []).includes(s.ID))
.reduce((acc, s) => acc + (parseFloat(s.precio) || 0), 0);
},
toggleServicio(id) {
const idx = (this.form.servicio_ids || []).indexOf(id);
if (idx === -1) {
this.form.servicio_ids.push(id);
} else {
this.form.servicio_ids.splice(idx, 1);
}
this.form.precio_acordado = this.precioSugerido;
}, },
openEdit(d) { openEdit(d) {
this.form = { this.form = {
cliente_id: d.cliente_id,
servicio_ids: (d.servicios || []).map(s => s.ID),
fecha_inicio: d.fecha_inicio ? d.fecha_inicio.substring(0,10) : '', fecha_inicio: d.fecha_inicio ? d.fecha_inicio.substring(0,10) : '',
fecha_vencimiento: d.fecha_vencimiento ? d.fecha_vencimiento.substring(0,10) : '', fecha_vencimiento: d.fecha_vencimiento ? d.fecha_vencimiento.substring(0,10) : '',
precio_acordado: d.precio_acordado, precio_acordado: d.precio_acordado,
@@ -229,7 +264,7 @@ document.addEventListener('alpine:init', () => {
closeModals() { closeModals() {
this.addModal = this.editModal = this.deleteModal = false; this.addModal = this.editModal = this.deleteModal = false;
this.selectedId = null; this.selectedId = null;
this.form = { cliente_id:'', servicio_id:'', fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' }; this.form = { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' };
}, },
async save() { async save() {
+36 -23
View File
@@ -31,8 +31,9 @@ func GetContratos(c *fiber.Ctx) error {
// Enriquecer con días restantes // Enriquecer con días restantes
type ContratoDTO struct { type ContratoDTO struct {
models.Contrato models.Contrato
DiasRestantes int `json:"dias_restantes"` DiasRestantes int `json:"dias_restantes"`
Urgencia string `json:"urgencia"` // verde | amarillo | rojo Urgencia string `json:"urgencia"` // verde | amarillo | rojo
PrecioSugerido float64 `json:"precio_sugerido"`
} }
dtos := make([]ContratoDTO, len(records)) dtos := make([]ContratoDTO, len(records))
now := time.Now() now := time.Now()
@@ -44,7 +45,11 @@ func GetContratos(c *fiber.Ctx) error {
} else if dias <= 30 { } else if dias <= 30 {
urgencia = "amarillo" urgencia = "amarillo"
} }
dtos[i] = ContratoDTO{Contrato: r, DiasRestantes: dias, Urgencia: urgencia} var sugerido float64
for _, s := range r.Servicios {
sugerido += s.Precio
}
dtos[i] = ContratoDTO{Contrato: r, DiasRestantes: dias, Urgencia: urgencia, PrecioSugerido: sugerido}
} }
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"registros": dtos, "registros": dtos,
@@ -57,13 +62,13 @@ func GetContratos(c *fiber.Ctx) error {
func CreateContrato(c *fiber.Ctx) error { func CreateContrato(c *fiber.Ctx) error {
type Input struct { type Input struct {
ClienteID uint `json:"cliente_id" form:"cliente_id"` ClienteID uint `json:"cliente_id"`
ServicioID uint `json:"servicio_id" form:"servicio_id"` ServicioIDs []uint `json:"servicio_ids"`
FechaInicio string `json:"fecha_inicio" form:"fecha_inicio"` FechaInicio string `json:"fecha_inicio"`
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"` FechaVencimiento string `json:"fecha_vencimiento"`
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"` PrecioAcordado float64 `json:"precio_acordado"`
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"` AutoRenovar bool `json:"auto_renovar"`
Notas string `json:"notas" form:"notas"` Notas string `json:"notas"`
} }
var inp Input var inp Input
if err := c.BodyParser(&inp); err != nil { if err := c.BodyParser(&inp); err != nil {
@@ -79,7 +84,6 @@ func CreateContrato(c *fiber.Ctx) error {
} }
m := models.Contrato{ m := models.Contrato{
ClienteID: inp.ClienteID, ClienteID: inp.ClienteID,
ServicioID: inp.ServicioID,
FechaInicio: fi, FechaInicio: fi,
FechaVencimiento: fv, FechaVencimiento: fv,
PrecioAcordado: inp.PrecioAcordado, PrecioAcordado: inp.PrecioAcordado,
@@ -87,7 +91,7 @@ func CreateContrato(c *fiber.Ctx) error {
Notas: inp.Notas, Notas: inp.Notas,
Estado: "activo", Estado: "activo",
} }
if err := models.CreateContrato(m); err != nil { if err := models.CreateContrato(m, inp.ServicioIDs); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()}) return c.Status(500).JSON(fiber.Map{"error": err.Error()})
} }
return c.Status(201).JSON(fiber.Map{"message": "Contrato creado", "ok": true}) return c.Status(201).JSON(fiber.Map{"message": "Contrato creado", "ok": true})
@@ -99,11 +103,12 @@ func UpdateContrato(c *fiber.Ctx) error {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"}) return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
} }
type Input struct { type Input struct {
Estado string `json:"estado" form:"estado"` ServicioIDs []uint `json:"servicio_ids"`
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"` Estado string `json:"estado"`
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"` FechaVencimiento string `json:"fecha_vencimiento"`
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"` PrecioAcordado float64 `json:"precio_acordado"`
Notas string `json:"notas" form:"notas"` AutoRenovar bool `json:"auto_renovar"`
Notas string `json:"notas"`
} }
var inp Input var inp Input
if err := c.BodyParser(&inp); err != nil { if err := c.BodyParser(&inp); err != nil {
@@ -125,7 +130,7 @@ func UpdateContrato(c *fiber.Ctx) error {
existing.PrecioAcordado = inp.PrecioAcordado existing.PrecioAcordado = inp.PrecioAcordado
existing.AutoRenovar = inp.AutoRenovar existing.AutoRenovar = inp.AutoRenovar
existing.Notas = inp.Notas existing.Notas = inp.Notas
if err := models.UpdateContrato(*existing); err != nil { if err := models.UpdateContrato(*existing, inp.ServicioIDs); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()}) return c.Status(500).JSON(fiber.Map{"error": err.Error()})
} }
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true}) return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
@@ -140,13 +145,21 @@ func RenovarContrato(c *fiber.Ctx) error {
if err != nil { if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"}) return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
} }
// Calcular nueva fecha según periodicidad del servicio // Usar periodicidad del primer servicio asociado
periodicidad := ""
if len(existing.Servicios) > 0 {
periodicidad = existing.Servicios[0].Periodicidad
}
nuevaInicio := existing.FechaVencimiento.AddDate(0, 0, 1) nuevaInicio := existing.FechaVencimiento.AddDate(0, 0, 1)
nuevaVenc := calcularFechaVencimiento(nuevaInicio, existing.Servicio.Periodicidad) nuevaVenc := calcularFechaVencimiento(nuevaInicio, periodicidad)
// Copiar IDs de servicios del contrato anterior
var servicioIDs []uint
for _, s := range existing.Servicios {
servicioIDs = append(servicioIDs, s.ID)
}
nuevo := models.Contrato{ nuevo := models.Contrato{
ClienteID: existing.ClienteID, ClienteID: existing.ClienteID,
ServicioID: existing.ServicioID,
FechaInicio: nuevaInicio, FechaInicio: nuevaInicio,
FechaVencimiento: nuevaVenc, FechaVencimiento: nuevaVenc,
PrecioAcordado: existing.PrecioAcordado, PrecioAcordado: existing.PrecioAcordado,
@@ -154,12 +167,12 @@ func RenovarContrato(c *fiber.Ctx) error {
Estado: "activo", Estado: "activo",
Notas: "Renovación automática desde contrato #" + strconv.Itoa(int(existing.ID)), Notas: "Renovación automática desde contrato #" + strconv.Itoa(int(existing.ID)),
} }
if err := models.CreateContrato(nuevo); err != nil { if err := models.CreateContrato(nuevo, servicioIDs); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()}) return c.Status(500).JSON(fiber.Map{"error": err.Error()})
} }
// Marcar anterior como renovado // Marcar anterior como renovado
existing.Estado = "renovado" existing.Estado = "renovado"
models.UpdateContrato(*existing) models.UpdateContrato(*existing, nil)
return c.JSON(fiber.Map{"message": "Renovado", "ok": true}) return c.JSON(fiber.Map{"message": "Renovado", "ok": true})
} }