349 lines
20 KiB
PHP
349 lines
20 KiB
PHP
<div class="space-y-6" wire:key="subscription-management">
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<flux:heading size="lg">Subscription Management</flux:heading>
|
|
<flux:subheading>Manage billing, plans, and subscriptions</flux:subheading>
|
|
</div>
|
|
<flux:button wire:click="$set('showCreateSubscriptionModal', true)" variant="primary" size="sm" icon="plus">
|
|
Create Subscription
|
|
</flux:button>
|
|
</div>
|
|
|
|
{{-- Revenue Stats --}}
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-green-600">${{ number_format($this->stats['monthly_revenue'], 2) }}</div>
|
|
<div class="text-sm text-gray-600">Monthly Revenue</div>
|
|
<div class="text-xs text-gray-500">{{ $this->stats['revenue_change'] > 0 ? '+' : '' }}{{ $this->stats['revenue_change'] }}% vs last month</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-blue-600">{{ $this->stats['active_subscriptions'] }}</div>
|
|
<div class="text-sm text-gray-600">Active Subscriptions</div>
|
|
<div class="text-xs text-gray-500">{{ $this->stats['new_subscriptions'] }} new this month</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-yellow-600">{{ $this->stats['expiring_soon'] }}</div>
|
|
<div class="text-sm text-gray-600">Expiring Soon</div>
|
|
<div class="text-xs text-gray-500">Next 30 days</div>
|
|
</div>
|
|
</flux:card>
|
|
<flux:card>
|
|
<div class="text-center">
|
|
<div class="text-2xl font-bold text-purple-600">{{ number_format($this->stats['avg_subscription_value'], 2) }}</div>
|
|
<div class="text-sm text-gray-600">Avg Value</div>
|
|
<div class="text-xs text-gray-500">Per subscription</div>
|
|
</div>
|
|
</flux:card>
|
|
</div>
|
|
|
|
{{-- Filters --}}
|
|
<flux:card>
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
<flux:input wire:model.live="filters.search" placeholder="Search subscriptions..." icon="magnifying-glass" />
|
|
|
|
<flux:select wire:model.live="filters.status">
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="expired">Expired</option>
|
|
<option value="trial">Trial</option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model.live="filters.plan">
|
|
<option value="">All Plans</option>
|
|
<option value="basic">Basic</option>
|
|
<option value="pro">Professional</option>
|
|
<option value="enterprise">Enterprise</option>
|
|
<option value="custom">Custom</option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model.live="filters.billing_cycle">
|
|
<option value="">All Cycles</option>
|
|
<option value="monthly">Monthly</option>
|
|
<option value="yearly">Yearly</option>
|
|
<option value="lifetime">Lifetime</option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model.live="filters.payment_status">
|
|
<option value="">All Payments</option>
|
|
<option value="paid">Paid</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="failed">Failed</option>
|
|
</flux:select>
|
|
</div>
|
|
</flux:card>
|
|
|
|
{{-- Subscriptions Table --}}
|
|
<flux:card>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Customer
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Plan
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Usage
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Billing
|
|
</th>
|
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@forelse($subscriptions as $subscription)
|
|
<tr>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 h-10 w-10">
|
|
<div class="h-10 w-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<span class="text-sm font-medium text-blue-700">
|
|
{{ strtoupper(substr($subscription->user->name, 0, 1)) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="ml-4">
|
|
<div class="text-sm font-medium text-gray-900">{{ $subscription->user->name }}</div>
|
|
<div class="text-sm text-gray-500">{{ $subscription->user->email }}</div>
|
|
@if($subscription->user->company)
|
|
<div class="text-xs text-gray-400">{{ $subscription->user->company }}</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900">{{ ucfirst($subscription->plan) }}</div>
|
|
<div class="text-sm text-gray-500">${{ number_format($subscription->amount, 2) }} / {{ $subscription->billing_cycle }}</div>
|
|
<div class="text-xs text-gray-400">{{ $subscription->device_limit }} device{{ $subscription->device_limit === 1 ? '' : 's' }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
|
|
@if($subscription->status === 'active') bg-green-100 text-green-800
|
|
@elseif($subscription->status === 'trial') bg-blue-100 text-blue-800
|
|
@elseif($subscription->status === 'cancelled') bg-red-100 text-red-800
|
|
@else bg-gray-100 text-gray-800 @endif">
|
|
{{ ucfirst($subscription->status) }}
|
|
</span>
|
|
@if($subscription->ends_at)
|
|
<div class="text-xs text-gray-500 mt-1">
|
|
{{ $subscription->ends_at->isPast() ? 'Expired' : 'Ends' }}: {{ $subscription->ends_at->format('M j, Y') }}
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
@php
|
|
$deviceCount = $subscription->user->devices()->count();
|
|
$usagePercent = $subscription->device_limit > 0 ? ($deviceCount / $subscription->device_limit) * 100 : 0;
|
|
@endphp
|
|
<div class="text-sm text-gray-900">{{ $deviceCount }} / {{ $subscription->device_limit }} devices</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2 mt-1">
|
|
<div class="h-2 rounded-full {{ $usagePercent > 90 ? 'bg-red-500' : ($usagePercent > 70 ? 'bg-yellow-500' : 'bg-green-500') }}"
|
|
style="width: {{ min($usagePercent, 100) }}%"></div>
|
|
</div>
|
|
<div class="text-xs text-gray-500">{{ number_format($usagePercent, 1) }}% used</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900">
|
|
@if($subscription->stripe_id)
|
|
<div class="flex items-center space-x-1">
|
|
<flux:icon.credit-card class="w-4 h-4 text-gray-400" />
|
|
<span>Stripe</span>
|
|
</div>
|
|
@else
|
|
<span class="text-gray-500">Manual</span>
|
|
@endif
|
|
</div>
|
|
@if($subscription->next_billing_date)
|
|
<div class="text-xs text-gray-500">Next: {{ $subscription->next_billing_date->format('M j, Y') }}</div>
|
|
@endif
|
|
@if($subscription->payment_status)
|
|
<div class="text-xs {{ $subscription->payment_status === 'paid' ? 'text-green-600' : ($subscription->payment_status === 'failed' ? 'text-red-600' : 'text-yellow-600') }}">
|
|
{{ ucfirst($subscription->payment_status) }}
|
|
</div>
|
|
@endif
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<flux:button wire:click="editSubscription({{ $subscription->id }})" variant="outline" size="xs">
|
|
Edit
|
|
</flux:button>
|
|
@if($subscription->status === 'active')
|
|
<flux:button wire:click="cancelSubscription({{ $subscription->id }})" variant="danger" size="xs">
|
|
Cancel
|
|
</flux:button>
|
|
@elseif($subscription->status === 'cancelled')
|
|
<flux:button wire:click="renewSubscription({{ $subscription->id }})" variant="primary" size="xs">
|
|
Renew
|
|
</flux:button>
|
|
@endif
|
|
<flux:button wire:click="viewBilling({{ $subscription->id }})" variant="ghost" size="xs">
|
|
Billing
|
|
</flux:button>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
|
|
No subscriptions found matching your criteria.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{{-- Pagination --}}
|
|
<div class="mt-4">
|
|
{{ $subscriptions->links() }}
|
|
</div>
|
|
</flux:card>
|
|
|
|
{{-- Create/Edit Subscription Modal --}}
|
|
<flux:modal name="subscription-form" x-show="$wire.showCreateSubscriptionModal || $wire.showEditSubscriptionModal" class="md:max-w-lg">
|
|
<form wire:submit="{{ $editingSubscription ? 'updateSubscription' : 'createSubscription' }}">
|
|
<div class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg">{{ $editingSubscription ? 'Edit Subscription' : 'Create Subscription' }}</flux:heading>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
@if(!$editingSubscription)
|
|
<flux:select wire:model="subscriptionForm.user_id" label="Customer" required>
|
|
<option value="">Select Customer</option>
|
|
@foreach($users as $user)
|
|
<option value="{{ $user->id }}">{{ $user->name }} ({{ $user->email }})</option>
|
|
@endforeach
|
|
</flux:select>
|
|
@endif
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<flux:select wire:model.live="subscriptionForm.plan" label="Plan" required>
|
|
<option value="">Select Plan</option>
|
|
<option value="basic">Basic</option>
|
|
<option value="pro">Professional</option>
|
|
<option value="enterprise">Enterprise</option>
|
|
<option value="custom">Custom</option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model="subscriptionForm.billing_cycle" label="Billing Cycle" required>
|
|
<option value="">Select Cycle</option>
|
|
<option value="monthly">Monthly</option>
|
|
<option value="yearly">Yearly</option>
|
|
<option value="lifetime">Lifetime</option>
|
|
</flux:select>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<flux:input wire:model="subscriptionForm.amount" label="Amount ($)" type="number" step="0.01" required />
|
|
<flux:input wire:model="subscriptionForm.device_limit" label="Device Limit" type="number" required />
|
|
</div>
|
|
|
|
<flux:select wire:model="subscriptionForm.status" label="Status">
|
|
<option value="active">Active</option>
|
|
<option value="trial">Trial</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="expired">Expired</option>
|
|
</flux:select>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<flux:input wire:model="subscriptionForm.starts_at" label="Start Date" type="date" required />
|
|
<flux:input wire:model="subscriptionForm.ends_at" label="End Date" type="date" />
|
|
</div>
|
|
|
|
@if($subscriptionForm['billing_cycle'] !== 'lifetime')
|
|
<flux:input wire:model="subscriptionForm.next_billing_date" label="Next Billing Date" type="date" />
|
|
@endif
|
|
|
|
<flux:input wire:model="subscriptionForm.stripe_id" label="Stripe Subscription ID" placeholder="Optional - for Stripe integration" />
|
|
|
|
<flux:textarea wire:model="subscriptionForm.notes" label="Notes" rows="3" />
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-2">
|
|
<flux:button type="button" variant="ghost" wire:click="closeSubscriptionModal">Cancel</flux:button>
|
|
<flux:button type="submit" variant="primary">
|
|
{{ $editingSubscription ? 'Update' : 'Create' }}
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</flux:modal>
|
|
|
|
{{-- Billing History Modal --}}
|
|
<flux:modal name="billing-history" x-show="$wire.showBillingModal" class="md:max-w-2xl">
|
|
@if($selectedSubscription)
|
|
<div class="space-y-6">
|
|
<div>
|
|
<flux:heading size="lg">Billing History</flux:heading>
|
|
<flux:subheading>{{ $selectedSubscription->user->name }} - {{ ucfirst($selectedSubscription->plan) }} Plan</flux:subheading>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Invoice</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
@forelse($billingHistory as $payment)
|
|
<tr>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{{ $payment->created_at->format('M j, Y') }}
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
${{ number_format($payment->amount, 2) }}
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap">
|
|
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
|
|
{{ $payment->status === 'paid' ? 'bg-green-100 text-green-800' :
|
|
($payment->status === 'failed' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800') }}">
|
|
{{ ucfirst($payment->status) }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
@if($payment->invoice_url)
|
|
<flux:button href="{{ $payment->invoice_url }}" target="_blank" variant="outline" size="xs">
|
|
View
|
|
</flux:button>
|
|
@else
|
|
<span class="text-gray-400">N/A</span>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="4" class="px-4 py-4 text-center text-gray-500">
|
|
No billing history available.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="flex justify-end">
|
|
<flux:button wire:click="closeBillingModal" variant="outline">Close</flux:button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</flux:modal>
|
|
</div>
|