feat(vcard-api): modales Ver/Editar Plan con todos los campos
- Añade botones 'Ver' y 'Editar' en cada card de plan - Modal 'Ver Plan': todos los campos + tabla completa de tarifas por país (nombre, descripción, características HTML, imagen, cantidad, estado, plan_id_anual/mensual, PayPal IDs, timestamps, tarifas con pais_id/monto) - Modal 'Editar Plan': formulario con todos los campos editables (nombre, descripcion, caracteristicas HTML, cantidad, img, estado, plan_id_anual/mensual, plan_anual/mensual_paypal) + tabla de tarifas en sólo lectura - Backend: agrega VcardApiPlan (GET) y VcardApiPlanUpdate (PUT) como proxy - Rutas: GET/PUT /app/vcard-api/planes/:id Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
15b38203f2
commit
3114a2da6c
@@ -225,6 +225,20 @@
|
||||
</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>
|
||||
@@ -1232,6 +1246,213 @@
|
||||
</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 ? 'https://vcard.u-site.app/' + verPlanData.img : ''"
|
||||
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> <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>
|
||||
@@ -1301,6 +1522,21 @@ function vcardApiApp() {
|
||||
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,
|
||||
@@ -1333,6 +1569,8 @@ function vcardApiApp() {
|
||||
this.editVcardModal = false;
|
||||
this.verModal = false;
|
||||
this.verVcardModal = false;
|
||||
this.verPlanModal = false;
|
||||
this.editPlanModal = false;
|
||||
},
|
||||
|
||||
async loadConfig() {
|
||||
@@ -1478,6 +1716,63 @@ function vcardApiApp() {
|
||||
return montos.slice(0, 5); // max 5 precios únicos
|
||||
},
|
||||
|
||||
// ─── 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');
|
||||
|
||||
@@ -216,6 +216,14 @@ func VcardApiPlanes(c *fiber.Ctx) error {
|
||||
return proxyVcard(c, http.MethodGet, "/api/admin/planes")
|
||||
}
|
||||
|
||||
func VcardApiPlan(c *fiber.Ctx) error {
|
||||
return proxyVcard(c, http.MethodGet, "/api/admin/planes/"+c.Params("id"))
|
||||
}
|
||||
|
||||
func VcardApiPlanUpdate(c *fiber.Ctx) error {
|
||||
return proxyVcardMutate(c, http.MethodPut, "/api/admin/planes/"+c.Params("id"))
|
||||
}
|
||||
|
||||
func VcardApiMembresia(c *fiber.Ctx) error {
|
||||
return proxyVcard(c, http.MethodGet, "/api/admin/usuarios/"+c.Params("id")+"/membresia")
|
||||
}
|
||||
|
||||
@@ -242,6 +242,8 @@ func UserRoutes(app fiber.Router) {
|
||||
protected.Put("/vcard-api/vcards/:id", controllers.VcardApiVcardUpdate)
|
||||
// Resto (solo GET)
|
||||
protected.Get("/vcard-api/planes", controllers.VcardApiPlanes)
|
||||
protected.Get("/vcard-api/planes/:id", controllers.VcardApiPlan)
|
||||
protected.Put("/vcard-api/planes/:id", controllers.VcardApiPlanUpdate)
|
||||
protected.Get("/vcard-api/pagos", controllers.VcardApiPagos)
|
||||
protected.Get("/vcard-api/transacciones", controllers.VcardApiTransacciones)
|
||||
protected.Get("/vcard-api/logs", controllers.VcardApiLogs)
|
||||
|
||||
Reference in New Issue
Block a user