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 {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user