@if(session('success'))
{{ session('success') }}
@endif @if(session('error'))
{{ session('error') }}
@endif {{-- Stats --}}
{{ $stats['total'] }}
Total
{{ $stats['active'] }}
Activos
{{ $stats['provisioning'] }}
En provisión
{{ $stats['paused'] }}
Suspendidos
{{ $stats['failed'] }}
Fallidos
${{ number_format($money['mrr'], 0) }}
MRR USD/mes
{{-- Banda de dinero — corte por motor (CD legacy vs BP producto) + churn. Misma fuente que el Centro de Comando (MissileFunnelMetrics::money). --}} {{-- Banda de vencimientos — tracking de cobros próximos y atrasados. Fuente: BillingMetricsService::dueMetrics() (cacheado 5 min). --}} {{-- Centro de Acción — chips clickeables con conteo de issues operacionales. SoT: TenantIssueService. Cada chip linkea a ?issue=. --}} @php $sevOrder = ['critical' => 1, 'warning' => 2, 'info' => 3]; $sevColor = ['critical' => 'danger', 'warning' => 'warning', 'info' => 'info']; $issuesWithCount = collect($actionableIssues)->filter(fn ($i) => ($i['count'] ?? 0) > 0) ->sortBy(fn ($i) => ($sevOrder[$i['severity']] ?? 9) * 1000 - $i['count']); @endphp @if($issuesWithCount->count() > 0 || $issue)
Centro de acción @foreach($issuesWithCount as $slug => $def) @php $color = $sevColor[$def['severity']] ?? 'secondary'; @endphp {{ $def['label'] }}: {{ $def['count'] }} @endforeach @if($issue && isset($actionableIssues[$issue])) {{ $actionableIssues[$issue]['cta'] }} limpiar @endif
@endif {{-- Integridad — lleva el integrity:audit a la grilla (chips accionables) --}} @php $integrityChips = [ 'orphan' => ['icon' => 'fa-user-slash', 'label' => 'Huérfanos', 'color' => 'danger'], 'no_reseller' => ['icon' => 'fa-handshake-slash','label' => 'Sin reseller', 'color' => 'warning'], 'no_product' => ['icon' => 'fa-box-open', 'label' => 'Sin producto', 'color' => 'warning'], 'invalid_product' => ['icon' => 'fa-link-slash', 'label' => 'Producto inválido', 'color' => 'warning'], ]; @endphp @if(array_sum($integrityCounts) > 0 || $integrity)
Integridad: @foreach($integrityChips as $key => $cfg) @php $c = $integrityCounts[$key] ?? 0; @endphp {{ $cfg['label'] }}: {{ $c }} @endforeach @if($integrity) limpiar lente @endif
@endif {{-- Tabla --}}

Proyectos {{ $tenants->total() }} tenants encontrados

