feat(vcard): agregar funcionalidad de edición de usuario y VCard, incluyendo filtros y modales
This commit is contained in:
+562
-13
@@ -1,5 +1,5 @@
|
||||
<!-- Vista: VCard API — Integración Admin Laravel -->
|
||||
<div x-data="vcardApiApp()" x-init="init()" class="bg-white rounded-lg shadow">
|
||||
<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">
|
||||
@@ -93,16 +93,160 @@
|
||||
class="text-xs px-2 py-0.5 rounded-full" x-text="u.estado ? 'Activo' : 'Inactivo'"></span>
|
||||
</td>
|
||||
<td class="py-2 px-3 text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<div class="flex justify-end gap-2 flex-wrap">
|
||||
<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"
|
||||
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"
|
||||
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"
|
||||
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>
|
||||
@@ -117,16 +261,19 @@
|
||||
<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">Usuario</th>
|
||||
<th class="py-2 px-3">Empresa</th>
|
||||
<th class="py-2 px-3">Email</th>
|
||||
<th class="py-2 px-3">Estado</th>
|
||||
<th class="py-2 px-3">Privacidad</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 font-medium" x-text="v.nombre || '—'"></td>
|
||||
<td class="py-2 px-3 text-xs text-gray-500" x-text="(v.user && v.user.email) ? v.user.email : (v.user_id || '—')"></td>
|
||||
<td class="py-2 px-3 text-gray-500" x-text="v.empresa || '—'"></td>
|
||||
<td class="py-2 px-3 text-gray-500" x-text="v.email || '—'"></td>
|
||||
<td class="py-2 px-3">
|
||||
@@ -137,6 +284,10 @@
|
||||
<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-right">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -173,6 +324,22 @@
|
||||
|
||||
<!-- ─── 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">
|
||||
@@ -208,6 +375,25 @@
|
||||
|
||||
<!-- ─── 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">
|
||||
@@ -245,6 +431,21 @@
|
||||
|
||||
<!-- ─── 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">
|
||||
@@ -381,6 +582,156 @@
|
||||
</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 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]" />
|
||||
</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: 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-lg mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex justify-between items-center p-5 border-b sticky top-0 bg-white z-10">
|
||||
<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>
|
||||
<div class="p-5 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>
|
||||
<input x-model="editVcardForm.telefono" 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">WhatsApp</label>
|
||||
<input x-model="editVcardForm.whatsapp" 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">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">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 class="flex items-center gap-4 sm:col-span-2">
|
||||
<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 x-show="editVcardError" class="sm:col-span-2 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-600" x-text="editVcardError"></div>
|
||||
<div x-show="editVcardSuccess" class="sm:col-span-2 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 pb-5">
|
||||
<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: 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">
|
||||
@@ -475,6 +826,34 @@ function vcardApiApp() {
|
||||
membresiaError: '',
|
||||
planes: [],
|
||||
|
||||
// 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 editar vcard
|
||||
editVcardModal: false,
|
||||
editVcardData: null,
|
||||
editVcardForm: { nombre: '', cargo: '', descripcion: '', telefono: '', whatsapp: '', email: '', empresa: '', direccion: '', website: '', pais: '', ciudad: '', estado: true, privacidad: 'publica', es_borrador: false },
|
||||
editVcardError: '',
|
||||
editVcardSuccess: false,
|
||||
|
||||
// Drill-down por usuario
|
||||
drillDownUserId: null,
|
||||
drillDownSection: '',
|
||||
drillDownItems: [],
|
||||
drillDownLoading: false,
|
||||
drillDownError: '',
|
||||
|
||||
get totalPages() {
|
||||
return Math.max(1, Math.ceil(this.total / this.perPage));
|
||||
},
|
||||
@@ -485,6 +864,13 @@ function vcardApiApp() {
|
||||
this.loadPlanes();
|
||||
},
|
||||
|
||||
closeModals() {
|
||||
this.configModal = false;
|
||||
this.membresiaModal = false;
|
||||
this.editUsuarioModal = false;
|
||||
this.editVcardModal = false;
|
||||
},
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
const r = await fetch('/app/vcard-api/config');
|
||||
@@ -551,27 +937,43 @@ function vcardApiApp() {
|
||||
this.items = [];
|
||||
this.total = 0;
|
||||
this.errorMsg = '';
|
||||
this.closeDrillDown();
|
||||
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 = '';
|
||||
const endpoints = {
|
||||
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}`,
|
||||
};
|
||||
try {
|
||||
const r = await fetch(endpoints[this.tab]);
|
||||
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; }
|
||||
// Laravel pagination o array plano
|
||||
if (d.data && Array.isArray(d.data)) {
|
||||
this.items = d.data;
|
||||
this.total = d.total || d.data.length;
|
||||
@@ -587,6 +989,14 @@ function vcardApiApp() {
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
async loadPlanes() {
|
||||
try {
|
||||
const r = await fetch('/app/vcard-api/planes');
|
||||
@@ -615,6 +1025,145 @@ function vcardApiApp() {
|
||||
this.loading = 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;
|
||||
},
|
||||
|
||||
// ─── Edit VCard ──────────────────────────────────────────────────────
|
||||
|
||||
openEditVcardModal(v) {
|
||||
this.editVcardData = v;
|
||||
this.editVcardForm = {
|
||||
nombre: v.nombre || '',
|
||||
cargo: v.cargo || '',
|
||||
descripcion: v.descripcion || '',
|
||||
telefono: v.telefono || '',
|
||||
whatsapp: v.whatsapp || '',
|
||||
email: v.email || '',
|
||||
empresa: v.empresa || '',
|
||||
direccion: v.direccion || '',
|
||||
website: v.website || '',
|
||||
pais: v.pais || '',
|
||||
ciudad: v.ciudad || '',
|
||||
estado: !!v.estado,
|
||||
privacidad: v.privacidad || 'publica',
|
||||
es_borrador: !!v.es_borrador,
|
||||
};
|
||||
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;
|
||||
},
|
||||
|
||||
// ─── Drill-down por usuario ──────────────────────────────────────────
|
||||
|
||||
openDrillDown(u) {
|
||||
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');
|
||||
},
|
||||
|
||||
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;
|
||||
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`,
|
||||
};
|
||||
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;
|
||||
},
|
||||
|
||||
// ─── Membresía ───────────────────────────────────────────────────────
|
||||
|
||||
async openMembresiaModal(u) {
|
||||
this.membresiaUser = u;
|
||||
this.membresiaInfo = null;
|
||||
|
||||
Reference in New Issue
Block a user