feat: contrato soporta múltiples servicios (many2many) con precio sugerido
This commit is contained in:
+41
-20
@@ -10,16 +10,15 @@ import (
|
||||
|
||||
type Contrato struct {
|
||||
gorm.Model
|
||||
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id"`
|
||||
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||
ServicioID uint `json:"servicio_id" gorm:"column:servicio_id"`
|
||||
Servicio Servicio `json:"servicio" gorm:"foreignKey:ServicioID"`
|
||||
FechaInicio time.Time `json:"fecha_inicio" gorm:"column:fecha_inicio"`
|
||||
FechaVencimiento time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
||||
PrecioAcordado float64 `json:"precio_acordado" gorm:"column:precio_acordado"`
|
||||
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"`
|
||||
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id"`
|
||||
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||
Servicios []Servicio `json:"servicios" gorm:"many2many:contrato_servicios"`
|
||||
FechaInicio time.Time `json:"fecha_inicio" gorm:"column:fecha_inicio"`
|
||||
FechaVencimiento time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
||||
PrecioAcordado float64 `json:"precio_acordado" gorm:"column:precio_acordado"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (Contrato) TableName() string { return "contratos" }
|
||||
@@ -28,7 +27,7 @@ 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("Servicio")
|
||||
Preload("Cliente").Preload("Servicios")
|
||||
if search != "" {
|
||||
db = db.Joins("JOIN clientes ON clientes.id = contratos.cliente_id").
|
||||
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) {
|
||||
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 &item, nil
|
||||
@@ -57,7 +56,7 @@ func GetContratoByID(id uint) (*Contrato, error) {
|
||||
// GetContratosByCliente devuelve contratos de un cliente específico
|
||||
func GetContratosByCliente(clienteID uint) ([]Contrato, error) {
|
||||
var items []Contrato
|
||||
if err := app.Http.Database.DB.Preload("Servicio").
|
||||
if err := app.Http.Database.DB.Preload("Servicios").
|
||||
Where("cliente_id = ?", clienteID).
|
||||
Order("fecha_vencimiento ASC").Find(&items).Error; err != nil {
|
||||
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)
|
||||
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).
|
||||
Find(&items).Error; err != nil {
|
||||
log.Printf("Error getting contratos proximos: %v", err)
|
||||
@@ -81,21 +80,42 @@ func GetContratosProximosVencer(diasAntes int) ([]Contrato, error) {
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func CreateContrato(c Contrato) error {
|
||||
return app.Http.Database.DB.Create(&c).Error
|
||||
func CreateContrato(c Contrato, servicioIDs []uint) 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 {
|
||||
return app.Http.Database.DB.Model(&Contrato{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
|
||||
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{}{
|
||||
"cliente_id": c.ClienteID,
|
||||
"servicio_id": c.ServicioID,
|
||||
"fecha_inicio": c.FechaInicio,
|
||||
"fecha_vencimiento": c.FechaVencimiento,
|
||||
"precio_acordado": c.PrecioAcordado,
|
||||
"estado": c.Estado,
|
||||
"auto_renovar": c.AutoRenovar,
|
||||
"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 {
|
||||
@@ -103,5 +123,6 @@ func DeleteContrato(id uint) error {
|
||||
if err := app.Http.Database.DB.First(&c, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
app.Http.Database.DB.Model(&c).Association("Servicios").Clear()
|
||||
return app.Http.Database.DB.Delete(&c).Error
|
||||
}
|
||||
|
||||
@@ -56,12 +56,18 @@ func ProcesarVencimientos() {
|
||||
for _, c := range contratos {
|
||||
switch regla.AplicaA {
|
||||
case "renovable":
|
||||
if c.Servicio.Tipo == "renovable" {
|
||||
filtrados = append(filtrados, c)
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "renovable" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "unico":
|
||||
if c.Servicio.Tipo == "unico" {
|
||||
filtrados = append(filtrados, c)
|
||||
for _, s := range c.Servicios {
|
||||
if s.Tipo == "unico" {
|
||||
filtrados = append(filtrados, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
filtrados = append(filtrados, c)
|
||||
|
||||
@@ -82,6 +82,15 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||
return err
|
||||
}
|
||||
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{
|
||||
ClienteNombre: contrato.Cliente.Nombre,
|
||||
ClienteEmpresa: contrato.Cliente.Empresa,
|
||||
@@ -89,12 +98,7 @@ func EnviarCorreoManual(contrato *models.Contrato) error {
|
||||
FechaVencimiento: contrato.FechaVencimiento.Format("02/01/2006"),
|
||||
DiasRestantes: dias,
|
||||
Total: contrato.PrecioAcordado,
|
||||
Servicios: []ItemServicio{{
|
||||
Nombre: contrato.Servicio.Nombre,
|
||||
Precio: contrato.PrecioAcordado,
|
||||
Moneda: contrato.Servicio.Moneda,
|
||||
FechaVenc: contrato.FechaVencimiento.Format("02/01/2006"),
|
||||
}},
|
||||
Servicios: items,
|
||||
}
|
||||
html, err := RenderPlantilla(p, datos)
|
||||
if err != nil {
|
||||
@@ -115,12 +119,14 @@ func EnviarNotificacionGrupo(regla *models.NotificacionRegla, cliente *models.Cl
|
||||
var fechaVenc time.Time
|
||||
|
||||
for _, c := range contratos {
|
||||
items = append(items, ItemServicio{
|
||||
Nombre: c.Servicio.Nombre,
|
||||
Precio: c.PrecioAcordado,
|
||||
Moneda: c.Servicio.Moneda,
|
||||
FechaVenc: c.FechaVencimiento.Format("02/01/2006"),
|
||||
})
|
||||
for _, s := range c.Servicios {
|
||||
items = append(items, ItemServicio{
|
||||
Nombre: s.Nombre,
|
||||
Precio: s.Precio,
|
||||
Moneda: s.Moneda,
|
||||
FechaVenc: c.FechaVencimiento.Format("02/01/2006"),
|
||||
})
|
||||
}
|
||||
total += c.PrecioAcordado
|
||||
if fechaVenc.IsZero() || c.FechaVencimiento.Before(fechaVenc) {
|
||||
fechaVenc = c.FechaVencimiento
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<thead class="text-left border-b border-gray-200 bg-gray-50">
|
||||
<tr>
|
||||
<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">Días</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="text-xs text-gray-400" x-text="d.cliente?.empresa||''"></div>
|
||||
</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">
|
||||
<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'"
|
||||
x-text="d.dias_restantes <= 0 ? 'Vencido' : d.dias_restantes+' días'"></span>
|
||||
</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">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium"
|
||||
:class="{
|
||||
@@ -111,14 +116,20 @@
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-2" x-show="!editModal">
|
||||
<label class="text-xs font-medium text-gray-600">Servicio *</label>
|
||||
<select x-model="form.servicio_id" required class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||
<option value="">Seleccionar...</option>
|
||||
<div class="col-span-2">
|
||||
<label class="text-xs font-medium text-gray-600">Servicios <span class="font-normal text-gray-400">(uno o varios)</span></label>
|
||||
<div class="mt-1 border rounded max-h-36 overflow-y-auto divide-y text-sm">
|
||||
<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>
|
||||
</select>
|
||||
<div x-show="servicios.length===0" class="text-xs text-gray-400 px-3 py-2">Sin servicios disponibles</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-600">Fecha inicio *</label>
|
||||
@@ -131,6 +142,9 @@
|
||||
<div>
|
||||
<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" />
|
||||
<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 x-show="editModal">
|
||||
<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,
|
||||
clientes: [], servicios: [],
|
||||
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' },
|
||||
|
||||
async init() {
|
||||
@@ -205,16 +219,37 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
async loadSelects() {
|
||||
const [c, s] = await Promise.all([
|
||||
axios.get('/app/api/clientes/select'),
|
||||
axios.get('/app/api/servicios/select'),
|
||||
]);
|
||||
this.clientes = c.data.registros || [];
|
||||
this.servicios = s.data.registros || [];
|
||||
try {
|
||||
const [c, s] = await Promise.all([
|
||||
axios.get('/app/api/clientes/select'),
|
||||
axios.get('/app/api/servicios/select'),
|
||||
]);
|
||||
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) {
|
||||
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_vencimiento: d.fecha_vencimiento ? d.fecha_vencimiento.substring(0,10) : '',
|
||||
precio_acordado: d.precio_acordado,
|
||||
@@ -229,7 +264,7 @@ document.addEventListener('alpine:init', () => {
|
||||
closeModals() {
|
||||
this.addModal = this.editModal = this.deleteModal = false;
|
||||
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() {
|
||||
|
||||
@@ -31,8 +31,9 @@ func GetContratos(c *fiber.Ctx) error {
|
||||
// Enriquecer con días restantes
|
||||
type ContratoDTO struct {
|
||||
models.Contrato
|
||||
DiasRestantes int `json:"dias_restantes"`
|
||||
Urgencia string `json:"urgencia"` // verde | amarillo | rojo
|
||||
DiasRestantes int `json:"dias_restantes"`
|
||||
Urgencia string `json:"urgencia"` // verde | amarillo | rojo
|
||||
PrecioSugerido float64 `json:"precio_sugerido"`
|
||||
}
|
||||
dtos := make([]ContratoDTO, len(records))
|
||||
now := time.Now()
|
||||
@@ -44,7 +45,11 @@ func GetContratos(c *fiber.Ctx) error {
|
||||
} else if dias <= 30 {
|
||||
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{
|
||||
"registros": dtos,
|
||||
@@ -57,13 +62,13 @@ func GetContratos(c *fiber.Ctx) error {
|
||||
|
||||
func CreateContrato(c *fiber.Ctx) error {
|
||||
type Input struct {
|
||||
ClienteID uint `json:"cliente_id" form:"cliente_id"`
|
||||
ServicioID uint `json:"servicio_id" form:"servicio_id"`
|
||||
FechaInicio string `json:"fecha_inicio" form:"fecha_inicio"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"`
|
||||
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"`
|
||||
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"`
|
||||
Notas string `json:"notas" form:"notas"`
|
||||
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"`
|
||||
}
|
||||
var inp Input
|
||||
if err := c.BodyParser(&inp); err != nil {
|
||||
@@ -79,7 +84,6 @@ func CreateContrato(c *fiber.Ctx) error {
|
||||
}
|
||||
m := models.Contrato{
|
||||
ClienteID: inp.ClienteID,
|
||||
ServicioID: inp.ServicioID,
|
||||
FechaInicio: fi,
|
||||
FechaVencimiento: fv,
|
||||
PrecioAcordado: inp.PrecioAcordado,
|
||||
@@ -87,7 +91,7 @@ func CreateContrato(c *fiber.Ctx) error {
|
||||
Notas: inp.Notas,
|
||||
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(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"})
|
||||
}
|
||||
type Input struct {
|
||||
Estado string `json:"estado" form:"estado"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"`
|
||||
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"`
|
||||
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"`
|
||||
Notas string `json:"notas" form:"notas"`
|
||||
ServicioIDs []uint `json:"servicio_ids"`
|
||||
Estado string `json:"estado"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||
PrecioAcordado float64 `json:"precio_acordado"`
|
||||
AutoRenovar bool `json:"auto_renovar"`
|
||||
Notas string `json:"notas"`
|
||||
}
|
||||
var inp Input
|
||||
if err := c.BodyParser(&inp); err != nil {
|
||||
@@ -125,7 +130,7 @@ func UpdateContrato(c *fiber.Ctx) error {
|
||||
existing.PrecioAcordado = inp.PrecioAcordado
|
||||
existing.AutoRenovar = inp.AutoRenovar
|
||||
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.JSON(fiber.Map{"message": "Actualizado", "ok": true})
|
||||
@@ -140,13 +145,21 @@ func RenovarContrato(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
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)
|
||||
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{
|
||||
ClienteID: existing.ClienteID,
|
||||
ServicioID: existing.ServicioID,
|
||||
FechaInicio: nuevaInicio,
|
||||
FechaVencimiento: nuevaVenc,
|
||||
PrecioAcordado: existing.PrecioAcordado,
|
||||
@@ -154,12 +167,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); err != nil {
|
||||
if err := models.CreateContrato(nuevo, servicioIDs); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
// Marcar anterior como renovado
|
||||
existing.Estado = "renovado"
|
||||
models.UpdateContrato(*existing)
|
||||
models.UpdateContrato(*existing, nil)
|
||||
|
||||
return c.JSON(fiber.Map{"message": "Renovado", "ok": true})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user