Files
soft_usite/resources/views/telegram.html
T
2026-05-14 22:30:31 -05:00

319 lines
19 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.
<!-- Vista: Configuración de Telegram -->
<div x-data="telegramApp()" x-init="init()" @keydown.escape="closeModal()" class="bg-white rounded-lg shadow">
<div class="container mx-auto p-6 w-full">
<h1 class="text-2xl font-bold mb-1">Notificaciones Telegram</h1>
<p class="text-sm text-gray-500 mb-4">
Configura bots de Telegram para enviar notificaciones dinámicas desde el panel.
Cada bot requiere un <code class="bg-gray-100 px-1 rounded">bot_token</code> y un <code class="bg-gray-100 px-1 rounded">chat_id</code> (puede ser un canal, grupo o usuario).
</p>
<!-- Tabs -->
<div class="flex gap-4 border-b mb-6 text-sm font-medium">
<button @click="tab='configs'"
:class="tab==='configs' ? 'border-b-2 border-[#8eb02f] text-[#8eb02f]' : 'text-gray-500 hover:text-gray-700'"
class="pb-2">Configuraciones</button>
<button @click="tab='send'"
:class="tab==='send' ? 'border-b-2 border-[#8eb02f] text-[#8eb02f]' : 'text-gray-500 hover:text-gray-700'"
class="pb-2">Enviar mensaje</button>
<button @click="tab='logs'; loadLogs(1)"
:class="tab==='logs' ? 'border-b-2 border-[#8eb02f] text-[#8eb02f]' : 'text-gray-500 hover:text-gray-700'"
class="pb-2">Historial</button>
</div>
<!-- ── Tab: Configuraciones ──────────────────────────────────────────── -->
<div x-show="tab==='configs'">
<div class="flex justify-end mb-4">
<button @click="openAdd()" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nueva configuración</button>
</div>
<template x-if="configs.length === 0">
<p class="text-gray-400 text-sm text-center py-8">Sin configuraciones. Agrega una para comenzar.</p>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<template x-for="cfg in configs" :key="cfg.ID">
<div class="border rounded-lg p-4 flex flex-col gap-2 hover:shadow-sm transition-shadow">
<div class="flex items-start justify-between gap-2">
<div>
<p class="font-semibold text-gray-800 text-sm" x-text="cfg.nombre"></p>
<p class="text-xs text-gray-400 mt-0.5">Chat ID: <span class="font-mono" x-text="cfg.chat_id"></span></p>
</div>
<span :class="cfg.activo ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
x-text="cfg.activo ? 'Activo' : 'Inactivo'"
class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap"></span>
</div>
<p x-show="cfg.notas" x-text="cfg.notas" class="text-xs text-gray-500 italic"></p>
<div class="flex gap-2 mt-auto pt-2 border-t">
<button @click="testConfig(cfg)" :disabled="testLoading[cfg.ID]"
class="flex-1 text-xs border border-[#8eb02f] text-[#8eb02f] px-3 py-1.5 rounded hover:bg-[#8eb02f] hover:text-white transition-colors disabled:opacity-50">
<span x-show="!testLoading[cfg.ID]">🔔 Probar</span>
<span x-show="testLoading[cfg.ID]">Enviando…</span>
</button>
<button @click="openEdit(cfg)"
class="text-xs border border-blue-400 text-blue-500 px-3 py-1.5 rounded hover:bg-blue-50">Editar</button>
<button @click="confirmDelete(cfg.ID)"
class="text-xs border border-red-300 text-red-400 px-3 py-1.5 rounded hover:bg-red-50">Eliminar</button>
</div>
<p x-show="testResult[cfg.ID]" x-text="testResult[cfg.ID]"
:class="testOk[cfg.ID] ? 'text-green-600' : 'text-red-600'"
class="text-xs mt-1"></p>
</div>
</template>
</div>
</div>
<!-- ── Tab: Enviar mensaje ───────────────────────────────────────────── -->
<div x-show="tab==='send'" class="max-w-lg">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Destinos <span class="text-red-500">*</span></label>
<div class="border rounded p-3 space-y-2 max-h-48 overflow-y-auto">
<template x-for="cfg in configs.filter(c => c.activo)" :key="cfg.ID">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" :value="cfg.ID" x-model.number="sendForm.config_ids" class="w-4 h-4">
<span class="text-sm" x-text="cfg.nombre"></span>
<span class="text-xs text-gray-400 font-mono ml-auto" x-text="cfg.chat_id"></span>
</label>
</template>
<p x-show="configs.filter(c => c.activo).length === 0" class="text-xs text-gray-400">No hay configuraciones activas.</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Título (opcional)</label>
<input type="text" x-model="sendForm.titulo" placeholder="Ej: Alerta de servidor caído"
class="border rounded w-full p-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mensaje <span class="text-red-500">*</span></label>
<textarea x-model="sendForm.mensaje" rows="5"
placeholder="Escribe el mensaje. Puedes usar HTML básico: &lt;b&gt;negrita&lt;/b&gt;, &lt;i&gt;cursiva&lt;/i&gt;, &lt;code&gt;código&lt;/code&gt;"
class="border rounded w-full p-2 text-sm"></textarea>
<p class="text-xs text-gray-400 mt-0.5">Soporta HTML básico de Telegram: &lt;b&gt;, &lt;i&gt;, &lt;code&gt;, &lt;pre&gt;</p>
</div>
<div class="flex items-center gap-3">
<button @click="sendMessage()" :disabled="sendLoading"
class="bg-[#8eb02f] text-white px-5 py-2 rounded text-sm disabled:opacity-50">
<span x-show="!sendLoading">📤 Enviar</span>
<span x-show="sendLoading">Enviando…</span>
</button>
<p x-show="sendResult" x-text="sendResult"
:class="sendOk ? 'text-green-600' : 'text-red-600'"
class="text-sm"></p>
</div>
</div>
</div>
<!-- ── Tab: Historial ────────────────────────────────────────────────── -->
<div x-show="tab==='logs'">
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm">
<thead class="border-b border-gray-200 text-left font-semibold text-gray-600">
<tr>
<th class="py-2 px-4">Bot / Config</th>
<th class="py-2 px-4">Título</th>
<th class="py-2 px-4">Mensaje</th>
<th class="py-2 px-4">Estado</th>
<th class="py-2 px-4">Fecha</th>
</tr>
</thead>
<tbody class="text-gray-500">
<template x-if="logs.length === 0">
<tr><td colspan="5" class="py-4 text-center text-gray-400">Sin registros</td></tr>
</template>
<template x-for="log in logs" :key="log.ID">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-4 text-xs" x-text="log.TelegramConfig ? log.TelegramConfig.nombre : '-'"></td>
<td class="py-2 px-4 text-xs" x-text="log.titulo || '-'"></td>
<td class="py-2 px-4 text-xs max-w-xs truncate" x-text="log.mensaje"></td>
<td class="py-2 px-4">
<span :class="log.estado === 'ok' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
x-text="log.estado" class="text-xs px-2 py-0.5 rounded-full"></span>
<span x-show="log.error_msg" x-text="log.error_msg" class="text-xs text-red-500 ml-1"></span>
</td>
<td class="py-2 px-4 text-xs text-gray-400" x-text="fmtDate(log.CreatedAt)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Paginación logs -->
<div class="flex justify-between items-center mt-4 text-sm text-gray-500">
<span>Total: <b x-text="logTotal"></b></span>
<div class="flex gap-1">
<button @click="loadLogs(logPage-1)" :disabled="logPage<=1" class="px-3 py-1 border rounded disabled:opacity-40"></button>
<span class="px-3 py-1" x-text="'Pág. ' + logPage + ' / ' + logTotalPages"></span>
<button @click="loadLogs(logPage+1)" :disabled="logPage>=logTotalPages" class="px-3 py-1 border rounded disabled:opacity-40"></button>
</div>
</div>
</div>
</div>
<!-- ── Modal Crear / Editar config ────────────────────────────────────────── -->
<div x-show="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" style="display:none">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
<div class="flex justify-between items-center p-5 border-b">
<h2 class="text-lg font-semibold" x-text="editMode ? 'Editar configuración' : 'Nueva configuración'"></h2>
<button @click="closeModal()" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre / etiqueta <span class="text-red-500">*</span></label>
<input type="text" x-model="form.nombre" placeholder="Ej: Bot Alertas Producción"
class="border rounded w-full p-2 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bot Token <span class="text-red-500">*</span></label>
<input type="password" x-model="form.bot_token"
:placeholder="editMode ? '••••••• (dejar vacío para no cambiar)' : 'Token del bot de @BotFather'"
class="border rounded w-full p-2 text-sm font-mono">
<p class="text-xs text-gray-400 mt-0.5">Obtén el token en <a href="https://t.me/BotFather" target="_blank" class="underline text-blue-500">@BotFather</a></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Chat ID <span class="text-red-500">*</span></label>
<input type="text" x-model="form.chat_id" placeholder="Ej: -1001234567890 o @micanal"
class="border rounded w-full p-2 text-sm font-mono">
<p class="text-xs text-gray-400 mt-0.5">ID del chat, grupo o canal. Para canales usa el formato <code>@nombre_canal</code>.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Notas internas</label>
<textarea x-model="form.notas" rows="2" placeholder="Para qué se usa este bot..."
class="border rounded w-full p-2 text-sm"></textarea>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="tg-activo" x-model="form.activo" class="w-4 h-4">
<label for="tg-activo" class="text-sm">Activo</label>
</div>
<p x-show="formError" x-text="formError" class="text-red-500 text-sm"></p>
</div>
<div class="flex justify-end gap-3 p-5 border-t">
<button @click="closeModal()" class="px-4 py-2 border rounded text-sm">Cancelar</button>
<button @click="save()" :disabled="saving"
class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm disabled:opacity-50">
<span x-show="!saving">Guardar</span>
<span x-show="saving">Guardando…</span>
</button>
</div>
</div>
</div>
<!-- ── Modal Confirmar eliminar ────────────────────────────────────────────── -->
<div x-show="deleteModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" style="display:none">
<div class="bg-white rounded-lg shadow-xl w-full max-w-sm mx-4 p-6">
<h2 class="text-lg font-bold mb-3">Confirmar eliminación</h2>
<p class="text-gray-600 mb-4 text-sm">¿Eliminar esta configuración de Telegram?</p>
<div class="flex justify-end gap-2">
<button @click="doDelete()" class="bg-red-500 text-white px-4 py-2 rounded text-sm">Eliminar</button>
<button @click="deleteModal=false" class="bg-gray-500 text-white px-4 py-2 rounded text-sm">Cancelar</button>
</div>
</div>
</div>
</div>
<script>
function telegramApp() {
return {
tab: 'configs',
configs: [],
showModal: false, editMode: false, saving: false, formError: '',
deleteModal: false, deleteId: null,
form: { nombre: '', bot_token: '', chat_id: '', notas: '', activo: true },
testLoading: {}, testResult: {}, testOk: {},
sendForm: { config_ids: [], titulo: '', mensaje: '' },
sendLoading: false, sendResult: '', sendOk: true,
logs: [], logPage: 1, logTotalPages: 1, logTotal: 0,
async init() {
await this.loadConfigs();
},
async loadConfigs() {
const res = await fetch('/app/loadtelegram');
this.configs = await res.json();
},
openAdd() {
this.editMode = false;
this.form = { nombre: '', bot_token: '', chat_id: '', notas: '', activo: true };
this.formError = '';
this.showModal = true;
},
openEdit(cfg) {
this.editMode = true;
this.form = { id: cfg.ID, nombre: cfg.nombre, bot_token: '', chat_id: cfg.chat_id, notas: cfg.notas || '', activo: cfg.activo };
this.formError = '';
this.showModal = true;
},
closeModal() { this.showModal = false; },
async save() {
this.formError = '';
if (!this.form.nombre.trim()) { this.formError = 'El nombre es requerido.'; return; }
if (!this.editMode && !this.form.bot_token.trim()) { this.formError = 'El bot_token es requerido.'; return; }
if (!this.form.chat_id.trim()) { this.formError = 'El chat_id es requerido.'; return; }
this.saving = true;
const url = this.editMode ? `/app/telegram/${this.form.id}` : '/app/telegram';
const method = this.editMode ? 'PUT' : 'POST';
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.form) });
this.saving = false;
if (!res.ok) { const d = await res.json(); this.formError = d.error || 'Error al guardar.'; return; }
this.closeModal();
await this.loadConfigs();
},
confirmDelete(id) { this.deleteId = id; this.deleteModal = true; },
async doDelete() {
await fetch(`/app/telegram/${this.deleteId}`, { method: 'DELETE' });
this.deleteModal = false;
await this.loadConfigs();
},
async testConfig(cfg) {
this.$set(this.testLoading, cfg.ID, true);
this.$set(this.testResult, cfg.ID, '');
const res = await fetch(`/app/telegram/${cfg.ID}/test`, { method: 'POST' });
const d = await res.json();
this.$set(this.testOk, cfg.ID, d.ok);
this.$set(this.testResult, cfg.ID, d.ok ? '✅ Mensaje enviado correctamente.' : ('❌ ' + (d.error || 'Error')));
this.$set(this.testLoading, cfg.ID, false);
},
async sendMessage() {
this.sendResult = '';
if (!this.sendForm.mensaje.trim()) { this.sendResult = 'El mensaje no puede estar vacío.'; this.sendOk = false; return; }
if (!this.sendForm.config_ids.length) { this.sendResult = 'Selecciona al menos un destino.'; this.sendOk = false; return; }
this.sendLoading = true;
const res = await fetch('/app/telegram/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.sendForm),
});
const d = await res.json();
this.sendOk = d.ok;
this.sendResult = d.ok
? `✅ Enviado a ${d.enviados} destino(s).`
: `⚠️ Enviados: ${d.enviados}, Fallidos: ${d.fallidos}`;
this.sendLoading = false;
},
async loadLogs(p = 1) {
if (p < 1 || p > this.logTotalPages && this.logTotalPages > 0) return;
this.logPage = p;
const res = await fetch(`/app/telegram/logs?page=${p}`);
const d = await res.json();
this.logs = d.items || [];
this.logTotal = d.total;
this.logTotalPages = d.totalPages || 1;
},
fmtDate(s) {
if (!s) return '-';
const d = new Date(s);
return d.toLocaleString('es-CO', { dateStyle: 'short', timeStyle: 'short' });
},
};
}
</script>