This commit is contained in:
Lizandro Guarnizo
2026-05-12 18:53:09 -05:00
parent 525d508982
commit 05141ce70e
3 changed files with 136 additions and 12 deletions
+99 -11
View File
@@ -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) {