{{-- Botón "⚖️ Comparar" — aparece solo cuando hay filtro por producto y N>=2 tenants live. --}} @if($product && $tenants->total() >= 2) @endif
@if($search || $status || $server || $product || $reseller || $billing || $vencimiento || $intentos || $integrity) Limpiar @endif
@forelse($tenants as $t) @php [$badgeLabel, $badgeClass] = $t->status_badge; @endphp {{-- Intentos de cobro fallidos — predictor de churn. 0 → guion · 1-2 → warning · 3+ → danger (umbral de past_due en webhook). --}} {{-- Último pago exitoso — lo escribe invoice.payment_succeeded. Permite ver "cliente vivo" sin consultar Stripe API. --}} {{-- Issues per-row — qué le falta a este tenant. SoT: TenantIssueService::issuesFor(). Click → filtra global. --}} @empty @endforelse
Proyecto / Producto Cliente Reseller Estado Server / Cpanel Plan Intentos Último pago Acciones requeridas Synced Acciones
pipeline_status === 'on_development') title="{{ $t->pipeline_status === 'on_development' ? 'No se puede borrar mientras provisión' : 'Seleccionar para borrado masivo' }}">
{{ $t->project_name }}
@if($t->domain) {{ $t->domain }} @endif
@if($t->product_name) @php $isCore = in_array($t->product_name, $cores); $isMarketplace = collect($marketplaceCores)->contains('slug', $t->product_name); $isSpecial = collect($specialCategories)->firstWhere('slug', $t->product_name); @endphp @if($isSpecial) {{ $isSpecial['label'] }} @elseif($isMarketplace || $isCore) {{ $t->product_name }} @else {{ $t->product_name }} @endif @else Sin Producto @endif
@php $pm = $productMeta[$t->product_name] ?? null; @endphp @if($pm && $pm['demo'])
{{ \Illuminate\Support\Str::after($pm['demo'], 'demo-') }} @foreach($pm['modules'] as $mod) {{ $mod }} @endforeach
@endif
@if($t->user) {{ $t->user->email }} @else Huérfano Asignar @endif @if($t->parentReseller) {{ $t->parentReseller->email }} @else Sin Reseller @endif {{ $t->parentReseller ? 'Cambiar' : 'Asignar' }} {{ $badgeLabel }} @if($t->server) {{ $t->server }} @if($t->cpanel_user) {{ $t->cpanel_user }} @endif @else @endif @php $mrr = $t->monthlyMrrUsd(); $model = $t->billing_model; $modelMap = [ 'stripe' => ['BewPro', 'primary'], 'legacy' => ['CD legacy', 'warning'], 'manual' => ['CD manual', 'warning'], 'free' => ['Free', 'secondary'], 'trial' => ['Trial', 'info'], ]; [$modelLabel, $modelColor] = $modelMap[$model] ?? [$model ?: '—', 'secondary']; @endphp @if($mrr > 0)
${{ number_format($mrr, 0) }}/mes
@elseif($t->amount_usd)
${{ number_format($t->amount_usd, 0) }} (no activo)
@else @endif {{ $modelLabel }}@if($t->billing_cycle === 'annual') · anual @elseif($t->billing_cycle === 'monthly') · mensual @endif @if($t->billing_status === 'past_due') EN MORA{{ $t->failed_payment_count ? ' · '.$t->failed_payment_count.' intento(s)' : '' }} @endif @if($t->next_billing_date) @php $overdue = $t->next_billing_date->isPast(); @endphp {{ $t->next_billing_date->format('d/m/y') }}@if($overdue) ¡vencido!@endif @endif
@php $fails = (int) ($t->failed_payment_count ?? 0); @endphp @if($fails === 0) @elseif($fails >= 3) {{ $fails }} @else {{ $fails }} @endif @if($t->last_payment_succeeded_at) {{ $t->last_payment_succeeded_at->diffForHumans() }} @else @endif @php $rIssues = $rowIssues[$t->id] ?? []; @endphp @if(empty($rIssues)) OK @else
@foreach($rIssues as $iss) @php $c = $sevColor[$iss['severity']] ?? 'secondary'; @endphp {{ $iss['label'] }} @endforeach
@endif
@if($t->last_stripe_sync_at)
{{ $t->last_stripe_sync_at->diffForHumans() }}
@endif
{{ $t->last_synced_at?->diffForHumans() ?? '—' }}
{{-- Credenciales + manual: cualquier tenant con dominio activo --}} @if($t->domain && in_array($t->pipeline_status, ['active','on_development','required','paused'])) @endif {{-- Stripe Dashboard link — solo si hay customer real (no mock). hasUsableBilling() filtra IDs falsos seedeados de dev. --}} @if($t->hasUsableBilling()) @endif {{-- Vincular subscripción — para tenants con domain pero sin sub real (típicamente tenants legacy con stripe_customer_id falso). --}} @if($t->domain && empty($t->stripe_subscription_id)) @endif {{-- Quick-edit branding/contenido: cualquier tenant con dominio + sitio realmente desplegado. Excluye solo failed/archived (sin archivos en VPS). 'paused' incluido porque los archivos siguen existiendo aunque el sitio esté offline. --}} @if($t->domain && in_array($t->pipeline_status, ['active','on_development','required','paused'])) @endif {{-- Update code (git pull + clear caches) — requiere cpanel_user + server para saber a qué VPS apuntar. --}} @if($t->cpanel_user && $t->server && in_array($t->pipeline_status, ['active','on_development','required','paused'])) @endif
Sin tenants con esos filtros.
@if(request()->hasAny(['q', 'status', 'server', 'product', 'reseller', 'billing', 'vencimiento', 'intentos', 'issue', 'integrity'])) Limpiar filtros @endif
{{ $tenants->links() }}
{{-- ════════════════════════════════════════════════════════════════════ Modales por fila — renderizados FUERA de .table-responsive para evitar el flicker (el overflow del contenedor rompía el backdrop/focus-trap). Los triggers viven en la tabla y apuntan acá por id. ════════════════════════════════════════════════════════════════════ --}} @foreach($tenants as $t) @unless($t->user) {{-- Asignar Cliente --}} @endunless {{-- Asignar/Cambiar Reseller --}} {{-- Editar Tenant --}} {{-- Eliminar Tenant --}} @endforeach {{-- ═══════════ Bulk actions: barra flotante con delete + update ═══════════ --}}
0 seleccionado(s)
{{-- ═══════════ Modal Bulk Update ═══════════ --}} {{-- ════════════ Modal Quick Edit (branding + content del tenant) ════════════ --}} {{-- Modal: Update código del tenant (git pull + clear caches via UpdateTenantJob). --}} {{-- Modal: Credenciales, manual y GTAG del tenant --}} {{-- Modal: Vincular subscripción Stripe desde tenant fantasma --}} {{-- Modal: Comparativa side-by-side de N tenants del mismo producto. --}}