up
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -174,6 +174,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Enviar Notificación -->
|
||||
<div x-show="notifModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4 p-6" @click.stop>
|
||||
<h2 class="text-lg font-semibold mb-1">Enviar notificación</h2>
|
||||
<p class="text-sm text-gray-400 mb-4" x-text="notifCliente ? 'Para: '+notifCliente : ''" ></p>
|
||||
<label class="text-xs font-medium text-gray-600">Selecciona la plantilla / regla *</label>
|
||||
<select x-model="notifReglaId" class="mt-1 w-full border rounded px-3 py-2 text-sm mb-4">
|
||||
<option value="">Seleccionar...</option>
|
||||
<template x-for="r in reglas" :key="r.ID">
|
||||
<option :value="r.ID" x-text="r.nombre + ' — ' + tipoLabel(r.tipo_evento)"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button @click="notifModal=false" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||
<button @click="confirmarEnvioNotif()" :disabled="!notifReglaId||loading"
|
||||
class="px-4 py-2 bg-purple-600 text-white rounded text-sm disabled:opacity-40">
|
||||
<span x-text="loading ? 'Enviando…' : 'Enviar'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Bienvenida (post-creación) -->
|
||||
<div x-show="bienvenidaModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4 p-6 text-center" @click.stop>
|
||||
<div class="text-3xl mb-3">🎉</div>
|
||||
<p class="font-semibold mb-1">¿Enviar correo de bienvenida?</p>
|
||||
<p class="text-sm text-gray-400 mb-5">El contrato fue creado. ¿Deseas enviar el correo de bienvenida al cliente ahora?</p>
|
||||
<template x-if="reglasBienvenida.length > 0">
|
||||
<div class="mb-4 text-left">
|
||||
<label class="text-xs font-medium text-gray-600">Plantilla de bienvenida</label>
|
||||
<select x-model="bienvenidaReglaId" class="mt-1 w-full border rounded px-3 py-2 text-sm">
|
||||
<template x-for="r in reglasBienvenida" :key="r.ID">
|
||||
<option :value="r.ID" x-text="r.nombre"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex justify-center gap-3">
|
||||
<button @click="bienvenidaModal=false" class="px-4 py-2 border rounded text-sm">No, omitir</button>
|
||||
<button @click="enviarBienvenida()" :disabled="loading"
|
||||
class="px-4 py-2 bg-[#8eb02f] text-white rounded text-sm disabled:opacity-40">
|
||||
<span x-text="loading ? 'Enviando…' : 'Sí, enviar'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Eliminar -->
|
||||
<div x-show="deleteModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-sm mx-4 text-center" @click.stop>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user