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 @@ + +
+
+

Enviar notificación

+

+ + +
+ + +
+
+
+ + +
+
+
🎉
+

¿Enviar correo de bienvenida?

+

El contrato fue creado. ¿Deseas enviar el correo de bienvenida al cliente ahora?

+ +
+ + +
+
+
+
@@ -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()}) }