feat(vcard-api): modal Ver Usuario con tabs + rol select + roles acumulados
- Reemplaza drill-down inline de Usuarios por modal 'Ver' con tabs: General, Perfil, VCards, Wallet, Pagos, Logs, MiniWebs - Añade columna Rol en tabla de usuarios con badge coloreado - Edit Usuario: rol_id ahora es <select> con nombres reales (administrador/cliente) - Roles se acumulan automáticamente al cargar usuarios (sin endpoint dedicado) - Carga lazy de Pagos, Logs, MiniWebs dentro del modal de detalle - VCards en detalle incluye URL con enlace directo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
62e49b39ad
commit
b43c144a82
+325
-190
@@ -77,6 +77,7 @@
|
||||
<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">Estado</th>
|
||||
<th class="py-2 px-3 text-right">Acciones</th>
|
||||
@@ -86,167 +87,32 @@
|
||||
<template x-for="u in items" :key="u.id || u._id">
|
||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||
<td class="py-2 px-3 font-medium" x-text="u.name || u.nombre || '—'"></td>
|
||||
<td class="py-2 px-3 text-gray-500" x-text="u.email || '—'"></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">
|
||||
<span :class="u.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="u.estado ? 'Activo' : 'Inactivo'"></span>
|
||||
<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" @click="toggleUsuario(u, 'activar')" title="Activar"
|
||||
<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" @click="toggleUsuario(u, 'desactivar')" title="Desactivar"
|
||||
<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 @click="openMembresiaModal(u)" title="Membresía"
|
||||
<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>
|
||||
<button @click="openDrillDown(u)" title="Ver detalle"
|
||||
class="text-purple-500 hover:text-purple-700 text-xs px-2 py-1 border border-purple-200 rounded">⊕ Ver</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Fila drill-down expandible -->
|
||||
<tr x-show="drillDownUserId === (u.id || u._id)" class="bg-purple-50">
|
||||
<td colspan="5" class="px-4 py-3">
|
||||
<div class="flex gap-2 mb-3 flex-wrap">
|
||||
<button @click="loadDrillDown('vcards')"
|
||||
:class="drillDownSection === 'vcards' ? 'bg-purple-600 text-white' : 'bg-white text-purple-600 border border-purple-300'"
|
||||
class="text-xs px-3 py-1 rounded-full">VCards</button>
|
||||
<button @click="loadDrillDown('pagos')"
|
||||
:class="drillDownSection === 'pagos' ? 'bg-purple-600 text-white' : 'bg-white text-purple-600 border border-purple-300'"
|
||||
class="text-xs px-3 py-1 rounded-full">Pagos</button>
|
||||
<button @click="loadDrillDown('transacciones')"
|
||||
:class="drillDownSection === 'transacciones' ? 'bg-purple-600 text-white' : 'bg-white text-purple-600 border border-purple-300'"
|
||||
class="text-xs px-3 py-1 rounded-full">Transacciones</button>
|
||||
<button @click="loadDrillDown('logs')"
|
||||
:class="drillDownSection === 'logs' ? 'bg-purple-600 text-white' : 'bg-white text-purple-600 border border-purple-300'"
|
||||
class="text-xs px-3 py-1 rounded-full">Logs</button>
|
||||
<button @click="loadDrillDown('miniwebs')"
|
||||
:class="drillDownSection === 'miniwebs' ? 'bg-purple-600 text-white' : 'bg-white text-purple-600 border border-purple-300'"
|
||||
class="text-xs px-3 py-1 rounded-full">MiniWebs</button>
|
||||
<button @click="closeDrillDown()" class="ml-auto text-xs text-gray-400 hover:text-gray-600">✕ Cerrar</button>
|
||||
</div>
|
||||
<div x-show="drillDownLoading" class="text-xs text-gray-400">Cargando...</div>
|
||||
<div x-show="drillDownError" class="text-xs text-red-500" x-text="drillDownError"></div>
|
||||
<div x-show="!drillDownLoading && drillDownItems.length === 0 && !drillDownError" class="text-xs text-gray-400">Sin registros.</div>
|
||||
<!-- VCards del usuario -->
|
||||
<template x-if="drillDownSection === 'vcards' && drillDownItems.length > 0">
|
||||
<div 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">Estado</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<template x-for="r in drillDownItems" :key="r.id || r._id">
|
||||
<tr class="border-b border-purple-100">
|
||||
<td class="py-1 px-2 font-medium" x-text="r.nombre || '—'"></td>
|
||||
<td class="py-1 px-2 text-gray-500" x-text="r.empresa || '—'"></td>
|
||||
<td class="py-1 px-2 text-gray-500" x-text="r.email || '—'"></td>
|
||||
<td class="py-1 px-2">
|
||||
<span :class="r.estado ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||||
class="px-2 py-0.5 rounded-full" x-text="r.estado ? 'Activa' : 'Inactiva'"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Pagos del usuario -->
|
||||
<template x-if="drillDownSection === 'pagos' && drillDownItems.length > 0">
|
||||
<div 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 drillDownItems" :key="r.id || r._id">
|
||||
<tr class="border-b border-purple-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-2 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>
|
||||
</template>
|
||||
<!-- Transacciones del usuario -->
|
||||
<template x-if="drillDownSection === 'transacciones' && drillDownItems.length > 0">
|
||||
<div 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">Monto</th>
|
||||
<th class="py-1 px-2">Estado</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 drillDownItems" :key="r.id || r._id">
|
||||
<tr class="border-b border-purple-100">
|
||||
<td class="py-1 px-2 font-mono bg-gray-100 rounded text-gray-700" x-text="r.type || '—'"></td>
|
||||
<td class="py-1 px-2 font-medium" x-text="r.amount || '—'"></td>
|
||||
<td class="py-1 px-2">
|
||||
<span :class="{'bg-green-100 text-green-700': r.status==='completed','bg-yellow-100 text-yellow-700': r.status==='pending','bg-red-100 text-red-700': r.status==='failed'}"
|
||||
class="px-2 py-0.5 rounded-full" x-text="r.status || '—'"></span>
|
||||
</td>
|
||||
<td class="py-1 px-2 text-gray-500 max-w-xs truncate" x-text="r.description || '—'"></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>
|
||||
</template>
|
||||
<!-- Logs del usuario -->
|
||||
<template x-if="drillDownSection === 'logs' && drillDownItems.length > 0">
|
||||
<div 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 drillDownItems" :key="r.id || r._id">
|
||||
<tr class="border-b border-purple-100">
|
||||
<td class="py-1 px-2 font-mono" 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>
|
||||
</template>
|
||||
<!-- MiniWebs del usuario -->
|
||||
<template x-if="drillDownSection === 'miniwebs' && drillDownItems.length > 0">
|
||||
<div 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 vinculada</th><th class="py-1 px-2">Estado</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<template x-for="r in drillDownItems" :key="r.id || r._id">
|
||||
<tr class="border-b border-purple-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-2 py-0.5 rounded-full" x-text="r.activo ? 'Activa' : 'Inactiva'"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -582,6 +448,252 @@
|
||||
</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="'https://vcard.u-site.app/'+v.url" 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">
|
||||
@@ -614,9 +726,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Rol ID <span class="text-gray-400 font-normal">(opcional)</span></label>
|
||||
<input x-model="editUsuarioForm.rol_id" type="text" placeholder="ID del rol"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
|
||||
<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>
|
||||
@@ -847,12 +964,20 @@ function vcardApiApp() {
|
||||
editVcardError: '',
|
||||
editVcardSuccess: false,
|
||||
|
||||
// Drill-down por usuario
|
||||
drillDownUserId: null,
|
||||
drillDownSection: '',
|
||||
drillDownItems: [],
|
||||
drillDownLoading: false,
|
||||
drillDownError: '',
|
||||
// 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));
|
||||
@@ -869,6 +994,7 @@ function vcardApiApp() {
|
||||
this.membresiaModal = false;
|
||||
this.editUsuarioModal = false;
|
||||
this.editVcardModal = false;
|
||||
this.verModal = false;
|
||||
},
|
||||
|
||||
async loadConfig() {
|
||||
@@ -937,7 +1063,6 @@ function vcardApiApp() {
|
||||
this.items = [];
|
||||
this.total = 0;
|
||||
this.errorMsg = '';
|
||||
this.closeDrillDown();
|
||||
if (this.hasConfig) this.loadTab();
|
||||
},
|
||||
|
||||
@@ -977,9 +1102,11 @@ function vcardApiApp() {
|
||||
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);
|
||||
} else if (Array.isArray(d)) {
|
||||
this.items = d;
|
||||
this.total = d.length;
|
||||
if (this.tab === 'usuarios') this._collectRoles(d);
|
||||
} else {
|
||||
this.items = [];
|
||||
this.total = 0;
|
||||
@@ -989,6 +1116,17 @@ function vcardApiApp() {
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
_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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clearFilters(tab) {
|
||||
if (tab === 'pagos') this.filters.pagos = { estado: '', moneda: '', desde: '', hasta: '' };
|
||||
if (tab === 'transacciones') this.filters.transacciones = { type: '', status: '' };
|
||||
@@ -1119,47 +1257,44 @@ function vcardApiApp() {
|
||||
this.saving = false;
|
||||
},
|
||||
|
||||
// ─── Drill-down por usuario ──────────────────────────────────────────
|
||||
// ─── Ver Usuario (detalle completo) ─────────────────────────────────
|
||||
|
||||
openDrillDown(u) {
|
||||
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;
|
||||
if (this.drillDownUserId === id) { this.closeDrillDown(); return; }
|
||||
this.drillDownUserId = id;
|
||||
this.drillDownSection = 'vcards';
|
||||
this.drillDownItems = [];
|
||||
this.drillDownError = '';
|
||||
this.loadDrillDown('vcards');
|
||||
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;
|
||||
},
|
||||
|
||||
closeDrillDown() {
|
||||
this.drillDownUserId = null;
|
||||
this.drillDownSection = '';
|
||||
this.drillDownItems = [];
|
||||
this.drillDownError = '';
|
||||
},
|
||||
|
||||
async loadDrillDown(section) {
|
||||
this.drillDownSection = section;
|
||||
this.drillDownItems = [];
|
||||
this.drillDownError = '';
|
||||
if (!this.drillDownUserId) return;
|
||||
this.drillDownLoading = true;
|
||||
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 = {
|
||||
vcards: `/app/vcard-api/usuarios/${this.drillDownUserId}/vcards`,
|
||||
pagos: `/app/vcard-api/usuarios/${this.drillDownUserId}/pagos`,
|
||||
transacciones: `/app/vcard-api/usuarios/${this.drillDownUserId}/transacciones`,
|
||||
logs: `/app/vcard-api/usuarios/${this.drillDownUserId}/logs`,
|
||||
miniwebs: `/app/vcard-api/usuarios/${this.drillDownUserId}/miniwebs`,
|
||||
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) { this.drillDownError = d.error || `Error ${r.status}`; }
|
||||
else if (d.data && Array.isArray(d.data)) { this.drillDownItems = d.data; }
|
||||
else if (Array.isArray(d)) { this.drillDownItems = d; }
|
||||
else { this.drillDownError = 'Respuesta inesperada.'; }
|
||||
} catch(e) { this.drillDownError = e.message; }
|
||||
this.drillDownLoading = false;
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user