diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go
index 96e5817..d74a634 100644
--- a/pkg/models/contrato.go
+++ b/pkg/models/contrato.go
@@ -61,6 +61,17 @@ func GetContratoByID(id uint) (*Contrato, error) {
return &item, nil
}
+// GetUltimoContratoByCliente devuelve el contrato más reciente de un cliente
+func GetUltimoContratoByCliente(clienteID uint) (*Contrato, error) {
+ var item Contrato
+ if err := app.Http.Database.DB.Preload("Cliente").Preload("Servicios").
+ Where("cliente_id = ?", clienteID).
+ Order("id DESC").First(&item).Error; err != nil {
+ return nil, err
+ }
+ return &item, nil
+}
+
// GetContratosByCliente devuelve contratos de un cliente específico
func GetContratosByCliente(clienteID uint) ([]Contrato, error) {
var items []Contrato
diff --git a/resources/views/renovaciones/contratos.html b/resources/views/renovaciones/contratos.html
index 31bc504..faca086 100644
--- a/resources/views/renovaciones/contratos.html
+++ b/resources/views/renovaciones/contratos.html
@@ -174,6 +174,54 @@
+
+
@@ -200,7 +248,9 @@ document.addEventListener('alpine:init', () => {
datos: [], total: 0, totalPages: 1, page: 1, limit: 10,
search: '', filtroEstado: '',
addModal: false, editModal: false, deleteModal: false,
- clientes: [], servicios: [],
+ notifModal: false, notifReglaId: '', notifContratoId: null, notifCliente: '',
+ bienvenidaModal: false, bienvenidaReglaId: '', bienvenidaContratoId: null,
+ clientes: [], servicios: [], reglas: [], reglasBienvenida: [],
selectedId: null,
form: { cliente_id:'', servicio_ids:[], fecha_inicio:'', fecha_vencimiento:'', precio_acordado:0, estado:'activo', auto_renovar:false, notas:'' },
toast: { show: false, msg: '', type: 'ok' },
@@ -220,12 +270,17 @@ document.addEventListener('alpine:init', () => {
async loadSelects() {
try {
- const [c, s] = await Promise.all([
+ const [c, s, r] = await Promise.all([
axios.get('/app/api/clientes/select'),
axios.get('/app/api/servicios/select'),
+ axios.get('/app/api/reglas-notificacion'),
]);
this.clientes = Array.isArray(c.data) ? c.data : (c.data.registros || []);
this.servicios = Array.isArray(s.data) ? s.data : (s.data.registros || []);
+ const todasReglas = r.data.registros || [];
+ this.reglas = todasReglas.filter(x => x.activo);
+ this.reglasBienvenida = todasReglas.filter(x => x.activo && x.tipo_evento === 'bienvenida');
+ if (this.reglasBienvenida.length > 0) this.bienvenidaReglaId = this.reglasBienvenida[0].ID;
} catch(e) {
this.showToast('Error cargando listas: ' + (e.response?.data?.error || e.message), 'error');
}
@@ -278,12 +333,19 @@ document.addEventListener('alpine:init', () => {
};
if (this.editModal) {
await axios.put(`/app/api/contratos/${this.selectedId}`, payload);
+ this.showToast('Guardado correctamente');
+ this.closeModals();
+ await this.loadData();
} else {
- await axios.post('/app/api/contratos', payload);
+ const res = await axios.post('/app/api/contratos', payload);
+ this.closeModals();
+ await this.loadData();
+ // Guardar el ID del contrato recién creado para bienvenida
+ // La API devuelve el ID o lo obtenemos del último registro
+ const contratoId = res.data.id || (this.datos.length > 0 ? this.datos[0].ID : null);
+ this.bienvenidaContratoId = contratoId;
+ this.bienvenidaModal = true;
}
- this.showToast('Guardado correctamente');
- this.closeModals();
- await this.loadData();
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
this.loading = false;
},
@@ -308,12 +370,38 @@ document.addEventListener('alpine:init', () => {
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
},
- async enviarCorreo(d) {
- if (!confirm(`¿Enviar correo de aviso a ${d.cliente?.email}?`)) return;
+ enviarCorreo(d) {
+ this.notifContratoId = d.ID;
+ this.notifCliente = (d.cliente?.nombre || '') + (d.cliente?.email ? ' <'+d.cliente.email+'>' : '');
+ this.notifReglaId = '';
+ this.notifModal = true;
+ },
+
+ async confirmarEnvioNotif() {
+ if (!this.notifReglaId) return;
+ this.loading = true;
try {
- await axios.post(`/app/api/contratos/${d.ID}/enviar-correo`);
- this.showToast('Correo enviado');
- } catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
+ await axios.post(`/app/api/contratos/${this.notifContratoId}/enviar-correo`, { regla_id: parseInt(this.notifReglaId) });
+ this.showToast('Correo enviado correctamente');
+ this.notifModal = false;
+ } catch(e) { this.showToast(e.response?.data?.error||'Error al enviar', 'error'); }
+ this.loading = false;
+ },
+
+ async enviarBienvenida() {
+ this.loading = true;
+ try {
+ const payload = this.bienvenidaReglaId ? { regla_id: parseInt(this.bienvenidaReglaId) } : {};
+ await axios.post(`/app/api/contratos/${this.bienvenidaContratoId}/enviar-correo`, payload);
+ this.showToast('Correo de bienvenida enviado');
+ } catch(e) { this.showToast(e.response?.data?.error||'Error al enviar', 'error'); }
+ this.bienvenidaModal = false;
+ this.loading = false;
+ },
+
+ tipoLabel(tipo) {
+ const m = { vencimiento_proximo:'Próximo a vencer', ya_vencido:'Ya vencido', bienvenida:'Bienvenida', pago_recibido:'Pago recibido', manual:'Manual' };
+ return m[tipo] || tipo;
},
fmtDate(raw) {
diff --git a/rest/controllers/contrato_controller.go b/rest/controllers/contrato_controller.go
index 797191a..07ef488 100644
--- a/rest/controllers/contrato_controller.go
+++ b/rest/controllers/contrato_controller.go
@@ -94,7 +94,13 @@ func CreateContrato(c *fiber.Ctx) error {
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})
+ // Devolver el ID del contrato creado para que el frontend pueda enviar bienvenida
+ created, _ := models.GetUltimoContratoByCliente(inp.ClienteID)
+ contratoID := uint(0)
+ if created != nil {
+ contratoID = created.ID
+ }
+ return c.Status(201).JSON(fiber.Map{"message": "Contrato creado", "ok": true, "id": contratoID})
}
func UpdateContrato(c *fiber.Ctx) error {
@@ -186,6 +192,25 @@ func EnviarCorreoContrato(c *fiber.Ctx) error {
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
+
+ // Si se pasa regla_id, usar esa regla específica (EnviarNotificacionGrupo)
+ var body struct {
+ ReglaID uint `json:"regla_id"`
+ }
+ _ = c.BodyParser(&body)
+
+ if body.ReglaID > 0 {
+ regla, err := models.GetReglaByID(body.ReglaID)
+ if err != nil {
+ return c.Status(404).JSON(fiber.Map{"error": "Regla no encontrada"})
+ }
+ if err := services.EnviarNotificacionGrupo(regla, &contrato.Cliente, []models.Contrato{*contrato}); err != nil {
+ return c.Status(500).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Correo enviado", "ok": true})
+ }
+
+ // Sin regla_id → flujo manual genérico
if err := services.EnviarCorreoManual(contrato); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}