Files
soft_usite/resources/views/renovaciones/plantillas.html
T
2026-05-12 18:08:30 -05:00

282 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div x-data="app" class="bg-white rounded-lg shadow">
<div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-75 flex justify-center items-center z-50">
<img src="../img/loading.gif" alt="Cargando..." class="w-16 h-16" />
</div>
<div class="container mx-auto p-6 w-full">
<div class="justify-between items-center w-full md:flex mb-4">
<div>
<h1 class="text-2xl font-bold mb-1">Plantillas de Correo</h1>
<p class="text-sm text-gray-500">Editor de plantillas HTML para notificaciones</p>
</div>
<div class="flex items-center gap-2 mt-3 md:mt-0">
<input type="text" placeholder="Buscar..." class="border border-gray-300 rounded p-2 text-sm"
x-model="search" @input.debounce.400ms="loadData()" />
<button @click="addModal = true" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nueva</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm">
<thead class="text-left border-b border-gray-200 bg-gray-50">
<tr>
<th class="py-2 px-3">Nombre</th>
<th class="py-2 px-3">Asunto</th>
<th class="py-2 px-3">Tipo</th>
<th class="py-2 px-3 w-36"></th>
</tr>
</thead>
<tbody class="text-gray-600">
<template x-for="d in datos" :key="d.ID">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3 font-medium" x-text="d.nombre"></td>
<td class="py-2 px-3 text-gray-500" x-text="d.asunto"></td>
<td class="py-2 px-3">
<span class="px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-700" x-text="d.tipo"></span>
</td>
<td class="py-2 px-3">
<div class="flex gap-2">
<button @click="openPreview(d)" title="Vista previa" class="text-gray-400 hover:text-blue-500">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/></svg>
</button>
<button @click="openTest(d)" title="Enviar prueba" class="text-gray-400 hover:text-purple-500">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5"/></svg>
</button>
<button @click="openEdit(d)" title="Editar" class="text-gray-400 hover:text-[#8eb02f]">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"/></svg>
</button>
<button @click="openDelete(d)" title="Eliminar" class="text-gray-400 hover:text-red-500">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/></svg>
</button>
</div>
</td>
</tr>
</template>
<tr x-show="!loading && datos.length===0">
<td colspan="4" class="text-center text-gray-400 py-8">Sin plantillas</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center justify-between mt-4 text-sm text-gray-500">
<span>Total: <span x-text="total"></span></span>
<div class="flex gap-1">
<button @click="prevPage()" :disabled="page===1" class="px-3 py-1 rounded border disabled:opacity-40"></button>
<span class="px-3 py-1" x-text="'Pág. '+page+' / '+totalPages"></span>
<button @click="nextPage()" :disabled="page>=totalPages" class="px-3 py-1 rounded border disabled:opacity-40"></button>
</div>
</div>
</div>
<!-- Modal Crear / Editar (con editor HTML) -->
<div x-show="addModal || editModal" 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-4xl mx-4 p-6 max-h-[90vh] overflow-y-auto" @click.stop>
<h2 class="text-lg font-semibold mb-4" x-text="editModal ? 'Editar Plantilla' : 'Nueva Plantilla'"></h2>
<!-- Variables disponibles -->
<div class="mb-4 p-3 bg-blue-50 rounded text-xs text-blue-700 leading-6">
<strong>Variables disponibles:</strong>
<code>&#123;&#123;.ClienteNombre&#125;&#125;</code>,
<code>&#123;&#123;.ClienteEmpresa&#125;&#125;</code>,
<code>&#123;&#123;.FechaVencimiento&#125;&#125;</code>,
<code>&#123;&#123;.DiasRestantes&#125;&#125;</code>,
<code>&#123;&#123;.Total&#125;&#125;</code>,
<code>&#123;&#123;range .Servicios&#125;&#125;...&#123;&#123;.Nombre&#125;&#125; &#123;&#123;.Precio&#125;&#125;...&#123;&#123;end&#125;&#125;</code>,
<code>&#123;&#123;.EnlacePago&#125;&#125;</code>
<span class="ml-1 text-blue-500">(URL de pago generada automáticamente por Bold o dLocal al enviar)</span>
</div>
<form @submit.prevent="save()">
<div class="grid grid-cols-2 gap-3 mb-3">
<div>
<label class="text-xs font-medium text-gray-600">Nombre *</label>
<input x-model="form.nombre" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
</div>
<div>
<label class="text-xs font-medium text-gray-600">Tipo</label>
<select x-model="form.tipo" class="mt-1 w-full border rounded px-3 py-2 text-sm">
<option value="bienvenida">Bienvenida</option>
<option value="renovacion">Renovación</option>
<option value="vencimiento">Vencimiento</option>
<option value="pago">Pago</option>
<option value="personalizado">Personalizado</option>
</select>
</div>
<div class="col-span-2">
<label class="text-xs font-medium text-gray-600">Asunto *</label>
<input x-model="form.asunto" required class="mt-1 w-full border rounded px-3 py-2 text-sm" />
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="text-xs font-medium text-gray-600 block mb-1">HTML del correo *</label>
<textarea x-model="form.cuerpo_html" rows="16"
@input.debounce.600ms="previewInline()"
:class="templateError ? 'border-red-400' : ''"
class="w-full border rounded px-3 py-2 text-xs font-mono" required></textarea>
<p x-show="templateError" x-text="templateError" class="text-red-500 text-xs mt-1"></p>
</div>
<div>
<label class="text-xs font-medium text-gray-600 block mb-1">Vista previa</label>
<iframe x-ref="previewFrame" class="w-full border rounded" style="height:352px"></iframe>
</div>
</div>
<div class="flex justify-end gap-2 mt-5">
<button type="button" @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
<button type="submit" class="px-4 py-2 bg-[#8eb02f] text-white rounded text-sm" :disabled="loading">
<span x-text="loading ? 'Guardando…' : 'Guardar'"></span>
</button>
</div>
</form>
</div>
</div>
<!-- Modal vista previa del servidor -->
<div x-show="previewModal" 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-2xl mx-4 p-5 max-h-[90vh] overflow-y-auto" @click.stop>
<div class="flex justify-between items-center mb-3">
<h2 class="text-lg font-semibold">Vista previa</h2>
<button @click="previewModal=false" class="text-gray-400 hover:text-gray-700"></button>
</div>
<iframe x-ref="serverPreviewFrame" class="w-full border rounded" style="height:500px"></iframe>
</div>
</div>
<!-- Modal Enviar prueba -->
<div x-show="testModal" 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-4">Enviar correo de prueba</h2>
<label class="text-xs font-medium text-gray-600">Email destino *</label>
<input x-model="testEmail" type="email" class="mt-1 w-full border rounded px-3 py-2 text-sm mb-4" placeholder="correo@ejemplo.com" />
<div class="flex justify-end gap-2">
<button @click="testModal=false" class="px-4 py-2 border rounded text-sm">Cancelar</button>
<button @click="sendTest()" class="px-4 py-2 bg-purple-600 text-white rounded text-sm" :disabled="loading">
<span x-text="loading ? 'Enviando…' : '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>
<p class="font-semibold mb-1">¿Eliminar plantilla?</p>
<p class="text-sm text-gray-400 mb-5">Esta acción no se puede deshacer.</p>
<div class="flex justify-center gap-3">
<button @click="closeModals()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
<button @click="deleteItem()" class="px-4 py-2 bg-red-500 text-white rounded text-sm">Eliminar</button>
</div>
</div>
</div>
<div x-show="toast.show" x-cloak x-transition
class="fixed bottom-4 right-4 z-[100] px-4 py-3 rounded shadow-lg text-sm text-white"
:class="toast.type==='error' ? 'bg-red-500' : 'bg-[#8eb02f]'"
x-text="toast.msg"></div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('app', () => ({
loading: false,
datos: [], total: 0, totalPages: 1, page: 1, limit: 10, search: '',
addModal: false, editModal: false, deleteModal: false, previewModal: false, testModal: false,
selectedId: null, testEmail: '', templateError: '',
form: { nombre:'', asunto:'', cuerpo_html:'', tipo:'renovacion' },
toast: { show:false, msg:'', type:'ok' },
async init() { await this.loadData(); },
async loadData() {
this.loading = true;
const { data } = await axios.get(`/app/api/plantillas-correo?page=${this.page}&limit=${this.limit}&search=${this.search}`);
this.datos = data.registros || [];
this.total = data.total || 0;
this.totalPages = data.totalPages || 1;
this.loading = false;
},
openEdit(d) {
this.form = { nombre: d.nombre, asunto: d.asunto, cuerpo_html: d.cuerpo_html, tipo: d.tipo };
this.selectedId = d.ID;
this.editModal = true;
this.$nextTick(() => this.previewInline());
},
openDelete(d) { this.selectedId = d.ID; this.deleteModal = true; },
openTest(d) { this.selectedId = d.ID; this.testModal = true; },
async openPreview(d) {
this.selectedId = d.ID;
const { data } = await axios.get(`/app/api/plantillas-correo/${d.ID}/preview`);
this.previewModal = true;
this.$nextTick(() => {
const frame = this.$refs.serverPreviewFrame;
frame.srcdoc = data.html || '<p>Sin contenido</p>';
});
},
async previewInline() {
const frame = this.$refs.previewFrame;
if (frame) frame.srcdoc = this.form.cuerpo_html || '<p style="color:#999;padding:1rem">Sin contenido</p>';
if (!this.form.cuerpo_html) { this.templateError = ''; return; }
try {
const { data } = await axios.post('/app/api/plantillas-correo/validate', { html: this.form.cuerpo_html });
this.templateError = data.ok ? '' : (data.error || 'Sintaxis inválida');
} catch { this.templateError = ''; }
},
closeModals() {
this.addModal = this.editModal = this.deleteModal = this.previewModal = this.testModal = false;
this.selectedId = null;
this.templateError = '';
this.form = { nombre:'', asunto:'', cuerpo_html:'', tipo:'renovacion' };
},
async save() {
if (this.templateError) { this.showToast('Corrige los errores de sintaxis antes de guardar', 'error'); return; }
this.loading = true;
try {
if (this.editModal) {
await axios.put(`/app/api/plantillas-correo/${this.selectedId}`, this.form);
} else {
await axios.post('/app/api/plantillas-correo', this.form);
}
this.showToast('Guardado');
this.closeModals();
await this.loadData();
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
this.loading = false;
},
async deleteItem() {
this.loading = true;
try {
await axios.delete(`/app/api/plantillas-correo/${this.selectedId}`);
this.showToast('Eliminado');
this.closeModals();
await this.loadData();
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
this.loading = false;
},
async sendTest() {
this.loading = true;
try {
await axios.post(`/app/api/plantillas-correo/${this.selectedId}/test`, { email: this.testEmail });
this.showToast('Correo de prueba enviado');
this.testModal = false;
} catch(e) { this.showToast(e.response?.data?.error||'Error', 'error'); }
this.loading = false;
},
prevPage() { if(this.page>1){ this.page--; this.loadData(); } },
nextPage() { if(this.page<this.totalPages){ this.page++; this.loadData(); } },
showToast(msg, type='ok') {
this.toast = { show:true, msg, type };
setTimeout(() => this.toast.show = false, 3000);
}
}));
});
</script>