up
This commit is contained in:
@@ -66,6 +66,7 @@ func main() {
|
||||
migrations.SeedRenovaciones()
|
||||
migrations.SeedIntegraciones()
|
||||
migrations.SeedPasarelas()
|
||||
migrations.SeedSaas()
|
||||
migrations.SeedServidores()
|
||||
migrations.SeedAdministracion()
|
||||
// Iniciar cron de vencimientos
|
||||
|
||||
@@ -95,6 +95,9 @@ func Migrate() {
|
||||
// Insertar submódulo "Pasarelas de Pago" en el módulo Integraciones
|
||||
SeedPasarelas()
|
||||
|
||||
// Insertar submódulos de SaaS (Productos + Integraciones) en el módulo Integraciones
|
||||
SeedSaas()
|
||||
|
||||
log.Println("Migration Completed...")
|
||||
}
|
||||
|
||||
@@ -476,3 +479,58 @@ func SeedAdministracion() {
|
||||
|
||||
log.Println("[SEED] Seed de Administración completado.")
|
||||
}
|
||||
|
||||
// SeedSaas agrega los submódulos de gestión de productos SaaS e integraciones
|
||||
// de pago al módulo "Integraciones" ya existente. Es idempotente.
|
||||
func SeedSaas() {
|
||||
db := app.Http.Database.DB
|
||||
|
||||
// Reutilizar el módulo "Integraciones" (creado por SeedIntegraciones)
|
||||
var modulo models.Modules
|
||||
if err := db.Where("title = ?", "Integraciones").First(&modulo).Error; err != nil {
|
||||
log.Printf("[SEED] Módulo 'Integraciones' no encontrado para SeedSaas: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
entries := []struct{ title, desc, url string }{
|
||||
{"Productos SaaS", "Gestión de productos SaaS y su documentación asociada", "/app/saas"},
|
||||
{"Integraciones SaaS", "Callbacks de pago hacia APIs externas y logs de despacho", "/app/saas-api"},
|
||||
}
|
||||
|
||||
var insertados []models.Submodules
|
||||
for _, e := range entries {
|
||||
var sub models.Submodules
|
||||
if err := db.Where("url = ?", e.url).First(&sub).Error; err != nil {
|
||||
sub = models.Submodules{
|
||||
Title: e.title,
|
||||
Description: e.desc,
|
||||
Url: e.url,
|
||||
ModuleId: modulo.ID,
|
||||
ModifiedAt: time.Now(),
|
||||
}
|
||||
if err := db.Create(&sub).Error; err != nil {
|
||||
log.Printf("[SEED] Error creando submódulo '%s': %v", e.title, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("[SEED] Submódulo '%s' creado (ID %d)", e.title, sub.ID)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulo '%s' ya existe (ID %d)", sub.Title, sub.ID)
|
||||
}
|
||||
insertados = append(insertados, sub)
|
||||
}
|
||||
|
||||
var roles []models.Roles
|
||||
if err := db.Find(&roles).Error; err != nil {
|
||||
log.Printf("[SEED] Error obteniendo roles: %v", err)
|
||||
return
|
||||
}
|
||||
for _, rol := range roles {
|
||||
if err := db.Model(&rol).Association("Submodules").Append(&insertados); err != nil {
|
||||
log.Printf("[SEED] Error asignando submódulos SaaS al rol '%s': %v", rol.Name, err)
|
||||
} else {
|
||||
log.Printf("[SEED] Submódulos SaaS asignados al rol '%s'", rol.Name)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[SEED] Seed de SaaS completado.")
|
||||
}
|
||||
|
||||
@@ -530,6 +530,20 @@
|
||||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||||
<div x-show="activeTab === 'dlocal'" x-transition>
|
||||
|
||||
<!-- Sub-tabs dLocal -->
|
||||
<div class="border-b border-gray-100 mb-5">
|
||||
<nav class="-mb-px flex gap-5 text-xs">
|
||||
<button @click="dlocalTab = 'config'"
|
||||
:class="dlocalTab==='config' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
||||
class="pb-2 transition-colors">Configuración</button>
|
||||
<button @click="dlocalTab = 'planes'; loadDlocalPlanes()"
|
||||
:class="dlocalTab==='planes' ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-400 hover:text-gray-600'"
|
||||
class="pb-2 transition-colors">Planes de suscripción</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- ── Sub-tab: Configuración ─────────────────────────────────── -->
|
||||
<div x-show="dlocalTab === 'config'">
|
||||
<div class="grid lg:grid-cols-2 gap-6">
|
||||
|
||||
<!-- Formulario dLocal -->
|
||||
@@ -635,28 +649,167 @@
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-200 rounded-xl p-5">
|
||||
<h3 class="text-sm font-semibold mb-2 text-gray-700">Endpoints disponibles</h3>
|
||||
<div class="space-y-1.5 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
||||
<span class="text-gray-600">/v1/dlocal/subscription/crear-plan</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-mono text-[10px]">GET</span>
|
||||
<span class="text-gray-600">/v1/dlocal/subscription/ver-plan/:id</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="bg-yellow-50 text-yellow-600 px-1.5 py-0.5 rounded font-mono text-[10px]">PATCH</span>
|
||||
<span class="text-gray-600">/v1/dlocal/subscription/actualizar-plan/:id</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-mono text-[10px]">POST</span>
|
||||
<span class="text-gray-600">/v1/dlocal/payment/crear-pago</span>
|
||||
</div>
|
||||
<h3 class="text-sm font-semibold mb-2 flex items-center gap-2 text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-[#8eb02f]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
|
||||
</svg>
|
||||
URL del Webhook
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 mb-3">Registra esta URL en el panel dLocal → Integraciones → Webhook</p>
|
||||
<div class="flex items-center gap-2 bg-gray-50 border border-gray-200 rounded-lg px-3 py-2">
|
||||
<span class="text-xs font-mono text-gray-700 flex-1 truncate">/webhooks/dlocal</span>
|
||||
<button @click="copyDlocalWebhook()" title="Copiar URL" class="text-[#8eb02f] hover:text-[#6d8c24] flex-shrink-0">
|
||||
<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="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /sub-tab config -->
|
||||
|
||||
<!-- ── Sub-tab: Planes de suscripción ─────────────────────────── -->
|
||||
<div x-show="dlocalTab === 'planes'">
|
||||
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<p class="text-xs text-gray-500">Planes almacenados en dLocal directamente. Los cambios se reflejan en tiempo real.</p>
|
||||
<button @click="openPlanAdd()" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm">+ Nuevo plan</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading planes -->
|
||||
<div x-show="dlocalPlanesLoading" class="py-8 text-center text-gray-400 text-sm">Consultando planes en dLocal…</div>
|
||||
<div x-show="dlocalPlanesError" class="py-4 px-4 bg-red-50 text-red-600 rounded text-sm" x-text="dlocalPlanesError"></div>
|
||||
|
||||
<!-- Tabla de planes -->
|
||||
<div x-show="!dlocalPlanesLoading && !dlocalPlanesError" class="border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div x-show="dlocalPlanes.length === 0" class="py-12 text-center text-gray-400 text-sm">
|
||||
Sin planes creados en dLocal aún.
|
||||
</div>
|
||||
<div x-show="dlocalPlanes.length > 0">
|
||||
<table class="w-full text-xs table-auto">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-left border-b">
|
||||
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">ID</th>
|
||||
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Nombre</th>
|
||||
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Monto</th>
|
||||
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Frecuencia</th>
|
||||
<th class="py-3 px-4 font-semibold text-gray-500 uppercase tracking-wide text-[10px]">Estado</th>
|
||||
<th class="py-3 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="plan in dlocalPlanes" :key="plan.id">
|
||||
<tr class="border-b last:border-0 hover:bg-gray-50 transition-colors">
|
||||
<td class="py-3 px-4 font-mono text-gray-400 text-[10px]" x-text="(plan.id||'—').slice(0,12)+'…'"></td>
|
||||
<td class="py-3 px-4">
|
||||
<div class="font-medium text-gray-800" x-text="plan.name || '—'"></div>
|
||||
<div class="text-gray-400 text-[10px] mt-0.5" x-text="plan.description || ''"></div>
|
||||
</td>
|
||||
<td class="py-3 px-4 font-semibold text-gray-800">
|
||||
<span x-text="plan.currency + ' ' + (plan.amount || 0).toLocaleString('es-CO')"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-gray-600">
|
||||
<span x-text="plan.frequency_type || '—'"></span>
|
||||
<span x-show="plan.frequency_value" x-text="' x' + plan.frequency_value" class="text-gray-400"></span>
|
||||
<span x-show="plan.day_of_month" x-text="' (día ' + plan.day_of_month + ')'" class="text-gray-400"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<span x-text="plan.status || 'active'"
|
||||
:class="(plan.status === 'inactive' || plan.status === 'INACTIVE') ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-700'"
|
||||
class="px-2 py-0.5 rounded-full text-[10px] font-semibold capitalize"></span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<button @click="openPlanEdit(plan)" title="Editar" class="text-blue-500 hover:text-blue-700 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal crear / editar plan -->
|
||||
<div x-show="showPlanModal" 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-xl mx-4 max-h-screen overflow-y-auto">
|
||||
<div class="flex justify-between items-center p-5 border-b">
|
||||
<h2 class="text-lg font-semibold" x-text="planEditMode ? 'Editar plan dLocal' : 'Nuevo plan dLocal'"></h2>
|
||||
<button @click="showPlanModal = false" class="text-gray-400 hover:text-gray-600 text-xl">✕</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-3">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Nombre <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="planForm.name" class="border rounded w-full p-2 text-sm" placeholder="Ej: Plan mensual VCard">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Descripción</label>
|
||||
<input type="text" x-model="planForm.description" class="border rounded w-full p-2 text-sm" placeholder="Descripción breve">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Moneda <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="planForm.currency" class="border rounded w-full p-2 text-sm font-mono" placeholder="COP / USD">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Monto <span class="text-red-500">*</span></label>
|
||||
<input type="number" x-model.number="planForm.amount" class="border rounded w-full p-2 text-sm" placeholder="50000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Tipo de frecuencia <span class="text-red-500">*</span></label>
|
||||
<select x-model="planForm.frequency_type" class="border rounded w-full p-2 text-sm">
|
||||
<option value="MONTHLY">Mensual</option>
|
||||
<option value="WEEKLY">Semanal</option>
|
||||
<option value="DAILY">Diario</option>
|
||||
<option value="YEARLY">Anual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Valor frecuencia <span class="text-gray-400 font-normal">(ej: 1)</span></label>
|
||||
<input type="number" x-model.number="planForm.frequency_value" class="border rounded w-full p-2 text-sm" min="1">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">Día del mes <span class="text-gray-400 font-normal">(opcional)</span></label>
|
||||
<input type="number" x-model.number="planForm.day_of_month" class="border rounded w-full p-2 text-sm" min="1" max="31">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">País <span class="text-gray-400 font-normal">(opcional)</span></label>
|
||||
<input type="text" x-model="planForm.country" class="border rounded w-full p-2 text-sm font-mono" placeholder="CO">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">URL de notificación (webhook)</label>
|
||||
<input type="url" x-model="planForm.notification_url" class="border rounded w-full p-2 text-sm font-mono" placeholder="https://…/webhooks/dlocal">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">URL éxito</label>
|
||||
<input type="url" x-model="planForm.success_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">URL retorno</label>
|
||||
<input type="url" x-model="planForm.back_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-700 mb-1">URL error</label>
|
||||
<input type="url" x-model="planForm.error_url" class="border rounded w-full p-2 text-sm font-mono">
|
||||
</div>
|
||||
</div>
|
||||
<p x-show="planError" x-text="planError" class="text-red-500 text-sm"></p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 p-5 border-t">
|
||||
<button @click="showPlanModal = false" class="px-4 py-2 border rounded text-sm">Cancelar</button>
|
||||
<button @click="savePlan()" :disabled="planSaving" class="bg-[#8eb02f] text-white px-4 py-2 rounded text-sm disabled:opacity-50">
|
||||
<span x-show="!planSaving">Guardar en dLocal</span>
|
||||
<span x-show="planSaving">Guardando…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /sub-tab planes -->
|
||||
|
||||
</div>
|
||||
|
||||
</div><!-- /container -->
|
||||
@@ -723,6 +876,23 @@ function pasarelasApp() {
|
||||
url_dev: '',
|
||||
},
|
||||
|
||||
// Sub-tabs dLocal
|
||||
dlocalTab: 'config',
|
||||
|
||||
// Planes dLocal
|
||||
dlocalPlanes: [],
|
||||
dlocalPlanesLoading: false,
|
||||
dlocalPlanesError: '',
|
||||
showPlanModal: false,
|
||||
planEditMode: false,
|
||||
planSaving: false,
|
||||
planError: '',
|
||||
planEditID: '',
|
||||
planForm: {},
|
||||
defaultPlanForm() {
|
||||
return { name:'', description:'', currency:'COP', amount:0, frequency_type:'MONTHLY', frequency_value:1, day_of_month:0, country:'CO', notification_url:'', success_url:'', back_url:'', error_url:'' };
|
||||
},
|
||||
|
||||
// ─── Init ───────────────────────────────────────────────────────
|
||||
init() {
|
||||
this.loadBold();
|
||||
@@ -849,6 +1019,90 @@ function pasarelasApp() {
|
||||
}
|
||||
},
|
||||
|
||||
async loadDlocalPlanes() {
|
||||
this.dlocalPlanesLoading = true;
|
||||
this.dlocalPlanesError = '';
|
||||
try {
|
||||
const r = await axios.get('/app/dlocal/planes');
|
||||
// dLocal devuelve { message, response } donde response es el JSON raw
|
||||
const raw = r.data.response;
|
||||
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
// La API de dLocal devuelve { plans: [...] } o directamente un array
|
||||
this.dlocalPlanes = Array.isArray(parsed) ? parsed : (parsed.plans || parsed.data || []);
|
||||
} catch (e) {
|
||||
this.dlocalPlanesError = e.response?.data?.error || 'No se pudo obtener los planes de dLocal.';
|
||||
} finally {
|
||||
this.dlocalPlanesLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
openPlanAdd() {
|
||||
this.planEditMode = false;
|
||||
this.planEditID = '';
|
||||
this.planForm = this.defaultPlanForm();
|
||||
this.planError = '';
|
||||
this.showPlanModal = true;
|
||||
},
|
||||
|
||||
openPlanEdit(plan) {
|
||||
this.planEditMode = true;
|
||||
this.planEditID = plan.id;
|
||||
this.planForm = {
|
||||
name: plan.name || '',
|
||||
description: plan.description || '',
|
||||
currency: plan.currency || 'COP',
|
||||
amount: plan.amount || 0,
|
||||
frequency_type: plan.frequency_type || 'MONTHLY',
|
||||
frequency_value: plan.frequency_value || 1,
|
||||
day_of_month: plan.day_of_month || 0,
|
||||
country: plan.country || '',
|
||||
notification_url: plan.notification_url || '',
|
||||
success_url: plan.success_url || '',
|
||||
back_url: plan.back_url || '',
|
||||
error_url: plan.error_url || '',
|
||||
};
|
||||
this.planError = '';
|
||||
this.showPlanModal = true;
|
||||
},
|
||||
|
||||
async savePlan() {
|
||||
this.planError = '';
|
||||
if (!this.planForm.name) { this.planError = 'El nombre es requerido.'; return; }
|
||||
if (!this.planForm.currency) { this.planError = 'La moneda es requerida.'; return; }
|
||||
if (!this.planForm.amount) { this.planError = 'El monto es requerido.'; return; }
|
||||
this.planSaving = true;
|
||||
try {
|
||||
if (this.planEditMode) {
|
||||
// Solo campos editables en PATCH
|
||||
const patch = {
|
||||
name: this.planForm.name,
|
||||
description: this.planForm.description,
|
||||
amount: this.planForm.amount,
|
||||
notification_url: this.planForm.notification_url,
|
||||
back_url: this.planForm.back_url,
|
||||
success_url: this.planForm.success_url,
|
||||
error_url: this.planForm.error_url,
|
||||
};
|
||||
await axios.patch(`/app/dlocal/planes/${this.planEditID}`, patch);
|
||||
this.showToast('Plan actualizado en dLocal ✓');
|
||||
} else {
|
||||
await axios.post('/app/dlocal/planes', this.planForm);
|
||||
this.showToast('Plan creado en dLocal ✓');
|
||||
}
|
||||
this.showPlanModal = false;
|
||||
await this.loadDlocalPlanes();
|
||||
} catch (e) {
|
||||
this.planError = e.response?.data?.error || 'Error al guardar el plan.';
|
||||
} finally {
|
||||
this.planSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
copyDlocalWebhook() {
|
||||
const url = window.location.origin + '/webhooks/dlocal';
|
||||
navigator.clipboard.writeText(url).then(() => this.showToast('URL copiada: ' + url));
|
||||
},
|
||||
|
||||
// ─── Toast ───────────────────────────────────────────────────────
|
||||
showToast(msg, type = 'success') {
|
||||
this.toast = { show: true, msg, type };
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b">Nombre</th>
|
||||
<th class="py-2 px-4 border-b">SaaS</th>
|
||||
<th class="py-2 px-4 border-b">Pasarela</th>
|
||||
<th class="py-2 px-4 border-b">Endpoint</th>
|
||||
<th class="py-2 px-4 border-b">Método</th>
|
||||
<th class="py-2 px-4 border-b">Estado</th>
|
||||
@@ -27,12 +28,21 @@
|
||||
</thead>
|
||||
<tbody class="text-gray-500">
|
||||
<template x-if="items.length === 0 && !loading">
|
||||
<tr><td colspan="6" class="py-4 text-center text-gray-400">Sin registros</td></tr>
|
||||
<tr><td colspan="7" class="py-4 text-center text-gray-400">Sin registros</td></tr>
|
||||
</template>
|
||||
<template x-for="item in items" :key="item.ID">
|
||||
<tr class="hover:bg-gray-50 border-b border-gray-100">
|
||||
<td class="py-2 px-4 font-medium text-gray-800" x-text="item.nombre"></td>
|
||||
<td class="py-2 px-4 text-xs text-gray-600" x-text="item.SaasProducto ? item.SaasProducto.nombre : '-'"></td>
|
||||
<td class="py-2 px-4">
|
||||
<span x-text="item.pasarela || 'ambas'"
|
||||
:class="{
|
||||
'bg-purple-100 text-purple-700': item.pasarela === 'dlocal',
|
||||
'bg-orange-100 text-orange-700': item.pasarela === 'bold',
|
||||
'bg-gray-100 text-gray-600': !item.pasarela || item.pasarela === 'ambas'
|
||||
}"
|
||||
class="text-xs px-2 py-0.5 rounded-full capitalize"></span>
|
||||
</td>
|
||||
<td class="py-2 px-4">
|
||||
<span class="font-mono text-xs text-blue-700 break-all" x-text="item.endpoint_url"></span>
|
||||
</td>
|
||||
@@ -86,16 +96,27 @@
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre / etiqueta <span class="text-red-500">*</span></label>
|
||||
<input type="text" x-model="form.nombre" class="border rounded w-full p-2 text-sm" placeholder="Ej: VCard – confirmar pago">
|
||||
</div>
|
||||
<!-- SaaS -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Producto SaaS <span class="text-red-500">*</span></label>
|
||||
<select x-model.number="form.saas_id" class="border rounded w-full p-2 text-sm">
|
||||
<option value="">— Seleccionar —</option>
|
||||
<template x-for="s in saasOpts" :key="s.ID">
|
||||
<option :value="s.ID" x-text="s.nombre"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-xs text-gray-400 mt-0.5">El SaaS debe tener un Servicio vinculado para que se active al confirmar contratos.</p>
|
||||
<!-- SaaS + Pasarela -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Producto SaaS <span class="text-red-500">*</span></label>
|
||||
<select x-model.number="form.saas_id" class="border rounded w-full p-2 text-sm">
|
||||
<option value="">— Seleccionar —</option>
|
||||
<template x-for="s in saasOpts" :key="s.ID">
|
||||
<option :value="s.ID" x-text="s.nombre"></option>
|
||||
</template>
|
||||
</select>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Debe tener un Servicio vinculado para activarse al confirmar contratos.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Pasarela de pago</label>
|
||||
<select x-model="form.pasarela" class="border rounded w-full p-2 text-sm">
|
||||
<option value="ambas">Ambas (Bold + dLocal)</option>
|
||||
<option value="dlocal">Solo dLocal</option>
|
||||
<option value="bold">Solo Bold</option>
|
||||
</select>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Qué pasarela dispara esta notificación.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Endpoint URL -->
|
||||
<div>
|
||||
@@ -170,7 +191,7 @@ function saasApiApp() {
|
||||
saving: false, error: '',
|
||||
form: {},
|
||||
defaultForm() {
|
||||
return { saas_id: 0, nombre: '', endpoint_url: '', metodo: 'POST', api_key_header: '', api_key_value: '', payload_template: '', timeout_seg: 10, activo: true };
|
||||
return { saas_id: 0, nombre: '', pasarela: 'ambas', endpoint_url: '', metodo: 'POST', api_key_header: '', api_key_value: '', payload_template: '', timeout_seg: 10, activo: true };
|
||||
},
|
||||
async init() { await this.load(1); },
|
||||
async load(p) {
|
||||
@@ -197,6 +218,7 @@ function saasApiApp() {
|
||||
id: item.ID,
|
||||
saas_id: item.saas_id,
|
||||
nombre: item.nombre,
|
||||
pasarela: item.pasarela || 'ambas',
|
||||
endpoint_url: item.endpoint_url,
|
||||
metodo: item.metodo || 'POST',
|
||||
api_key_header: item.api_key_header || '',
|
||||
|
||||
Reference in New Issue
Block a user