This commit is contained in:
Lizandro Guarnizo
2026-05-25 14:37:59 -05:00
parent 471b2f50c6
commit 03c6e74bd6
2 changed files with 31 additions and 42 deletions
+2 -2
View File
@@ -197,10 +197,10 @@ function notifConfig() {
toast: { visible: false, ok: true, msg: '' },
async init() {
// Verificar si hay telegram configurado
// Verificar si hay telegram configurado (la API devuelve array directo)
try {
const t = await axios.get('/app/loadtelegram');
this.hasTelegramConfig = (t.data?.items?.length ?? 0) > 0;
this.hasTelegramConfig = Array.isArray(t.data) && t.data.some(b => b.activo);
} catch {}
// Cargar configuración existente
+29 -40
View File
@@ -72,8 +72,14 @@
<div x-show="tab === 'usuarios'">
<div x-show="items.length === 0 && !loading" class="text-sm text-gray-400 py-8 text-center">Sin datos. Haz clic en "Cargar".</div>
<!-- Toolbar: ordenar por vencimiento -->
<!-- Toolbar: filtro plan + ordenar por vencimiento -->
<div class="flex flex-wrap items-center gap-3 mb-3" x-show="items.length > 0">
<select x-model="filterPlan" class="border border-gray-300 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Todos los planes —</option>
<template x-for="p in planesDisponibles()" :key="p">
<option :value="p" x-text="p"></option>
</template>
</select>
<button @click="toggleSortVenc()" :disabled="loadingVenc"
:class="sortByVenc ? 'bg-[#8eb02f] text-white border-[#8eb02f]' : 'text-gray-600 border-gray-300 hover:bg-gray-50'"
class="flex items-center gap-1.5 text-xs px-3 py-1.5 border rounded-lg transition disabled:opacity-50">
@@ -106,7 +112,7 @@
</tr>
</thead>
<tbody>
<template x-for="u in items" :key="u.id || u._id">
<template x-for="u in usuariosFiltrados()" :key="u.id || u._id">
<tr :class="sortByVenc ? vencimientoRowClass(u) : 'hover:bg-gray-50'" class="border-b border-gray-100 transition-colors">
<td class="py-2 px-3 font-medium" x-text="u.name || u.nombre || '—'"></td>
<td class="py-2 px-3 text-gray-500 text-xs" x-text="u.email || '—'"></td>
@@ -1574,6 +1580,7 @@ function vcardApiApp() {
// Ordenar usuarios por vencimiento
sortByVenc: false,
loadingVenc: false,
filterPlan: '',
membresiaMap: {}, // { userId: { fecha_fin, activo, plan } }
// Desactivar usuario + VCards
@@ -1811,15 +1818,7 @@ function vcardApiApp() {
// ─── Ordenar por vencimiento ─────────────────────────────────────────
// Retorna true para planes de pago (emprendimiento / normal).
// Estos planes no tienen vencimiento relevante en el contexto de membresía premium.
_isPaidPlan(u) {
const nombre = ((u.plan && u.plan.nombre) || '').toLowerCase();
return nombre.includes('emprendimiento') || nombre.includes('normal');
},
diasHastaVencer(u) {
if (this._isPaidPlan(u)) return null; // plan de pago: sin vencimiento
const id = u.id || u._id;
const m = this.membresiaMap[id];
if (!m || !m.fecha_fin) return null;
@@ -1845,7 +1844,6 @@ function vcardApiApp() {
},
vencimientoTexto(u) {
if (this._isPaidPlan(u)) return '—'; // plan de pago: no aplica vencimiento
const id = u.id || u._id;
const m = this.membresiaMap[id];
if (this.loadingVenc && !m) return '...';
@@ -1860,10 +1858,8 @@ function vcardApiApp() {
async _fetchAndSortVenc() {
this.loadingVenc = true;
const batchSize = 5;
// Solo buscar membresía para usuarios free (los de plan de pago no tienen vencimiento relevante)
const usersToFetch = this.items.filter(u => !this._isPaidPlan(u));
for (let i = 0; i < usersToFetch.length; i += batchSize) {
const batch = usersToFetch.slice(i, i + batchSize);
for (let i = 0; i < this.items.length; i += batchSize) {
const batch = this.items.slice(i, i + batchSize);
await Promise.all(batch.map(async u => {
const id = u.id || u._id;
if (this.membresiaMap[id]) return;
@@ -1874,32 +1870,15 @@ function vcardApiApp() {
}));
}
this.loadingVenc = false;
// Orden: 0 = free+premium (más urgente, por fecha), 1 = plan pagado, 2 = free sin premium
this.items = [...this.items].sort((a, b) => {
const paidA = this._isPaidPlan(a);
const paidB = this._isPaidPlan(b);
const premA = a.premium === true || a.premium === 'true';
const premB = b.premium === true || b.premium === 'true';
const grp = (isPaid, isPrem) => {
if (!isPaid && isPrem) return 0; // free + premium → primero
if (isPaid) return 1; // plan pagado → en medio
return 2; // free sin premium → al final
};
const ga = grp(paidA, premA);
const gb = grp(paidB, premB);
if (ga !== gb) return ga - gb;
// Dentro del grupo 0 (free+premium), ordenar por fecha_fin (más próxima primero)
if (ga === 0) {
const ma = this.membresiaMap[a.id || a._id];
const mb = this.membresiaMap[b.id || b._id];
const fa = ma?.fecha_fin ? new Date(ma.fecha_fin) : null;
const fb = mb?.fecha_fin ? new Date(mb.fecha_fin) : null;
if (!fa && !fb) return 0;
if (!fa) return 1;
if (!fb) return -1;
return fa - fb;
}
return 0;
const ma = this.membresiaMap[a.id || a._id];
const mb = this.membresiaMap[b.id || b._id];
const fa = ma?.fecha_fin ? new Date(ma.fecha_fin) : null;
const fb = mb?.fecha_fin ? new Date(mb.fecha_fin) : null;
if (!fa && !fb) return 0;
if (!fa) return 1;
if (!fb) return -1;
return fa - fb;
});
},
@@ -1926,6 +1905,16 @@ function vcardApiApp() {
return montos.slice(0, 5); // max 5 precios únicos
},
planesDisponibles() {
const nombres = [...new Set(this.items.map(u => (u.plan && u.plan.nombre) || ''))].filter(Boolean).sort();
return nombres;
},
usuariosFiltrados() {
if (!this.filterPlan) return this.items;
return this.items.filter(u => ((u.plan && u.plan.nombre) || '') === this.filterPlan);
},
// ─── Planes ──────────────────────────────────────────────────────────
openVerPlanModal(p) {
this.verPlanData = p;