Files
soft_usite/resources/views/vcard_api.html
T
2026-05-26 14:06:38 -05:00

2317 lines
150 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: VCard API — Integración Admin Laravel -->
<div x-data="vcardApiApp()" x-init="init()" class="bg-white rounded-lg shadow" @keydown.escape.window="closeModals()">
<!-- Overlay de carga -->
<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">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold">VCard API</h1>
<p class="text-xs text-slate-500 mt-0.5">Integración con el sistema VCard externo (Laravel + Sanctum). Gestiona usuarios, vcards, membresías y más.</p>
</div>
<button @click="configModal = true"
class="flex items-center gap-2 text-white text-sm font-medium px-4 py-2 rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
</svg>
Configurar API
</button>
</div>
<!-- Alerta sin configuración -->
<div x-show="!hasConfig" class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700">
<strong>Sin configuración activa.</strong> Haz clic en "Configurar API" para ingresar la URL base y el Bearer Token del sistema VCard.
</div>
<!-- Badge config activa -->
<div x-show="hasConfig && configData" class="mb-5 flex items-center gap-3 p-3 bg-green-50 border border-green-200 rounded-lg text-sm">
<span class="h-2 w-2 rounded-full bg-green-500 inline-block"></span>
<span class="text-green-800 font-medium" x-text="configData ? configData.nombre || configData.base_url : ''"></span>
<span class="text-green-600 text-xs" x-text="configData ? configData.base_url : ''"></span>
<span x-show="configData && !configData.activo" class="ml-2 px-2 py-0.5 text-xs rounded-full bg-red-100 text-red-600">Inactiva</span>
</div>
<!-- Tabs -->
<div class="border-b border-gray-200 mb-6">
<nav class="-mb-px flex flex-wrap gap-4 text-sm">
<button @click="setTab('usuarios')" :class="tab==='usuarios' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">Usuarios</button>
<button @click="setTab('vcards')" :class="tab==='vcards' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">VCards</button>
<button @click="setTab('planes')" :class="tab==='planes' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">Planes</button>
<button @click="setTab('pagos')" :class="tab==='pagos' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">Pagos</button>
<button @click="setTab('transacciones')" :class="tab==='transacciones' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">Transacciones</button>
<button @click="setTab('logs')" :class="tab==='logs' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">Logs</button>
<button @click="setTab('miniwebs')" :class="tab==='miniwebs' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'" class="pb-2 transition-colors">MiniWebs</button>
</nav>
</div>
<!-- Barra de búsqueda + cargar -->
<div class="flex flex-col sm:flex-row gap-3 mb-4">
<input x-model="search" @keyup.enter="loadTab()" type="text" placeholder="Buscar..." class="border border-gray-300 rounded-lg px-3 py-2 text-sm flex-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<button @click="loadTab()" :disabled="loading || !hasConfig"
class="flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 text-sm hover:bg-gray-50 transition disabled:opacity-40">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" :class="loading && 'animate-spin'" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/>
</svg>
<span x-text="loading ? 'Cargando...' : 'Cargar'"></span>
</button>
</div>
<!-- Alerta de error -->
<div x-show="errorMsg" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700" x-text="errorMsg"></div>
<!-- ─── 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>
<!-- 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">
<svg x-show="loadingVenc" class="w-3 h-3 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<svg x-show="!loadingVenc" class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span x-text="loadingVenc ? 'Cargando membresías...' : (sortByVenc ? '✓ Ordenado por vencimiento' : 'Ordenar por vencimiento')"></span>
</button>
<div class="flex items-center gap-2 text-xs" x-show="sortByVenc">
<span class="px-2 py-0.5 rounded-full bg-red-100 text-red-700">Vencido / ≤7 días</span>
<span class="px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">830 días</span>
<span class="px-2 py-0.5 rounded-full bg-green-100 text-green-700">&gt;30 días</span>
</div>
</div>
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Nombre</th>
<th class="py-2 px-3">Email</th>
<th class="py-2 px-3">Rol</th>
<th class="py-2 px-3">Plan</th>
<th class="py-2 px-3" x-show="sortByVenc">Vence</th>
<th class="py-2 px-3">Estado</th>
<th class="py-2 px-3 text-right">Acciones</th>
</tr>
</thead>
<tbody>
<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>
<td class="py-2 px-3">
<span :class="(u.rol && u.rol.nombre === 'administrador') ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="(u.rol && u.rol.nombre) ? u.rol.nombre : '—'"></span>
</td>
<td class="py-2 px-3 text-xs" x-text="(u.plan && u.plan.nombre) ? u.plan.nombre : '—'"></td>
<td class="py-2 px-3 text-xs" x-show="sortByVenc">
<span :class="vencimientoBadgeClass(u)" class="px-2 py-0.5 rounded-full whitespace-nowrap" x-text="vencimientoTexto(u)"></span>
</td>
<td class="py-2 px-3">
<span :class="(u.estado === true || u.estado === 'true') ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full"
x-text="(u.estado === true || u.estado === 'true') ? 'Activo' : 'Inactivo'"></span>
</td>
<td class="py-2 px-3 text-right">
<div class="flex justify-end gap-2 flex-wrap">
<button @click="openVerModal(u)" title="Ver detalle"
class="text-indigo-500 hover:text-indigo-700 text-xs px-2 py-1 border border-indigo-200 rounded">Ver</button>
<button @click="openEditUsuarioModal(u)" title="Editar"
class="text-gray-600 hover:text-gray-800 text-xs px-2 py-1 border border-gray-300 rounded">Editar</button>
<button x-show="!(u.estado === true || u.estado === 'true')" @click="toggleUsuario(u, 'activar')"
class="text-green-500 hover:text-green-700 text-xs px-2 py-1 border border-green-300 rounded">Activar</button>
<button x-show="u.estado === true || u.estado === 'true'" @click="toggleUsuario(u, 'desactivar')"
class="text-red-400 hover:text-red-600 text-xs px-2 py-1 border border-red-200 rounded">Desactivar</button>
<button x-show="u.estado === true || u.estado === 'true'" @click="openDesactivarConVcardsModal(u)"
class="text-orange-500 hover:text-orange-700 text-xs px-2 py-1 border border-orange-300 rounded" title="Inactiva todas las VCards (sin desactivar usuario)">
🚫 VCards
</button>
<button @click="openMembresiaModal(u)"
class="text-blue-500 hover:text-blue-700 text-xs px-2 py-1 border border-blue-200 rounded">Membresía</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- ─── TAB: VCards ─── -->
<div x-show="tab === 'vcards'">
<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 class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Nombre / Cargo</th>
<th class="py-2 px-3">Usuario</th>
<th class="py-2 px-3">Empresa</th>
<th class="py-2 px-3">Email</th>
<th class="py-2 px-3">URL</th>
<th class="py-2 px-3">Estado</th>
<th class="py-2 px-3">Borrador</th>
<th class="py-2 px-3">Privacidad</th>
<th class="py-2 px-3">País / Ciudad</th>
<th class="py-2 px-3">Creada</th>
<th class="py-2 px-3 text-right">Acciones</th>
</tr>
</thead>
<tbody>
<template x-for="v in items" :key="v.id || v._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3">
<div class="font-medium" x-text="v.nombre || '—'"></div>
<div class="text-xs text-gray-400" x-text="v.cargo || ''"></div>
</td>
<td class="py-2 px-3 text-xs text-gray-500">
<div x-text="(v.user && v.user.name) ? v.user.name : '—'"></div>
<div class="text-gray-400" x-text="(v.user && v.user.email) ? v.user.email : (v.user_id || '')"></div>
</td>
<td class="py-2 px-3 text-gray-500 text-xs" x-text="v.empresa || '—'"></td>
<td class="py-2 px-3 text-gray-500 text-xs" x-text="v.email || '—'"></td>
<td class="py-2 px-3 text-xs">
<a :href="v.url || (v.unico ? 'https://vcard.u-site.app/' + v.unico : null)" target="_blank"
x-show="v.unico || v.url"
class="text-blue-500 hover:underline font-mono" x-text="v.unico || '—'"></a>
<span x-show="!v.unico && !v.url" class="text-gray-400"></span>
</td>
<td class="py-2 px-3">
<span :class="v.estado ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="v.estado ? 'Activa' : 'Inactiva'"></span>
</td>
<td class="py-2 px-3">
<span x-show="v.es_borrador" class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">Borrador</span>
<span x-show="!v.es_borrador" class="text-xs text-gray-300"></span>
</td>
<td class="py-2 px-3">
<span :class="v.privacidad === 'publica' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="v.privacidad || '—'"></span>
</td>
<td class="py-2 px-3 text-xs text-gray-500">
<div x-text="v.pais || '—'"></div>
<div class="text-gray-400" x-text="v.ciudad || ''"></div>
</td>
<td class="py-2 px-3 text-xs text-gray-400" x-text="v.created_at ? v.created_at.substring(0,10) : '—'"></td>
<td class="py-2 px-3 text-right">
<div class="flex justify-end gap-2">
<button @click="openVerVcardModal(v)"
class="text-indigo-500 hover:text-indigo-700 text-xs px-2 py-1 border border-indigo-200 rounded">Ver</button>
<button @click="openEditVcardModal(v)"
class="text-gray-600 hover:text-gray-800 text-xs px-2 py-1 border border-gray-300 rounded">Editar</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- ─── TAB: Planes ─── -->
<div x-show="tab === 'planes'">
<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 class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" x-show="items.length > 0">
<template x-for="p in items" :key="p.id || p._id">
<div class="border rounded-xl p-4 bg-white shadow-sm hover:shadow-md transition">
<!-- Header -->
<div class="flex items-start justify-between mb-2 gap-2">
<div class="flex items-center gap-2">
<img x-show="p.img" :src="p.img ? 'https://vcard.u-site.app/' + p.img : null"
class="h-8 w-8 rounded object-cover border shrink-0" onerror="this.style.display='none'" />
<span class="font-semibold text-sm" x-text="p.nombre || '—'"></span>
</div>
<span :class="p.estado ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
class="text-xs px-2 py-0.5 rounded-full shrink-0" x-text="p.estado ? 'Activo' : 'Inactivo'"></span>
</div>
<!-- Descripción -->
<p class="text-xs text-gray-500 mb-2 line-clamp-2" x-text="p.descripcion || ''"></p>
<!-- Cantidad vcards -->
<div x-show="p.cantidad" class="text-xs text-gray-600 mb-2">
<span class="font-medium">VCards permitidas:</span>
<span x-text="p.cantidad"></span>
</div>
<!-- Precios únicos (resumidos) -->
<div x-show="p.tarifas && p.tarifas.length" class="mt-2">
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Precios (muestra)</p>
<div class="flex flex-wrap gap-1">
<template x-for="t in uniqueMontos(p.tarifas || [])" :key="t">
<span class="text-xs bg-gray-100 text-gray-700 px-2 py-0.5 rounded font-mono" x-text="t === '0' || t === 'Gratis' ? 'Gratis' : ('$' + t)"></span>
</template>
</div>
<p class="text-xs text-gray-400 mt-1" x-text="(p.tarifas||[]).length + ' tarifas por país'"></p>
</div>
<!-- ID -->
<p class="text-xs text-gray-300 font-mono mt-3 truncate" x-text="p._id || p.id"></p>
<!-- Acciones -->
<div class="flex gap-2 mt-3 pt-3 border-t">
<button @click="openVerPlanModal(p)"
class="flex-1 px-3 py-1.5 text-xs border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-600">
Ver
</button>
<button @click="openEditPlanModal(p)"
class="flex-1 px-3 py-1.5 text-xs text-white rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
Editar
</button>
</div>
</div>
</template>
</div>
</div>
<!-- ─── TAB: Pagos ─── -->
<div x-show="tab === 'pagos'">
<!-- Filtros pagos -->
<div class="flex flex-wrap gap-3 mb-4">
<select x-model="filters.pagos.estado" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Estado —</option>
<option value="pendiente">Pendiente</option>
<option value="completado">Completado</option>
<option value="fallido">Fallido</option>
</select>
<input x-model="filters.pagos.moneda" type="text" placeholder="Moneda (USD, COP...)"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="filters.pagos.desde" type="date"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="filters.pagos.hasta" type="date"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<button @click="clearFilters('pagos')" class="text-xs text-gray-400 hover:text-gray-600 px-2">✕ Limpiar</button>
</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>
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Referencia</th>
<th class="py-2 px-3">Usuario</th>
<th class="py-2 px-3">Monto</th>
<th class="py-2 px-3">Estado</th>
<th class="py-2 px-3">Fecha</th>
</tr>
</thead>
<tbody>
<template x-for="p in items" :key="p.id || p._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3 font-mono text-xs" x-text="p.referencia || p.reference || '—'"></td>
<td class="py-2 px-3 text-gray-500" x-text="p.user_id || '—'"></td>
<td class="py-2 px-3 font-medium" x-text="(p.moneda || '') + ' ' + (p.monto || p.amount || '—')"></td>
<td class="py-2 px-3">
<span :class="{
'bg-green-100 text-green-700': p.estado === 'completado',
'bg-yellow-100 text-yellow-700': p.estado === 'pendiente',
'bg-red-100 text-red-700': p.estado === 'fallido'
}" class="text-xs px-2 py-0.5 rounded-full" x-text="p.estado || '—'"></span>
</td>
<td class="py-2 px-3 text-xs text-gray-400" x-text="p.created_at ? p.created_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- ─── TAB: Transacciones ─── -->
<div x-show="tab === 'transacciones'">
<!-- Filtros transacciones -->
<div class="flex flex-wrap gap-3 mb-4">
<select x-model="filters.transacciones.type" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Tipo —</option>
<option value="deposit">deposit</option>
<option value="withdraw">withdraw</option>
<option value="transfer_in">transfer_in</option>
<option value="transfer_out">transfer_out</option>
<option value="purchase">purchase</option>
<option value="adjustment">adjustment</option>
</select>
<select x-model="filters.transacciones.status" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Estado —</option>
<option value="pending">pending</option>
<option value="completed">completed</option>
<option value="failed">failed</option>
</select>
<button @click="clearFilters('transacciones')" class="text-xs text-gray-400 hover:text-gray-600 px-2">✕ Limpiar</button>
</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>
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Tipo</th>
<th class="py-2 px-3">Monto</th>
<th class="py-2 px-3">Estado</th>
<th class="py-2 px-3">Descripción</th>
<th class="py-2 px-3">Fecha</th>
</tr>
</thead>
<tbody>
<template x-for="t in items" :key="t.id || t._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3">
<span class="bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded font-mono" x-text="t.type || '—'"></span>
</td>
<td class="py-2 px-3 font-medium" x-text="t.amount || '—'"></td>
<td class="py-2 px-3">
<span :class="{
'bg-green-100 text-green-700': t.status === 'completed',
'bg-yellow-100 text-yellow-700': t.status === 'pending',
'bg-red-100 text-red-700': t.status === 'failed'
}" class="text-xs px-2 py-0.5 rounded-full" x-text="t.status || '—'"></span>
</td>
<td class="py-2 px-3 text-xs text-gray-500 max-w-xs truncate" x-text="t.description || '—'"></td>
<td class="py-2 px-3 text-xs text-gray-400" x-text="t.created_at ? t.created_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- ─── TAB: Logs ─── -->
<div x-show="tab === 'logs'">
<!-- Filtros logs -->
<div class="flex flex-wrap gap-3 mb-4">
<select x-model="filters.logs.type" class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Tipo —</option>
<option value="create">create</option>
<option value="edit">edit</option>
<option value="delete">delete</option>
<option value="delete cuenta">delete cuenta</option>
</select>
<input x-model="filters.logs.desde" type="date"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="filters.logs.hasta" type="date"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<button @click="clearFilters('logs')" class="text-xs text-gray-400 hover:text-gray-600 px-2">✕ Limpiar</button>
</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>
<div class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Tipo</th>
<th class="py-2 px-3">Usuario</th>
<th class="py-2 px-3">Descripción</th>
<th class="py-2 px-3">Fecha</th>
</tr>
</thead>
<tbody>
<template x-for="l in items" :key="l.id || l._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3">
<span class="bg-gray-100 text-gray-700 text-xs px-2 py-0.5 rounded font-mono" x-text="l.type || l.tipo || '—'"></span>
</td>
<td class="py-2 px-3 text-gray-500 text-xs" x-text="(l.user && l.user.email) ? l.user.email : (l.user_id || '—')"></td>
<td class="py-2 px-3 text-xs text-gray-500 max-w-xs truncate" x-text="l.description || l.descripcion || '—'"></td>
<td class="py-2 px-3 text-xs text-gray-400" x-text="l.created_at ? l.created_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- ─── TAB: MiniWebs ─── -->
<div x-show="tab === 'miniwebs'">
<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 class="overflow-x-auto">
<table class="table-auto w-full text-sm" x-show="items.length > 0">
<thead class="border-b border-gray-200 text-left text-xs font-semibold text-gray-500 uppercase">
<tr>
<th class="py-2 px-3">Título</th>
<th class="py-2 px-3">Usuario</th>
<th class="py-2 px-3">VCard vinculada</th>
<th class="py-2 px-3">Estado</th>
</tr>
</thead>
<tbody>
<template x-for="m in items" :key="m.id || m._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3 font-medium" x-text="m.titulo || m.title || '—'"></td>
<td class="py-2 px-3 text-xs text-gray-500" x-text="(m.user && m.user.email) ? m.user.email : (m.user_id || '—')"></td>
<td class="py-2 px-3 text-xs text-gray-500" x-text="(m.vcard && m.vcard.nombre) ? m.vcard.nombre : '—'"></td>
<td class="py-2 px-3">
<span :class="m.activo ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="m.activo ? 'Activa' : 'Inactiva'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Paginación -->
<div x-show="total > 0" class="flex justify-between items-center mt-5 text-sm text-gray-500">
<span>Total: <b x-text="total"></b></span>
<div class="flex gap-1">
<button @click="changePage(page - 1)" :disabled="page <= 1" class="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-50"></button>
<span class="px-3 py-1" x-text="'Pág. ' + page + ' / ' + totalPages"></span>
<button @click="changePage(page + 1)" :disabled="page >= totalPages" class="px-3 py-1 border rounded disabled:opacity-40 hover:bg-gray-50"></button>
</div>
</div>
</div>
<!-- ─── Modal: Configurar API ─────────────────────────────────────────── -->
<div x-show="configModal" 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">Configurar VCard API</h2>
<button @click="configModal = false" 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</label>
<input x-model="form.nombre" type="text" placeholder="Ej: VCard Producción"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">URL Base <span class="text-red-500">*</span></label>
<input x-model="form.base_url" type="url" placeholder="https://midominio.com"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<p class="text-xs text-gray-400 mt-1">Sin barra final. Ej: https://midominio.com</p>
</div>
<!-- Login para obtener token automáticamente -->
<div class="border border-gray-200 rounded-lg p-4 space-y-3 bg-gray-50">
<p class="text-xs font-medium text-gray-600 uppercase tracking-wide">Obtener token automáticamente</p>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs text-gray-600 mb-1">Email admin</label>
<input x-model="loginForm.email" type="email" placeholder="admin@dominio.com"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f] bg-white" />
</div>
<div>
<label class="block text-xs text-gray-600 mb-1">Contraseña</label>
<input x-model="loginForm.password" type="password" placeholder="••••••••"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f] bg-white" />
</div>
</div>
<button @click="obtenerToken()" :disabled="loginLoading"
class="w-full px-4 py-2 text-sm border border-[#8eb02f] text-[#8eb02f] rounded-lg hover:bg-[#8eb02f] hover:text-white transition-colors disabled:opacity-50">
<span x-text="loginLoading ? 'Conectando...' : '⚡ Obtener Token'"></span>
</button>
<div x-show="loginMsg" class="text-xs rounded px-3 py-2"
:class="loginOk ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-600 border border-red-200'"
x-text="loginMsg"></div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bearer Token <span class="text-red-500">*</span></label>
<input x-model="form.bearer_token" type="password" placeholder="1|abc123..."
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<p class="text-xs text-gray-400 mt-1">Se rellena automáticamente al obtener el token, o pégalo manualmente.</p>
</div>
<div class="flex items-center gap-2">
<input x-model="form.activo" type="checkbox" id="vcardActivo" class="rounded" />
<label for="vcardActivo" class="text-sm text-gray-700">Integración activa</label>
</div>
<div x-show="configError" class="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="configError"></div>
</div>
<div class="flex justify-end gap-3 px-5 pb-5">
<button @click="configModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cancelar</button>
<button @click="saveConfig()" :disabled="saving"
class="px-4 py-2 text-sm text-white rounded-lg disabled:opacity-50"
style="background-color:#8eb02f"
onmouseover="if(!this.disabled)this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
<span x-text="saving ? 'Guardando...' : 'Guardar'"></span>
</button>
</div>
</div>
</div>
<!-- ─── Modal: Ver Usuario ───────────────────────────────────────────── -->
<div x-show="verModal" 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-2xl mx-4 max-h-[90vh] flex flex-col">
<!-- Header -->
<div class="flex justify-between items-start p-5 border-b shrink-0">
<div>
<h2 class="text-lg font-semibold" x-text="verData ? (verData.name || verData.nombre || '—') : 'Cargando...'"></h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="verData ? (verData.email || '') : ''"></p>
<div class="mt-1 flex gap-2 flex-wrap">
<template x-if="verData">
<span :class="(verData.estado === true || verData.estado === 'true') ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full"
x-text="(verData.estado === true || verData.estado === 'true') ? 'Activo' : 'Inactivo'"></span>
</template>
<template x-if="verData && verData.rol">
<span :class="verData.rol.nombre === 'administrador' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verData.rol.nombre || '—'"></span>
</template>
</div>
</div>
<button @click="verModal = false" class="text-gray-400 hover:text-gray-600 text-xl shrink-0"></button>
</div>
<!-- Tabs -->
<div class="border-b px-5 flex gap-1 overflow-x-auto shrink-0">
<template x-for="tab in ['general','perfil','vcards','wallet','pagos','logs','miniwebs']" :key="tab">
<button @click="verTab = tab; loadVerSection(tab)"
:class="verTab === tab ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
class="text-xs py-2 px-3 whitespace-nowrap capitalize">
<span x-text="tab.charAt(0).toUpperCase() + tab.slice(1)"></span>
</button>
</template>
</div>
<!-- Content -->
<div class="overflow-y-auto flex-1 p-5">
<div x-show="verLoading" class="text-sm text-gray-400 text-center py-8">Cargando...</div>
<div x-show="verError && !verLoading" class="text-sm text-red-500" x-text="verError"></div>
<!-- Tab General -->
<template x-if="!verLoading && verTab === 'general' && verData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Nombre</span><span class="font-medium" x-text="verData.name || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Email</span><span x-text="verData.email || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Rol</span>
<span :class="verData.rol && verData.rol.nombre === 'administrador' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="(verData.rol && verData.rol.nombre) ? verData.rol.nombre : (verData.rol_id || '—')"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Plan</span>
<span class="text-xs px-2 py-0.5 bg-blue-50 text-blue-700 rounded-full" x-text="(verData.plan && verData.plan.nombre) ? verData.plan.nombre : (verData.plan_id || '—')"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Estado</span>
<span :class="(verData.estado === true || verData.estado === 'true') ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full"
x-text="(verData.estado === true || verData.estado === 'true') ? 'Activo' : 'Inactivo'"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Premium</span>
<span :class="verData.premium ? 'bg-yellow-100 text-yellow-700' : 'text-gray-500'"
class="text-xs" x-text="verData.premium ? 'Sí' : 'No'"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Anual</span><span class="text-xs" x-text="verData.anual ? 'Sí' : 'No'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Renovación</span><span class="text-xs" x-text="verData.renovacion || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Fecha Premium</span><span class="text-xs" x-text="verData.premium_date ? verData.premium_date.substring(0,10) : '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Referido por</span><span class="text-xs" x-text="verData.reference_by || '—'"></span></div>
<div class="sm:col-span-2"><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">ID</span><span class="text-xs font-mono text-gray-400" x-text="verData._id || verData.id || '—'"></span></div>
</div>
</template>
<!-- Tab Perfil -->
<template x-if="!verLoading && verTab === 'perfil' && verData">
<div>
<template x-if="verData.perfil">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Email notificación</span><span x-text="verData.perfil.email_notificacion || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Ciudad</span><span x-text="verData.perfil.ciudad || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Indicativo</span><span x-text="verData.perfil.indicativo || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">País ID</span><span class="text-xs font-mono" x-text="verData.perfil.pais_id || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Empresa</span><span x-text="verData.perfil.nombre_empresa || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Sitio web</span>
<a :href="verData.perfil.sitio_web" target="_blank" x-show="verData.perfil.sitio_web"
class="text-blue-600 hover:underline text-xs" x-text="verData.perfil.sitio_web"></a>
<span x-show="!verData.perfil.sitio_web" class="text-gray-400"></span>
</div>
<div x-show="verData.perfil.logo" class="sm:col-span-2">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1">Logo</span>
<img :src="verData.perfil.logo" class="h-16 rounded border" alt="Logo" onerror="this.style.display='none'"/>
</div>
</div>
</template>
<template x-if="!verData.perfil">
<p class="text-sm text-gray-400">Sin datos de perfil.</p>
</template>
</div>
</template>
<!-- Tab VCards -->
<template x-if="!verLoading && verTab === 'vcards'">
<div>
<div x-show="!verData || !verData.vcards || verData.vcards.length === 0" class="text-sm text-gray-400">Sin VCards.</div>
<div x-show="verData && verData.vcards && verData.vcards.length > 0" class="overflow-x-auto">
<table class="table-auto w-full text-xs">
<thead class="text-gray-500 uppercase border-b">
<tr>
<th class="py-1 px-2">Nombre</th>
<th class="py-1 px-2">Empresa</th>
<th class="py-1 px-2">Email</th>
<th class="py-1 px-2">URL</th>
<th class="py-1 px-2">Estado</th>
<th class="py-1 px-2">Privacidad</th>
</tr>
</thead>
<tbody>
<template x-for="v in (verData && verData.vcards ? verData.vcards : [])" :key="v.id || v._id">
<tr class="border-b border-gray-100 hover:bg-gray-50">
<td class="py-1 px-2 font-medium" x-text="v.nombre || '—'"></td>
<td class="py-1 px-2 text-gray-500" x-text="v.empresa || '—'"></td>
<td class="py-1 px-2 text-gray-500" x-text="v.email || '—'"></td>
<td class="py-1 px-2 text-blue-500">
<a :href="v.url ? 'https://vcard.u-site.app/'+v.url : null" target="_blank" x-text="v.url || '—'" class="hover:underline"></a>
</td>
<td class="py-1 px-2">
<span :class="v.estado ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="px-1.5 py-0.5 rounded-full" x-text="v.estado ? 'Activa' : 'Inactiva'"></span>
</td>
<td class="py-1 px-2">
<span :class="v.privacidad === 'publica' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'"
class="px-1.5 py-0.5 rounded-full" x-text="v.privacidad || '—'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- Tab Wallet -->
<template x-if="!verLoading && verTab === 'wallet'">
<div>
<template x-if="verData && verData.wallet">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Balance</span>
<span class="text-lg font-semibold" x-text="verData.wallet.balance || '0'"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Moneda</span><span x-text="verData.wallet.moneda || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Estado</span>
<span :class="verData.wallet.activo ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verData.wallet.activo ? 'Activa' : 'Inactiva'"></span>
</div>
</div>
</template>
<template x-if="!(verData && verData.wallet)">
<p class="text-sm text-gray-400">Sin wallet asociada.</p>
</template>
</div>
</template>
<!-- Tab Pagos -->
<template x-if="!verLoading && verTab === 'pagos'">
<div>
<div x-show="verSectionLoading" class="text-xs text-gray-400">Cargando pagos...</div>
<div x-show="verSectionItems.length === 0 && !verSectionLoading" class="text-sm text-gray-400">Sin pagos.</div>
<div x-show="verSectionItems.length > 0" class="overflow-x-auto">
<table class="table-auto w-full text-xs">
<thead class="text-gray-500 uppercase border-b">
<tr><th class="py-1 px-2">Referencia</th><th class="py-1 px-2">Monto</th><th class="py-1 px-2">Estado</th><th class="py-1 px-2">Fecha</th></tr>
</thead>
<tbody>
<template x-for="r in verSectionItems" :key="r.id || r._id">
<tr class="border-b border-gray-100">
<td class="py-1 px-2 font-mono" x-text="r.referencia || r.reference || '—'"></td>
<td class="py-1 px-2 font-medium" x-text="(r.moneda||'') + ' ' + (r.monto || r.amount || '—')"></td>
<td class="py-1 px-2">
<span :class="{'bg-green-100 text-green-700': r.estado==='completado','bg-yellow-100 text-yellow-700': r.estado==='pendiente','bg-red-100 text-red-700': r.estado==='fallido'}"
class="px-1.5 py-0.5 rounded-full" x-text="r.estado || '—'"></span>
</td>
<td class="py-1 px-2 text-gray-400" x-text="r.created_at ? r.created_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- Tab Logs -->
<template x-if="!verLoading && verTab === 'logs'">
<div>
<div x-show="verSectionLoading" class="text-xs text-gray-400">Cargando logs...</div>
<div x-show="verSectionItems.length === 0 && !verSectionLoading" class="text-sm text-gray-400">Sin logs.</div>
<div x-show="verSectionItems.length > 0" class="overflow-x-auto">
<table class="table-auto w-full text-xs">
<thead class="text-gray-500 uppercase border-b">
<tr><th class="py-1 px-2">Tipo</th><th class="py-1 px-2">Descripción</th><th class="py-1 px-2">Fecha</th></tr>
</thead>
<tbody>
<template x-for="r in verSectionItems" :key="r.id || r._id">
<tr class="border-b border-gray-100">
<td class="py-1 px-2 font-mono text-gray-700" x-text="r.type || r.tipo || '—'"></td>
<td class="py-1 px-2 text-gray-500 max-w-xs truncate" x-text="r.description || r.descripcion || '—'"></td>
<td class="py-1 px-2 text-gray-400" x-text="r.created_at ? r.created_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
<!-- Tab MiniWebs -->
<template x-if="!verLoading && verTab === 'miniwebs'">
<div>
<div x-show="verSectionLoading" class="text-xs text-gray-400">Cargando miniwebs...</div>
<div x-show="verSectionItems.length === 0 && !verSectionLoading" class="text-sm text-gray-400">Sin miniwebs.</div>
<div x-show="verSectionItems.length > 0" class="overflow-x-auto">
<table class="table-auto w-full text-xs">
<thead class="text-gray-500 uppercase border-b">
<tr><th class="py-1 px-2">Título</th><th class="py-1 px-2">VCard</th><th class="py-1 px-2">Estado</th></tr>
</thead>
<tbody>
<template x-for="r in verSectionItems" :key="r.id || r._id">
<tr class="border-b border-gray-100">
<td class="py-1 px-2 font-medium" x-text="r.titulo || r.title || '—'"></td>
<td class="py-1 px-2 text-gray-500" x-text="(r.vcard && r.vcard.nombre) ? r.vcard.nombre : '—'"></td>
<td class="py-1 px-2">
<span :class="r.activo ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="px-1.5 py-0.5 rounded-full" x-text="r.activo ? 'Activa' : 'Inactiva'"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
<!-- Footer -->
<div class="flex justify-between items-center gap-3 px-5 py-4 border-t shrink-0">
<button @click="openEditUsuarioModal(verData); verModal = false"
class="px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">✏ Editar</button>
<button @click="verModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cerrar</button>
</div>
</div>
</div>
<!-- ─── Modal: Editar Usuario ────────────────────────────────────────── -->
<div x-show="editUsuarioModal" 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">
<div>
<h2 class="text-lg font-semibold">Editar Usuario</h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="editUsuarioData ? (editUsuarioData.email || '') : ''"></p>
</div>
<button @click="editUsuarioModal = false" 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</label>
<input x-model="editUsuarioForm.name" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input x-model="editUsuarioForm.email" type="email"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Plan ID</label>
<select x-model="editUsuarioForm.plan_id"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Sin cambiar —</option>
<template x-for="pl in planes" :key="pl.id || pl._id">
<option :value="pl.id || pl._id" x-text="pl.nombre"></option>
</template>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Rol</label>
<select x-model="editUsuarioForm.rol_id"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Sin cambiar —</option>
<template x-for="r in roles" :key="r.id">
<option :value="r.id" x-text="r.nombre"></option>
</template>
</select>
</div>
<div x-show="editUsuarioError" class="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="editUsuarioError"></div>
<div x-show="editUsuarioSuccess" class="p-3 bg-green-50 border border-green-200 rounded text-sm text-green-700">✓ Usuario actualizado correctamente.</div>
</div>
<div class="flex justify-end gap-3 px-5 pb-5">
<button @click="editUsuarioModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cancelar</button>
<button @click="saveEditUsuario()" :disabled="saving"
class="px-4 py-2 text-sm text-white rounded-lg disabled:opacity-50"
style="background-color:#8eb02f"
onmouseover="if(!this.disabled)this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
<span x-text="saving ? 'Guardando...' : 'Guardar'"></span>
</button>
</div>
</div>
</div>
<!-- ─── Modal: Ver VCard ─────────────────────────────────────────────── -->
<div x-show="verVcardModal" 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-2xl mx-4 max-h-[90vh] flex flex-col">
<!-- Header -->
<div class="flex justify-between items-start p-5 border-b shrink-0">
<div>
<h2 class="text-lg font-semibold" x-text="verVcardData ? (verVcardData.nombre || '—') : 'Cargando...'"></h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="verVcardData ? (verVcardData.cargo || '') : ''"></p>
<div class="flex flex-wrap gap-2 mt-1">
<template x-if="verVcardData">
<span :class="verVcardData.estado ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verVcardData.estado ? 'Activa' : 'Inactiva'"></span>
</template>
<template x-if="verVcardData && verVcardData.es_borrador">
<span class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">Borrador</span>
</template>
<template x-if="verVcardData && verVcardData.privacidad">
<span :class="verVcardData.privacidad === 'publica' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verVcardData.privacidad"></span>
</template>
</div>
</div>
<button @click="verVcardModal = false" class="text-gray-400 hover:text-gray-600 text-xl shrink-0 ml-3"></button>
</div>
<!-- Tabs -->
<div class="border-b px-5 flex gap-1 overflow-x-auto shrink-0">
<template x-for="vt in ['info','redes','apariencia']" :key="vt">
<button @click="verVcardTab = vt"
:class="verVcardTab === vt ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
class="text-xs py-2 px-3 whitespace-nowrap capitalize"
x-text="vt === 'info' ? 'Información' : vt.charAt(0).toUpperCase()+vt.slice(1)"></button>
</template>
</div>
<!-- Content -->
<div class="overflow-y-auto flex-1 p-5">
<!-- Tab Información -->
<template x-if="verVcardTab === 'info' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Nombre</span><span class="font-medium" x-text="verVcardData.nombre || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Cargo</span><span x-text="verVcardData.cargo || '—'"></span></div>
<div class="sm:col-span-2"><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Descripción</span><p class="text-sm text-gray-600" x-text="verVcardData.descripcion || '—'"></p></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Empresa</span><span x-text="verVcardData.empresa || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Email</span><span x-text="verVcardData.email || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Teléfono</span>
<span x-text="(verVcardData.consecutivo_telefono||'') + ' ' + (verVcardData.telefono||'—')"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">WhatsApp</span>
<span x-text="(verVcardData.consecutivo_whatsapp||'') + ' ' + (verVcardData.whatsapp||'—')"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Dirección</span><span x-text="verVcardData.direccion || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Website</span>
<a :href="verVcardData.website" target="_blank" x-show="verVcardData.website"
class="text-blue-600 hover:underline text-xs" x-text="verVcardData.website"></a>
<span x-show="!verVcardData.website" class="text-gray-400"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">País</span><span x-text="verVcardData.pais || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Ciudad</span><span x-text="verVcardData.ciudad || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Usuario</span>
<span x-text="(verVcardData.user && verVcardData.user.name) ? verVcardData.user.name : '—'"></span><br>
<span class="text-xs text-gray-400" x-text="(verVcardData.user && verVcardData.user.email) ? verVcardData.user.email : ''"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Creada</span>
<span class="text-xs" x-text="verVcardData.created_at ? verVcardData.created_at.substring(0,10) : '—'"></span></div>
<div x-show="verVcardData.foto_perfil">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1">Foto perfil</span>
<img :src="verVcardData.foto_perfil" class="h-14 w-14 rounded-full object-cover border" onerror="this.style.display='none'" />
</div>
<div x-show="verVcardData.logo">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1">Logo</span>
<img :src="verVcardData.logo" class="h-14 rounded border object-contain" onerror="this.style.display='none'" />
</div>
<div class="sm:col-span-2"><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">ID</span>
<span class="text-xs font-mono text-gray-400" x-text="verVcardData._id || verVcardData.id || '—'"></span></div>
</div>
</template>
<!-- Tab Redes -->
<template x-if="verVcardTab === 'redes' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<template x-for="red in [
{key:'linkedin',label:'LinkedIn'},{key:'twitter',label:'Twitter / X'},
{key:'facebook',label:'Facebook'},{key:'instagram',label:'Instagram'},
{key:'tiktok',label:'TikTok'},{key:'github',label:'GitHub'},
{key:'behance',label:'Behance'},{key:'youtube',label:'YouTube'},
{key:'youtube_channel',label:'Canal YouTube'},{key:'telegram',label:'Telegram'},
{key:'messenger',label:'Messenger'},{key:'wechat',label:'WeChat'},
{key:'signal',label:'Signal'},{key:'line',label:'Line'}
]" :key="red.key">
<div x-show="verVcardData[red.key]">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5" x-text="red.label"></span>
<span class="text-sm" x-text="verVcardData[red.key]"></span>
</div>
</template>
<div x-show="!['linkedin','twitter','facebook','instagram','tiktok','github','behance','youtube','youtube_channel','telegram','messenger','wechat','signal','line'].some(k => verVcardData[k])"
class="sm:col-span-2 text-sm text-gray-400">Sin redes sociales configuradas.</div>
</div>
</template>
<!-- Tab Apariencia -->
<template x-if="verVcardTab === 'apariencia' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<template x-for="c in [
{key:'cabecera',label:'Cabecera'},{key:'fondo',label:'Fondo'},
{key:'color_cuerpo',label:'Cuerpo'},{key:'color_iconos',label:'Iconos'},
{key:'color_info',label:'Info'}
]" :key="c.key">
<div x-show="verVcardData[c.key]">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1" x-text="c.label"></span>
<div class="flex items-center gap-2">
<div class="h-6 w-6 rounded border border-gray-200 shrink-0" :style="'background:' + verVcardData[c.key]"></div>
<span class="font-mono text-xs" x-text="verVcardData[c.key]"></span>
</div>
</div>
</template>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Fuente</span><span x-text="verVcardData.fuente || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Bordes</span><span x-text="verVcardData.bordes || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Estilo</span><span x-text="verVcardData.estilo_vcard || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Orden iconos</span><span x-text="verVcardData.orden_iconos || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Animación</span><span x-text="verVcardData.animacion || 'Sin animación'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Botón</span><span x-text="verVcardData.boton || '—'"></span></div>
</div>
</template>
</div>
<!-- Footer -->
<div class="flex justify-between items-center gap-3 px-5 py-4 border-t shrink-0">
<div class="flex gap-2">
<a :href="verVcardData ? (verVcardData.url || (verVcardData.unico ? 'https://vcard.u-site.app/' + verVcardData.unico : '#')) : '#'"
target="_blank"
class="inline-flex items-center gap-1 px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#3b82f6"
onmouseover="this.style.backgroundColor='#2563eb'"
onmouseout="this.style.backgroundColor='#3b82f6'">
🔗 Ver VCard
</a>
<button @click="openEditVcardModal(verVcardData); verVcardModal = false"
class="px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">✏ Editar</button>
</div>
<button @click="verVcardModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cerrar</button>
</div>
</div>
</div>
<!-- ─── Modal: Editar VCard ───────────────────────────────────────────── -->
<div x-show="editVcardModal" 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-2xl mx-4 max-h-[90vh] flex flex-col">
<div class="flex justify-between items-center p-5 border-b shrink-0">
<div>
<h2 class="text-lg font-semibold">Editar VCard</h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="editVcardData ? (editVcardData.nombre || '') : ''"></p>
</div>
<button @click="editVcardModal = false" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<!-- Tabs del modal -->
<div class="border-b px-5 flex gap-1 shrink-0 overflow-x-auto">
<template x-for="vt in ['datos','redes','apariencia']" :key="vt">
<button @click="editVcardTab = vt"
:class="editVcardTab === vt ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
class="text-xs py-2 px-3 whitespace-nowrap capitalize" x-text="vt.charAt(0).toUpperCase()+vt.slice(1)"></button>
</template>
</div>
<div class="overflow-y-auto flex-1 p-5">
<!-- Tab Datos -->
<div x-show="editVcardTab === 'datos'" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Nombre</label>
<input x-model="editVcardForm.nombre" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Cargo</label>
<input x-model="editVcardForm.cargo" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Descripción</label>
<textarea x-model="editVcardForm.descripcion" rows="2" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Teléfono</label>
<div class="flex gap-1">
<input x-model="editVcardForm.consecutivo_telefono" type="text" placeholder="+57" class="w-16 border border-gray-300 rounded-lg px-2 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="editVcardForm.telefono" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">WhatsApp</label>
<div class="flex gap-1">
<input x-model="editVcardForm.consecutivo_whatsapp" type="text" placeholder="+57" class="w-16 border border-gray-300 rounded-lg px-2 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="editVcardForm.whatsapp" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Email</label>
<input x-model="editVcardForm.email" type="email" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Empresa</label>
<input x-model="editVcardForm.empresa" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Dirección</label>
<input x-model="editVcardForm.direccion" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Website</label>
<input x-model="editVcardForm.website" type="url" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">País</label>
<input x-model="editVcardForm.pais" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Ciudad</label>
<input x-model="editVcardForm.ciudad" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Foto perfil (URL)</label>
<input x-model="editVcardForm.foto_perfil" type="url" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Logo (URL)</label>
<input x-model="editVcardForm.logo" type="url" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Privacidad</label>
<select x-model="editVcardForm.privacidad" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="publica">Pública</option>
<option value="privada">Privada</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Texto botón</label>
<input x-model="editVcardForm.boton" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div class="flex items-center gap-4 sm:col-span-2 pt-1">
<label class="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
<input x-model="editVcardForm.estado" type="checkbox" class="rounded" /> Activa
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
<input x-model="editVcardForm.es_borrador" type="checkbox" class="rounded" /> Borrador
</label>
</div>
</div>
<!-- Tab Redes Sociales -->
<div x-show="editVcardTab === 'redes'" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<template x-for="red in [
{key:'linkedin', label:'LinkedIn'},
{key:'twitter', label:'Twitter / X'},
{key:'facebook', label:'Facebook'},
{key:'instagram', label:'Instagram'},
{key:'tiktok', label:'TikTok'},
{key:'github', label:'GitHub'},
{key:'behance', label:'Behance'},
{key:'youtube', label:'YouTube'},
{key:'youtube_channel', label:'Canal YouTube'},
{key:'telegram', label:'Telegram'},
{key:'messenger', label:'Messenger'},
{key:'wechat', label:'WeChat'},
{key:'signal', label:'Signal'},
{key:'line', label:'Line'}
]" :key="red.key">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1" x-text="red.label"></label>
<div class="flex gap-1" x-show="red.key === 'signal'">
<input x-model="editVcardForm.consecutivo_signal" type="text" placeholder="+57" class="w-16 border border-gray-300 rounded-lg px-2 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<input x-model="editVcardForm[red.key]" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<input x-show="red.key !== 'signal'" x-model="editVcardForm[red.key]" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</template>
</div>
<!-- Tab Apariencia -->
<div x-show="editVcardTab === 'apariencia'" class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Color cabecera</label>
<div class="flex gap-2 items-center">
<input x-model="editVcardForm.cabecera" type="color" class="h-9 w-12 border border-gray-300 rounded cursor-pointer" />
<input x-model="editVcardForm.cabecera" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Color fondo</label>
<div class="flex gap-2 items-center">
<input x-model="editVcardForm.fondo" type="color" class="h-9 w-12 border border-gray-300 rounded cursor-pointer" />
<input x-model="editVcardForm.fondo" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Color cuerpo</label>
<div class="flex gap-2 items-center">
<input x-model="editVcardForm.color_cuerpo" type="color" class="h-9 w-12 border border-gray-300 rounded cursor-pointer" />
<input x-model="editVcardForm.color_cuerpo" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Color iconos</label>
<div class="flex gap-2 items-center">
<input x-model="editVcardForm.color_iconos" type="color" class="h-9 w-12 border border-gray-300 rounded cursor-pointer" />
<input x-model="editVcardForm.color_iconos" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Color info</label>
<div class="flex gap-2 items-center">
<input x-model="editVcardForm.color_info" type="color" class="h-9 w-12 border border-gray-300 rounded cursor-pointer" />
<input x-model="editVcardForm.color_info" type="text" class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Fuente</label>
<select x-model="editVcardForm.fuente" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="Inter">Inter</option>
<option value="Roboto">Roboto</option>
<option value="Montserrat">Montserrat</option>
<option value="Poppins">Poppins</option>
<option value="Lato">Lato</option>
<option value="Open Sans">Open Sans</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Bordes</label>
<select x-model="editVcardForm.bordes" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="rounded-full">Circular</option>
<option value="rounded-lg">Redondeado</option>
<option value="rounded-none">Recto</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Estilo VCard</label>
<select x-model="editVcardForm.estilo_vcard" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option :value="1">Estilo 1</option>
<option :value="2">Estilo 2</option>
<option :value="3">Estilo 3</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Orden iconos</label>
<select x-model="editVcardForm.orden_iconos" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="top">Arriba</option>
<option value="bottom">Abajo</option>
<option value="left">Izquierda</option>
<option value="right">Derecha</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Animación</label>
<select x-model="editVcardForm.animacion" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">Sin animación</option>
<option value="fade">Fade</option>
<option value="slide">Slide</option>
<option value="zoom">Zoom</option>
<option value="bounce">Bounce</option>
<option value="flip">Flip</option>
</select>
</div>
</div>
<div x-show="editVcardError" class="mt-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="editVcardError"></div>
<div x-show="editVcardSuccess" class="mt-4 p-3 bg-green-50 border border-green-200 rounded text-sm text-green-700">✓ VCard actualizada correctamente.</div>
</div>
<div class="flex justify-end gap-3 px-5 py-4 border-t shrink-0">
<button @click="editVcardModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cancelar</button>
<button @click="saveEditVcard()" :disabled="saving"
class="px-4 py-2 text-sm text-white rounded-lg disabled:opacity-50"
style="background-color:#8eb02f"
onmouseover="if(!this.disabled)this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
<span x-text="saving ? 'Guardando...' : 'Guardar'"></span>
</button>
</div>
</div>
</div>
<!-- ─── Modal: Desactivar Usuario + VCards ───────────────────────────── -->
<div x-show="desactivarVcardsModal" 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">
<div>
<h3 class="text-base font-semibold text-orange-700">Inactivar VCards del usuario</h3>
<p class="text-xs text-gray-500 mt-0.5" x-text="desactivarVcardsUser ? (desactivarVcardsUser.name || desactivarVcardsUser.email) : ''"></p>
</div>
<button @click="desactivarVcardsModal = false" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<div class="p-5 space-y-4">
<!-- Cargando vcards -->
<div x-show="desactivarVcardsLoading" class="flex items-center gap-2 text-sm text-gray-500">
<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Cargando VCards del usuario...
</div>
<!-- Resumen de vcards encontradas -->
<div x-show="!desactivarVcardsLoading && desactivarVcardsItems.length > 0" class="p-3 bg-orange-50 border border-orange-200 rounded-lg text-sm">
<p class="font-medium text-orange-800 mb-2">
Se inactivarán <span class="font-bold" x-text="desactivarVcardsItems.length"></span> VCard(s):
</p>
<ul class="space-y-1 max-h-40 overflow-y-auto">
<template x-for="v in desactivarVcardsItems" :key="v.id || v._id">
<li class="flex items-center gap-2 text-xs text-orange-700">
<span class="text-orange-400"></span>
<span class="font-medium" x-text="v.nombre || v.name || '—'"></span>
<span class="text-orange-500 font-mono" x-text="v.unico ? '/' + v.unico : ''"></span>
<span :class="v.estado ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'"
class="ml-auto px-1.5 py-0.5 rounded-full text-xs"
x-text="v.estado ? 'Activa' : 'Inactiva'"></span>
</li>
</template>
</ul>
</div>
<!-- Sin vcards -->
<div x-show="!desactivarVcardsLoading && desactivarVcardsItems.length === 0" class="p-3 bg-gray-50 border border-gray-200 rounded-lg text-sm text-gray-500">
Este usuario no tiene VCards activas para inactivar.
</div>
<p class="text-sm text-gray-700">¿Confirmas inactivar todas las VCards de este usuario? <span class="text-orange-600 font-medium">El usuario permanecerá activo.</span></p>
<!-- Error -->
<div x-show="desactivarVcardsError" class="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="desactivarVcardsError"></div>
<!-- Resultado -->
<div x-show="desactivarVcardsResult" class="p-3 bg-green-50 border border-green-200 rounded text-sm text-green-700" x-text="desactivarVcardsResult"></div>
</div>
<div class="flex justify-end gap-3 px-5 pb-5">
<button @click="desactivarVcardsModal = false" class="text-sm px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">Cancelar</button>
<button @click="confirmarDesactivarConVcards()" :disabled="saving || desactivarVcardsLoading"
class="text-sm px-4 py-2 rounded-lg text-white bg-orange-500 hover:bg-orange-600 disabled:opacity-50 flex items-center gap-2">
<svg x-show="saving" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span x-text="saving ? 'Inactivando...' : 'Inactivar VCards'"></span>
</button>
</div>
</div>
</div>
<!-- ─── Modal: Membresía ──────────────────────────────────────────────── -->
<div x-show="membresiaModal" 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">
<div>
<h2 class="text-lg font-semibold">Gestionar Membresía</h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="membresiaUser ? (membresiaUser.name || membresiaUser.email) : ''"></p>
</div>
<button @click="membresiaModal = false" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<div class="p-5 space-y-4">
<!-- Info membresía actual -->
<div x-show="membresiaInfo" class="p-3 bg-blue-50 border border-blue-200 rounded-lg text-xs space-y-1">
<div class="flex justify-between"><span class="font-medium">Plan:</span><span x-text="membresiaInfo && membresiaInfo.plan ? membresiaInfo.plan.nombre : '—'"></span></div>
<div class="flex justify-between"><span class="font-medium">Vence:</span><span x-text="membresiaInfo && membresiaInfo.fecha_fin ? membresiaInfo.fecha_fin.substring(0,10) : '—'"></span></div>
<div class="flex justify-between"><span class="font-medium">Estado:</span>
<span :class="membresiaInfo && membresiaInfo.activo ? 'text-green-600' : 'text-red-500'"
x-text="membresiaInfo && membresiaInfo.activo ? 'Activa' : 'Inactiva'"></span>
</div>
</div>
<!-- Activar membresía -->
<div class="border rounded-lg p-3 space-y-2">
<p class="text-sm font-medium">Activar / Renovar</p>
<div class="flex items-center gap-2">
<input x-model="membresiaForm.anual" type="checkbox" id="mAnual" class="rounded" />
<label for="mAnual" class="text-sm text-gray-700">Anual (+365 días)</label>
</div>
<div x-show="!membresiaForm.anual">
<input x-model.number="membresiaForm.dias" type="number" min="1" placeholder="Días a agregar"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<button @click="activarMembresia()" :disabled="saving"
class="w-full px-4 py-2 text-sm text-white rounded-lg disabled:opacity-50"
style="background-color:#8eb02f">
<span x-text="saving ? 'Procesando...' : 'Activar / Renovar'"></span>
</button>
</div>
<!-- Cambiar plan -->
<div class="border rounded-lg p-3 space-y-2">
<p class="text-sm font-medium">Cambiar Plan</p>
<select x-model="membresiaForm.plan_id"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">— Seleccionar plan —</option>
<template x-for="pl in planes" :key="pl.id || pl._id">
<option :value="pl.id || pl._id" x-text="pl.nombre"></option>
</template>
</select>
<button @click="cambiarPlan()" :disabled="saving || !membresiaForm.plan_id"
class="w-full px-4 py-2 text-sm border border-blue-300 text-blue-600 rounded-lg hover:bg-blue-50 disabled:opacity-50">
<span x-text="saving ? 'Procesando...' : 'Cambiar Plan'"></span>
</button>
</div>
<!-- Desactivar -->
<div x-show="membresiaInfo && membresiaInfo.activo">
<button @click="desactivarMembresia()" :disabled="saving"
class="w-full px-4 py-2 text-sm border border-red-200 text-red-500 rounded-lg hover:bg-red-50 disabled:opacity-50">
Desactivar membresía
</button>
</div>
<div x-show="membresiaError" class="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="membresiaError"></div>
</div>
</div>
</div>
<!-- ─── Modal: Ver Plan ──────────────────────────────────────────────────── -->
<div x-show="verPlanModal" 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-xl shadow-2xl w-full max-w-2xl mx-4 flex flex-col max-h-[90vh]">
<div class="flex justify-between items-center px-5 py-4 border-b shrink-0">
<div class="flex items-center gap-3">
<img x-show="verPlanData && verPlanData.img" :src="verPlanData && verPlanData.img ? 'https://vcard.u-site.app/' + verPlanData.img : null"
class="h-10 w-10 rounded-lg object-cover border" onerror="this.style.display='none'" />
<div>
<h2 class="text-lg font-semibold" x-text="verPlanData ? verPlanData.nombre : ''"></h2>
<span x-show="verPlanData"
:class="verPlanData && verPlanData.estado ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
class="text-xs px-2 py-0.5 rounded-full"
x-text="verPlanData && verPlanData.estado ? 'Activo' : 'Inactivo'"></span>
</div>
</div>
<button @click="verPlanModal = false" class="text-gray-400 hover:text-gray-600 text-xl shrink-0"></button>
</div>
<div class="overflow-y-auto p-5 space-y-5 flex-1" x-show="verPlanData">
<!-- Info general -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<p class="text-xs font-medium text-gray-500 uppercase mb-1">VCards permitidas</p>
<p x-text="verPlanData && verPlanData.cantidad || '—'"></p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 uppercase mb-1">ID</p>
<p class="font-mono text-xs truncate" x-text="verPlanData && (verPlanData._id || verPlanData.id) || '—'"></p>
</div>
<div x-show="verPlanData && verPlanData.plan_id_anual">
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Plan anual (ID externo)</p>
<p class="font-mono text-xs" x-text="verPlanData && verPlanData.plan_id_anual || '—'"></p>
</div>
<div x-show="verPlanData && verPlanData.plan_id_mensual">
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Plan mensual (ID externo)</p>
<p class="font-mono text-xs" x-text="verPlanData && verPlanData.plan_id_mensual || '—'"></p>
</div>
<div x-show="verPlanData && verPlanData.plan_anual_paypal">
<p class="text-xs font-medium text-gray-500 uppercase mb-1">PayPal anual</p>
<p class="font-mono text-xs" x-text="verPlanData && verPlanData.plan_anual_paypal || '—'"></p>
</div>
<div x-show="verPlanData && verPlanData.plan_mensual_paypal">
<p class="text-xs font-medium text-gray-500 uppercase mb-1">PayPal mensual</p>
<p class="font-mono text-xs" x-text="verPlanData && verPlanData.plan_mensual_paypal || '—'"></p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Creado</p>
<p class="text-xs" x-text="verPlanData && verPlanData.created_at ? verPlanData.created_at.substring(0,10) : '—'"></p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Actualizado</p>
<p class="text-xs" x-text="verPlanData && verPlanData.updated_at ? verPlanData.updated_at.substring(0,10) : '—'"></p>
</div>
</div>
<!-- Descripción -->
<div>
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Descripción</p>
<p class="text-sm text-gray-700" x-text="verPlanData && verPlanData.descripcion || '—'"></p>
</div>
<!-- Características (HTML) -->
<div x-show="verPlanData && verPlanData.caracteristicas">
<p class="text-xs font-medium text-gray-500 uppercase mb-1">Características</p>
<ul class="list-disc list-inside text-sm text-gray-700 space-y-0.5"
x-html="verPlanData && verPlanData.caracteristicas || ''"></ul>
</div>
<!-- Tarifas por país -->
<div x-show="verPlanData && verPlanData.tarifas && verPlanData.tarifas.length">
<p class="text-xs font-medium text-gray-500 uppercase mb-2">Tarifas por país</p>
<div class="overflow-x-auto border rounded-lg">
<table class="w-full text-xs">
<thead class="bg-gray-50 border-b">
<tr>
<th class="py-2 px-3 text-left font-semibold text-gray-500">#</th>
<th class="py-2 px-3 text-left font-semibold text-gray-500">País ID</th>
<th class="py-2 px-3 text-left font-semibold text-gray-500">Monto</th>
<th class="py-2 px-3 text-left font-semibold text-gray-500">Actualizado</th>
</tr>
</thead>
<tbody>
<template x-for="(t, i) in (verPlanData ? verPlanData.tarifas : [])" :key="t._id">
<tr class="border-b last:border-0 hover:bg-gray-50">
<td class="py-1.5 px-3 text-gray-400" x-text="i+1"></td>
<td class="py-1.5 px-3 font-mono text-gray-500 truncate max-w-[120px]" x-text="t.pais_id || '—'"></td>
<td class="py-1.5 px-3 font-medium">
<span :class="t.monto === 'Gratis' || t.monto === '0' ? 'text-green-600' : 'text-gray-800'"
x-text="t.monto === 'Gratis' || t.monto === '0' ? 'Gratis' : ('$' + t.monto)"></span>
</td>
<td class="py-1.5 px-3 text-gray-400" x-text="t.updated_at ? t.updated_at.substring(0,10) : '—'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex justify-end gap-3 px-5 py-4 border-t shrink-0">
<button @click="verPlanModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cerrar</button>
<button @click="verPlanModal = false; openEditPlanModal(verPlanData)"
class="px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
Editar
</button>
</div>
</div>
</div>
<!-- ─── Modal: Editar Plan ────────────────────────────────────────────────── -->
<div x-show="editPlanModal" 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-xl shadow-2xl w-full max-w-xl mx-4 flex flex-col max-h-[90vh]">
<div class="flex justify-between items-center px-5 py-4 border-b shrink-0">
<h2 class="text-lg font-semibold">Editar Plan</h2>
<button @click="editPlanModal = false" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<div class="overflow-y-auto p-5 flex-1">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Nombre del plan</label>
<input x-model="editPlanForm.nombre" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Descripción</label>
<textarea x-model="editPlanForm.descripcion" rows="2"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"></textarea>
</div>
<div class="sm:col-span-2">
<label class="block text-xs font-medium text-gray-700 mb-1">Características (HTML)</label>
<textarea x-model="editPlanForm.caracteristicas" rows="4"
placeholder="<li>1 vCard</li>&#10;<li>Información personal</li>"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">VCards permitidas</label>
<input x-model="editPlanForm.cantidad" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Imagen (ruta storage)</label>
<input x-model="editPlanForm.img" type="text" placeholder="storage/archivo.png"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Plan ID anual (externo)</label>
<input x-model="editPlanForm.plan_id_anual" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Plan ID mensual (externo)</label>
<input x-model="editPlanForm.plan_id_mensual" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">PayPal plan anual</label>
<input x-model="editPlanForm.plan_anual_paypal" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">PayPal plan mensual</label>
<input x-model="editPlanForm.plan_mensual_paypal" type="text"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
</div>
<div class="sm:col-span-2 flex items-center gap-2 pt-1">
<input x-model="editPlanForm.estado" type="checkbox" id="planEstado" class="rounded" />
<label for="planEstado" class="text-sm text-gray-700 cursor-pointer">Plan activo</label>
</div>
</div>
<!-- Tarifas (sólo lectura) -->
<div x-show="editPlanData && editPlanData.tarifas && editPlanData.tarifas.length" class="mt-5">
<p class="text-xs font-medium text-gray-500 uppercase mb-2">Tarifas por país (sólo lectura)</p>
<div class="max-h-40 overflow-y-auto border rounded-lg">
<table class="w-full text-xs">
<thead class="bg-gray-50 border-b sticky top-0">
<tr>
<th class="py-1.5 px-3 text-left text-gray-500">País ID</th>
<th class="py-1.5 px-3 text-left text-gray-500">Monto</th>
</tr>
</thead>
<tbody>
<template x-for="t in (editPlanData ? editPlanData.tarifas : [])" :key="t._id">
<tr class="border-b last:border-0">
<td class="py-1 px-3 font-mono text-gray-500 truncate max-w-[140px]" x-text="t.pais_id"></td>
<td class="py-1 px-3 font-medium"
:class="t.monto === 'Gratis' || t.monto === '0' ? 'text-green-600' : ''"
x-text="t.monto === 'Gratis' || t.monto === '0' ? 'Gratis' : ('$' + t.monto)"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div x-show="editPlanError" class="mt-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="editPlanError"></div>
<div x-show="editPlanSuccess" class="mt-4 p-3 bg-green-50 border border-green-200 rounded text-sm text-green-700">✓ Plan actualizado correctamente.</div>
</div>
<div class="flex justify-end gap-3 px-5 py-4 border-t shrink-0">
<button @click="editPlanModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cancelar</button>
<button @click="saveEditPlan()" :disabled="saving"
class="px-4 py-2 text-sm text-white rounded-lg disabled:opacity-50"
style="background-color:#8eb02f"
onmouseover="if(!this.disabled)this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">
<span x-text="saving ? 'Guardando...' : 'Guardar'"></span>
</button>
</div>
</div>
</div>
</div>
<script>
function vcardApiApp() {
return {
tab: 'usuarios',
items: [],
total: 0,
page: 1,
perPage: 20,
search: '',
loading: false,
saving: false,
errorMsg: '',
hasConfig: false,
configData: null,
configModal: false,
configError: '',
form: { nombre: '', base_url: '', bearer_token: '', activo: true },
loginForm: { email: '', password: '' },
loginLoading: false,
loginMsg: '',
loginOk: false,
membresiaModal: false,
membresiaUser: null,
membresiaInfo: null,
membresiaForm: { anual: true, dias: 30, plan_id: '' },
membresiaError: '',
planes: [],
// Ordenar usuarios por vencimiento
sortByVenc: false,
loadingVenc: false,
filterPlan: '',
knownPlans: [], // planes acumulados de todas las páginas cargadas
membresiaMap: {}, // { userId: { fecha_fin, activo, plan } }
// Inactivar VCards del usuario (sin desactivar usuario)
desactivarVcardsModal: false,
desactivarVcardsUser: null,
desactivarVcardsItems: [],
desactivarVcardsLoading: false,
desactivarVcardsError: '',
desactivarVcardsResult: '',
// Cache acumulado de usuarios de todas las páginas (para filtro de planes)
allUsersCache: {},
// Filtros por tab
filters: {
pagos: { estado: '', moneda: '', desde: '', hasta: '' },
transacciones: { type: '', status: '' },
logs: { type: '', desde: '', hasta: '' },
},
// Modal editar usuario
editUsuarioModal: false,
editUsuarioData: null,
editUsuarioForm: { name: '', email: '', rol_id: '', plan_id: '' },
editUsuarioError: '',
editUsuarioSuccess: false,
// Modal ver vcard (detalle completo)
verVcardModal: false,
verVcardData: null,
verVcardTab: 'info',
// Modal editar vcard
editVcardModal: false,
editVcardTab: 'datos',
editVcardData: null,
editVcardForm: {
nombre: '', cargo: '', descripcion: '', telefono: '', consecutivo_telefono: '',
whatsapp: '', consecutivo_whatsapp: '', email: '', empresa: '', direccion: '',
website: '', pais: '', ciudad: '', foto_perfil: '', logo: '',
privacidad: 'publica', boton: '', estado: true, es_borrador: false,
linkedin: '', twitter: '', facebook: '', instagram: '', tiktok: '',
github: '', behance: '', youtube: '', youtube_channel: '',
telegram: '', messenger: '', wechat: '', signal: '', consecutivo_signal: '', line: '',
cabecera: '#E8E8E8', fondo: '#2B2D35', color_cuerpo: '#FFFFFF',
color_iconos: '#2B2D35', color_info: '#2B2D35',
fuente: 'Inter', bordes: 'rounded-full', estilo_vcard: 1,
orden_iconos: 'top', animacion: '',
},
editVcardError: '',
editVcardSuccess: false,
// Modal ver plan
verPlanModal: false,
verPlanData: null,
// Modal editar plan
editPlanModal: false,
editPlanData: null,
editPlanForm: {
nombre: '', descripcion: '', caracteristicas: '', cantidad: '', img: '',
estado: true, plan_id_anual: '', plan_id_mensual: '',
plan_anual_paypal: '', plan_mensual_paypal: '',
},
editPlanError: '',
editPlanSuccess: false,
// Modal ver usuario (detalle completo)
verModal: false,
verData: null,
verTab: 'general',
verLoading: false,
verError: '',
verSectionItems: [],
verSectionLoading: false,
// Roles conocidos (seed + acumulados de usuarios)
roles: [
{ id: '63c48dd0f0117680cf0cdb79', nombre: 'administrador' },
{ id: '63c48ddbf0117680cf0cdb7a', nombre: 'cliente' },
],
get totalPages() {
return Math.max(1, Math.ceil(this.total / this.perPage));
},
async init() {
await this.loadConfig();
if (this.hasConfig) this.loadTab();
this.loadPlanes();
},
closeModals() {
this.configModal = false;
this.membresiaModal = false;
this.editUsuarioModal = false;
this.editVcardModal = false;
this.verModal = false;
this.verVcardModal = false;
this.verPlanModal = false;
this.editPlanModal = false;
this.desactivarVcardsModal = false;
},
async loadConfig() {
try {
const r = await fetch('/app/vcard-api/config');
const d = await r.json();
if (d.config) {
this.configData = d.config;
this.hasConfig = !!d.config.activo;
this.form.nombre = d.config.nombre || '';
this.form.base_url = d.config.base_url || '';
this.form.activo = d.config.activo ?? true;
}
} catch(e) {}
},
async obtenerToken() {
this.loginMsg = '';
if (!this.form.base_url.trim()) { this.loginMsg = 'Ingresa primero la URL Base.'; this.loginOk = false; return; }
if (!this.loginForm.email || !this.loginForm.password) { this.loginMsg = 'Email y contraseña son requeridos.'; this.loginOk = false; return; }
this.loginLoading = true;
try {
const r = await fetch('/app/vcard-api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ base_url: this.form.base_url, email: this.loginForm.email, password: this.loginForm.password })
});
const d = await r.json();
if (d.ok && d.token) {
this.form.bearer_token = d.token;
this.loginMsg = '✓ Token obtenido. Haz clic en Guardar para aplicar.';
this.loginOk = true;
} else {
this.loginMsg = d.error || 'Error al obtener el token.';
this.loginOk = false;
}
} catch(e) { this.loginMsg = e.message; this.loginOk = false; }
this.loginLoading = false;
},
async saveConfig() {
this.configError = '';
if (!this.form.base_url.trim()) { this.configError = 'La URL base es requerida.'; return; }
this.saving = true;
try {
const r = await fetch('/app/vcard-api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
});
const d = await r.json();
if (d.ok) {
this.configModal = false;
await this.loadConfig();
if (this.hasConfig) this.loadTab();
} else {
this.configError = d.error || 'Error al guardar.';
}
} catch(e) { this.configError = e.message; }
this.saving = false;
},
setTab(t) {
this.tab = t;
this.page = 1;
this.items = [];
this.total = 0;
this.errorMsg = '';
this.filterPlan = '';
this.knownPlans = [];
this.allUsersCache = {};
if (this.hasConfig) this.loadTab();
},
buildEndpoint() {
const qs = (p) => Object.entries(p).filter(([,v]) => v !== '').map(([k,v]) => `${k}=${encodeURIComponent(v)}`).join('&');
const base = {
usuarios: `/app/vcard-api/usuarios?search=${encodeURIComponent(this.search)}&per_page=${this.perPage}&page=${this.page}`,
vcards: `/app/vcard-api/vcards?search=${encodeURIComponent(this.search)}&per_page=${this.perPage}&page=${this.page}`,
planes: `/app/vcard-api/planes`,
pagos: `/app/vcard-api/pagos?per_page=${this.perPage}&page=${this.page}`,
transacciones: `/app/vcard-api/transacciones?per_page=${this.perPage}&page=${this.page}`,
logs: `/app/vcard-api/logs?search=${encodeURIComponent(this.search)}&per_page=${this.perPage}&page=${this.page}`,
miniwebs: `/app/vcard-api/miniwebs?search=${encodeURIComponent(this.search)}&per_page=${this.perPage}&page=${this.page}`,
};
let url = base[this.tab] || '';
if (this.tab === 'pagos') {
const f = qs(this.filters.pagos);
if (f) url += '&' + f;
} else if (this.tab === 'transacciones') {
const f = qs(this.filters.transacciones);
if (f) url += '&' + f;
} else if (this.tab === 'logs') {
const f = qs(this.filters.logs);
if (f) url += '&' + f;
}
return url;
},
async loadTab() {
if (!this.hasConfig) return;
this.loading = true;
this.errorMsg = '';
try {
const r = await fetch(this.buildEndpoint());
const d = await r.json();
if (!r.ok) { this.errorMsg = d.error || `Error ${r.status}`; this.loading = false; return; }
if (d.data && Array.isArray(d.data)) {
this.items = d.data;
this.total = d.total || d.data.length;
if (this.tab === 'usuarios') { this._collectRoles(d.data); this._collectPlanes(d.data); }
} else if (Array.isArray(d)) {
this.items = d;
this.total = d.length;
if (this.tab === 'usuarios') { this._collectRoles(d); this._collectPlanes(d); }
} else {
this.items = [];
this.total = 0;
this.errorMsg = 'Respuesta inesperada de la API.';
}
} catch(e) { this.errorMsg = e.message; }
this.loading = false;
// Si el sort por vencimiento estaba activo, re-aplicarlo con los nuevos items
if (this.sortByVenc && this.tab === 'usuarios' && this.items.length > 0) {
await this._fetchAndSortVenc();
}
},
_collectRoles(users) {
users.forEach(u => {
if (u.rol_id && u.rol && u.rol.nombre) {
const id = u.rol_id;
if (!this.roles.find(r => r.id === id)) {
this.roles.push({ id, nombre: u.rol.nombre });
}
}
});
},
_collectPlanes(users) {
users.forEach(u => {
// Acumular en cache para filtrado cross-página
const uid = u.id || u._id;
if (uid) this.allUsersCache[uid] = u;
const nombre = (u.plan && u.plan.nombre) ? u.plan.nombre : '';
if (nombre && !this.knownPlans.includes(nombre)) {
this.knownPlans.push(nombre);
this.knownPlans.sort();
}
});
},
// ─── Ordenar por vencimiento ─────────────────────────────────────────
diasHastaVencer(u) {
const id = u.id || u._id;
const m = this.membresiaMap[id];
if (!m || !m.fecha_fin) return null;
const diff = new Date(m.fecha_fin) - new Date();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
},
vencimientoRowClass(u) {
const dias = this.diasHastaVencer(u);
if (dias === null) return 'hover:bg-gray-50';
if (dias <= 7) return 'bg-red-50 hover:bg-red-100';
if (dias <= 30) return 'bg-yellow-50 hover:bg-yellow-100';
return 'bg-green-50 hover:bg-green-100';
},
vencimientoBadgeClass(u) {
const dias = this.diasHastaVencer(u);
if (dias === null) return 'bg-gray-100 text-gray-400';
if (dias <= 0) return 'bg-red-200 text-red-800 font-semibold';
if (dias <= 7) return 'bg-red-100 text-red-700 font-semibold';
if (dias <= 30) return 'bg-yellow-100 text-yellow-700';
return 'bg-green-100 text-green-700';
},
vencimientoTexto(u) {
const id = u.id || u._id;
const m = this.membresiaMap[id];
if (this.loadingVenc && !m) return '...';
if (!m || !m.fecha_fin) return '—';
const dias = this.diasHastaVencer(u);
const fecha = m.fecha_fin.substring(0, 10);
if (dias <= 0) return `Vencido · ${fecha}`;
if (dias === 1) return `Mañana · ${fecha}`;
return `${dias}d · ${fecha}`;
},
async _fetchAndSortVenc() {
this.loadingVenc = true;
const batchSize = 5;
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;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}/membresia`);
if (r.ok) { this.membresiaMap[id] = await r.json(); }
} catch(e) { /* ignorar errores individuales */ }
}));
}
this.loadingVenc = false;
this.items = [...this.items].sort((a, b) => {
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;
});
},
async toggleSortVenc() {
if (this.sortByVenc) {
this.sortByVenc = false;
this.loadTab();
return;
}
this.sortByVenc = true;
await this._fetchAndSortVenc();
},
clearFilters(tab) {
if (tab === 'pagos') this.filters.pagos = { estado: '', moneda: '', desde: '', hasta: '' };
if (tab === 'transacciones') this.filters.transacciones = { type: '', status: '' };
if (tab === 'logs') this.filters.logs = { type: '', desde: '', hasta: '' };
this.page = 1;
this.loadTab();
},
uniqueMontos(tarifas) {
const montos = [...new Set(tarifas.map(t => t.monto || '—'))];
return montos.slice(0, 5); // max 5 precios únicos
},
planesDisponibles() {
return this.knownPlans;
},
usuariosFiltrados() {
if (!this.filterPlan) return this.items;
// Filtrar desde el cache acumulado de TODAS las páginas cargadas
return Object.values(this.allUsersCache)
.filter(u => ((u.plan && u.plan.nombre) || '') === this.filterPlan);
},
// ─── Planes ──────────────────────────────────────────────────────────
openVerPlanModal(p) {
this.verPlanData = p;
this.verPlanModal = true;
},
openEditPlanModal(p) {
this.editPlanData = p;
this.editPlanForm = {
nombre: p.nombre || '',
descripcion: p.descripcion || '',
caracteristicas: p.caracteristicas || '',
cantidad: p.cantidad || '',
img: p.img || '',
estado: p.estado ?? true,
plan_id_anual: p.plan_id_anual || '',
plan_id_mensual: p.plan_id_mensual || '',
plan_anual_paypal: p.plan_anual_paypal || '',
plan_mensual_paypal: p.plan_mensual_paypal || '',
};
this.editPlanError = '';
this.editPlanSuccess = false;
this.editPlanModal = true;
},
async saveEditPlan() {
this.editPlanError = '';
this.editPlanSuccess = false;
const id = this.editPlanData._id || this.editPlanData.id;
const body = { ...this.editPlanForm };
// Limpiar campos vacíos opcionales
['plan_id_anual','plan_id_mensual','plan_anual_paypal','plan_mensual_paypal'].forEach(k => {
if (!body[k]) delete body[k];
});
this.saving = true;
try {
const r = await fetch(`/app/vcard-api/planes/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const d = await r.json();
if (!r.ok) {
this.editPlanError = d.error || d.message || 'Error al guardar el plan.';
} else {
this.editPlanSuccess = true;
// Actualiza la card local
const idx = this.items.findIndex(p => (p._id || p.id) === id);
if (idx > -1) this.items[idx] = { ...this.items[idx], ...body };
this.loadPlanes();
}
} catch(e) {
this.editPlanError = e.message;
}
this.saving = false;
},
async loadPlanes() {
try {
const r = await fetch('/app/vcard-api/planes');
const d = await r.json();
if (Array.isArray(d)) this.planes = d;
else if (d.data) this.planes = d.data;
} catch(e) {}
},
changePage(p) {
if (p < 1 || p > this.totalPages) return;
this.page = p;
this.loadTab();
},
async toggleUsuario(u, accion) {
if (!confirm(`¿${accion === 'activar' ? 'Activar' : 'Desactivar'} este usuario?`)) return;
this.loading = true;
const id = u.id || u._id;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}/${accion}`, { method: 'POST' });
const d = await r.json();
if (!r.ok) { alert(d.error || 'Error al procesar.'); }
else await this.loadTab();
} catch(e) { alert(e.message); }
this.loading = false;
},
async openDesactivarConVcardsModal(u) {
this.desactivarVcardsUser = u;
this.desactivarVcardsItems = [];
this.desactivarVcardsError = '';
this.desactivarVcardsResult = '';
this.desactivarVcardsLoading = true;
this.desactivarVcardsModal = true;
try {
const id = u.id || u._id;
const r = await fetch(`/app/vcard-api/usuarios/${id}/vcards`);
const d = await r.json();
if (d.data && Array.isArray(d.data)) this.desactivarVcardsItems = d.data;
else if (Array.isArray(d)) this.desactivarVcardsItems = d;
} catch(e) { this.desactivarVcardsError = 'No se pudieron cargar las VCards: ' + e.message; }
this.desactivarVcardsLoading = false;
},
async confirmarDesactivarConVcards() {
this.desactivarVcardsError = '';
this.desactivarVcardsResult = '';
this.saving = true;
try {
// Solo inactivar cada VCard activa — no se toca al usuario
let inactivadas = 0;
const vcards = this.desactivarVcardsItems.filter(v => v.estado === true || v.estado === 1);
for (const v of vcards) {
const vid = v.id || v._id;
const r = await fetch(`/app/vcard-api/vcards/${vid}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ estado: false })
});
if (r.ok) inactivadas++;
}
this.desactivarVcardsResult = `VCards inactivadas: ${inactivadas} de ${vcards.length}. El usuario sigue activo.`;
setTimeout(async () => {
this.desactivarVcardsModal = false;
await this.loadTab();
}, 1800);
} catch(e) { this.desactivarVcardsError = e.message; }
this.saving = false;
},
// ─── Edit Usuario ────────────────────────────────────────────────────
openEditUsuarioModal(u) {
this.editUsuarioData = u;
this.editUsuarioForm = {
name: u.name || u.nombre || '',
email: u.email || '',
rol_id: (u.rol && (u.rol.id || u.rol._id)) ? (u.rol.id || u.rol._id) : '',
plan_id: (u.plan && (u.plan.id || u.plan._id)) ? (u.plan.id || u.plan._id) : '',
};
this.editUsuarioError = '';
this.editUsuarioSuccess = false;
this.editUsuarioModal = true;
},
async saveEditUsuario() {
this.editUsuarioError = '';
this.editUsuarioSuccess = false;
if (!this.editUsuarioForm.name.trim() || !this.editUsuarioForm.email.trim()) {
this.editUsuarioError = 'Nombre y email son requeridos.'; return;
}
this.saving = true;
const id = this.editUsuarioData.id || this.editUsuarioData._id;
const body = {};
if (this.editUsuarioForm.name) body.name = this.editUsuarioForm.name;
if (this.editUsuarioForm.email) body.email = this.editUsuarioForm.email;
if (this.editUsuarioForm.rol_id) body.rol_id = this.editUsuarioForm.rol_id;
if (this.editUsuarioForm.plan_id) body.plan_id = this.editUsuarioForm.plan_id;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const d = await r.json();
if (!r.ok) {
this.editUsuarioError = d.error || d.message || `Error ${r.status}`;
} else {
this.editUsuarioSuccess = true;
await this.loadTab();
setTimeout(() => { this.editUsuarioModal = false; }, 1200);
}
} catch(e) { this.editUsuarioError = e.message; }
this.saving = false;
},
// ─── Ver VCard ───────────────────────────────────────────────────────
openVerVcardModal(v) {
this.verVcardData = v;
this.verVcardTab = 'info';
this.verVcardModal = true;
},
// ─── Edit VCard ──────────────────────────────────────────────────────
openEditVcardModal(v) {
this.editVcardData = v;
this.editVcardTab = 'datos';
this.editVcardForm = {
nombre: v.nombre || '',
cargo: v.cargo || '',
descripcion: v.descripcion || '',
telefono: v.telefono || '',
consecutivo_telefono: v.consecutivo_telefono || '',
whatsapp: v.whatsapp || '',
consecutivo_whatsapp: v.consecutivo_whatsapp || '',
email: v.email || '',
empresa: v.empresa || '',
direccion: v.direccion || '',
website: v.website || '',
pais: v.pais || '',
ciudad: v.ciudad || '',
foto_perfil: v.foto_perfil || '',
logo: v.logo || '',
privacidad: v.privacidad || 'publica',
boton: v.boton || '',
estado: !!v.estado,
es_borrador: !!v.es_borrador,
linkedin: v.linkedin || '',
twitter: v.twitter || '',
facebook: v.facebook || '',
instagram: v.instagram || '',
tiktok: v.tiktok || '',
github: v.github || '',
behance: v.behance || '',
youtube: v.youtube || '',
youtube_channel: v.youtube_channel || '',
telegram: v.telegram || '',
messenger: v.messenger || '',
wechat: v.wechat || '',
signal: v.signal || '',
consecutivo_signal: v.consecutivo_signal || '',
line: v.line || '',
cabecera: v.cabecera || '#E8E8E8',
fondo: v.fondo || '#2B2D35',
color_cuerpo: v.color_cuerpo || '#FFFFFF',
color_iconos: v.color_iconos || '#2B2D35',
color_info: v.color_info || '#2B2D35',
fuente: v.fuente || 'Inter',
bordes: v.bordes || 'rounded-full',
estilo_vcard: v.estilo_vcard || 1,
orden_iconos: v.orden_iconos || 'top',
animacion: v.animacion || '',
};
this.editVcardError = '';
this.editVcardSuccess = false;
this.editVcardModal = true;
},
async saveEditVcard() {
this.editVcardError = '';
this.editVcardSuccess = false;
this.saving = true;
const id = this.editVcardData.id || this.editVcardData._id;
try {
const r = await fetch(`/app/vcard-api/vcards/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.editVcardForm)
});
const d = await r.json();
if (!r.ok) {
this.editVcardError = d.error || d.message || `Error ${r.status}`;
} else {
this.editVcardSuccess = true;
await this.loadTab();
setTimeout(() => { this.editVcardModal = false; }, 1200);
}
} catch(e) { this.editVcardError = e.message; }
this.saving = false;
},
// ─── Ver Usuario (detalle completo) ─────────────────────────────────
async openVerModal(u) {
this.verData = u;
this.verTab = 'general';
this.verError = '';
this.verSectionItems = [];
this.verModal = true;
this.verLoading = true;
const id = u.id || u._id;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}`);
const d = await r.json();
if (!r.ok) { this.verError = d.error || `Error ${r.status}`; }
else { this.verData = d; }
} catch(e) { this.verError = e.message; }
this.verLoading = false;
},
async loadVerSection(section) {
if (!['pagos','logs','miniwebs'].includes(section)) return;
this.verSectionItems = [];
this.verSectionLoading = true;
const id = this.verData && (this.verData.id || this.verData._id);
if (!id) { this.verSectionLoading = false; return; }
const map = {
pagos: `/app/vcard-api/usuarios/${id}/pagos`,
logs: `/app/vcard-api/usuarios/${id}/logs`,
miniwebs: `/app/vcard-api/usuarios/${id}/miniwebs`,
};
try {
const r = await fetch(map[section]);
const d = await r.json();
if (!r.ok) {}
else if (d.data && Array.isArray(d.data)) { this.verSectionItems = d.data; }
else if (Array.isArray(d)) { this.verSectionItems = d; }
} catch(e) {}
this.verSectionLoading = false;
},
// ─── Membresía ───────────────────────────────────────────────────────
async openMembresiaModal(u) {
this.membresiaUser = u;
this.membresiaInfo = null;
this.membresiaError = '';
this.membresiaForm = { anual: true, dias: 30, plan_id: '' };
this.membresiaModal = true;
try {
const id = u.id || u._id;
const r = await fetch(`/app/vcard-api/usuarios/${id}/membresia`);
const d = await r.json();
this.membresiaInfo = d;
// Cachear para el sort por vencimiento
this.membresiaMap[id] = d;
} catch(e) {}
},
async activarMembresia() {
this.membresiaError = '';
this.saving = true;
const id = this.membresiaUser.id || this.membresiaUser._id;
const body = { anual: this.membresiaForm.anual };
if (!this.membresiaForm.anual) body.dias = this.membresiaForm.dias;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}/activar-membresia`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const d = await r.json();
if (!r.ok) this.membresiaError = d.error || d.message || 'Error al activar.';
else {
const rInfo = await fetch(`/app/vcard-api/usuarios/${id}/membresia`);
this.membresiaInfo = await rInfo.json();
}
} catch(e) { this.membresiaError = e.message; }
this.saving = false;
},
async desactivarMembresia() {
if (!confirm('¿Desactivar membresía?')) return;
this.membresiaError = '';
this.saving = true;
const id = this.membresiaUser.id || this.membresiaUser._id;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}/desactivar-membresia`, { method: 'POST' });
const d = await r.json();
if (!r.ok) this.membresiaError = d.error || 'Error al desactivar.';
else this.membresiaInfo = { ...this.membresiaInfo, activo: false };
} catch(e) { this.membresiaError = e.message; }
this.saving = false;
},
async cambiarPlan() {
if (!this.membresiaForm.plan_id) return;
this.membresiaError = '';
this.saving = true;
const id = this.membresiaUser.id || this.membresiaUser._id;
try {
const r = await fetch(`/app/vcard-api/usuarios/${id}/cambiar-plan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_id: this.membresiaForm.plan_id, anual: this.membresiaForm.anual })
});
const d = await r.json();
if (!r.ok) this.membresiaError = d.error || d.message || 'Error al cambiar plan.';
else {
const rInfo = await fetch(`/app/vcard-api/usuarios/${id}/membresia`);
this.membresiaInfo = await rInfo.json();
}
} catch(e) { this.membresiaError = e.message; }
this.saving = false;
},
};
}
</script>