up
This commit is contained in:
@@ -197,10 +197,10 @@ function notifConfig() {
|
|||||||
toast: { visible: false, ok: true, msg: '' },
|
toast: { visible: false, ok: true, msg: '' },
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// Verificar si hay telegram configurado
|
// Verificar si hay telegram configurado (la API devuelve array directo)
|
||||||
try {
|
try {
|
||||||
const t = await axios.get('/app/loadtelegram');
|
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 {}
|
} catch {}
|
||||||
|
|
||||||
// Cargar configuración existente
|
// Cargar configuración existente
|
||||||
|
|||||||
@@ -72,8 +72,14 @@
|
|||||||
<div x-show="tab === 'usuarios'">
|
<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>
|
<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">
|
<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"
|
<button @click="toggleSortVenc()" :disabled="loadingVenc"
|
||||||
:class="sortByVenc ? 'bg-[#8eb02f] text-white border-[#8eb02f]' : 'text-gray-600 border-gray-300 hover:bg-gray-50'"
|
: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">
|
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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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">
|
<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 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>
|
<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
|
// Ordenar usuarios por vencimiento
|
||||||
sortByVenc: false,
|
sortByVenc: false,
|
||||||
loadingVenc: false,
|
loadingVenc: false,
|
||||||
|
filterPlan: '',
|
||||||
membresiaMap: {}, // { userId: { fecha_fin, activo, plan } }
|
membresiaMap: {}, // { userId: { fecha_fin, activo, plan } }
|
||||||
|
|
||||||
// Desactivar usuario + VCards
|
// Desactivar usuario + VCards
|
||||||
@@ -1811,15 +1818,7 @@ function vcardApiApp() {
|
|||||||
|
|
||||||
// ─── Ordenar por vencimiento ─────────────────────────────────────────
|
// ─── 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) {
|
diasHastaVencer(u) {
|
||||||
if (this._isPaidPlan(u)) return null; // plan de pago: sin vencimiento
|
|
||||||
const id = u.id || u._id;
|
const id = u.id || u._id;
|
||||||
const m = this.membresiaMap[id];
|
const m = this.membresiaMap[id];
|
||||||
if (!m || !m.fecha_fin) return null;
|
if (!m || !m.fecha_fin) return null;
|
||||||
@@ -1845,7 +1844,6 @@ function vcardApiApp() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
vencimientoTexto(u) {
|
vencimientoTexto(u) {
|
||||||
if (this._isPaidPlan(u)) return '—'; // plan de pago: no aplica vencimiento
|
|
||||||
const id = u.id || u._id;
|
const id = u.id || u._id;
|
||||||
const m = this.membresiaMap[id];
|
const m = this.membresiaMap[id];
|
||||||
if (this.loadingVenc && !m) return '...';
|
if (this.loadingVenc && !m) return '...';
|
||||||
@@ -1860,10 +1858,8 @@ function vcardApiApp() {
|
|||||||
async _fetchAndSortVenc() {
|
async _fetchAndSortVenc() {
|
||||||
this.loadingVenc = true;
|
this.loadingVenc = true;
|
||||||
const batchSize = 5;
|
const batchSize = 5;
|
||||||
// Solo buscar membresía para usuarios free (los de plan de pago no tienen vencimiento relevante)
|
for (let i = 0; i < this.items.length; i += batchSize) {
|
||||||
const usersToFetch = this.items.filter(u => !this._isPaidPlan(u));
|
const batch = this.items.slice(i, i + batchSize);
|
||||||
for (let i = 0; i < usersToFetch.length; i += batchSize) {
|
|
||||||
const batch = usersToFetch.slice(i, i + batchSize);
|
|
||||||
await Promise.all(batch.map(async u => {
|
await Promise.all(batch.map(async u => {
|
||||||
const id = u.id || u._id;
|
const id = u.id || u._id;
|
||||||
if (this.membresiaMap[id]) return;
|
if (this.membresiaMap[id]) return;
|
||||||
@@ -1874,32 +1870,15 @@ function vcardApiApp() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
this.loadingVenc = false;
|
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) => {
|
this.items = [...this.items].sort((a, b) => {
|
||||||
const paidA = this._isPaidPlan(a);
|
const ma = this.membresiaMap[a.id || a._id];
|
||||||
const paidB = this._isPaidPlan(b);
|
const mb = this.membresiaMap[b.id || b._id];
|
||||||
const premA = a.premium === true || a.premium === 'true';
|
const fa = ma?.fecha_fin ? new Date(ma.fecha_fin) : null;
|
||||||
const premB = b.premium === true || b.premium === 'true';
|
const fb = mb?.fecha_fin ? new Date(mb.fecha_fin) : null;
|
||||||
const grp = (isPaid, isPrem) => {
|
if (!fa && !fb) return 0;
|
||||||
if (!isPaid && isPrem) return 0; // free + premium → primero
|
if (!fa) return 1;
|
||||||
if (isPaid) return 1; // plan pagado → en medio
|
if (!fb) return -1;
|
||||||
return 2; // free sin premium → al final
|
return fa - fb;
|
||||||
};
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1926,6 +1905,16 @@ function vcardApiApp() {
|
|||||||
return montos.slice(0, 5); // max 5 precios únicos
|
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 ──────────────────────────────────────────────────────────
|
// ─── Planes ──────────────────────────────────────────────────────────
|
||||||
openVerPlanModal(p) {
|
openVerPlanModal(p) {
|
||||||
this.verPlanData = p;
|
this.verPlanData = p;
|
||||||
|
|||||||
Reference in New Issue
Block a user