349 lines
12 KiB
PHP
349 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Models\Subscription;
|
|
use Filament\Actions\Action;
|
|
use Filament\Actions\Concerns\InteractsWithActions;
|
|
use Filament\Forms\Components\DatePicker;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Concerns\InteractsWithForms;
|
|
use Filament\Forms\Contracts\HasForms;
|
|
use Filament\Pages\Page;
|
|
use Filament\Schemas\Components\Grid;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\Filter;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use UnitEnum;
|
|
|
|
class CustomerAnalytics extends Page implements HasForms, HasTable
|
|
{
|
|
use InteractsWithActions;
|
|
use InteractsWithForms;
|
|
use InteractsWithTable;
|
|
|
|
protected static string|null|\BackedEnum $navigationIcon = 'heroicon-o-chart-bar';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Admin';
|
|
|
|
protected static ?int $navigationSort = 1;
|
|
|
|
protected string $view = 'filament.pages.customer-analytics';
|
|
|
|
public $startDate;
|
|
|
|
public $endDate;
|
|
|
|
public $selectedProvider = 'all';
|
|
|
|
public $selectedPlan = 'all';
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->startDate = now()->subDays(30)->format('Y-m-d');
|
|
$this->endDate = now()->format('Y-m-d');
|
|
}
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return 'Customer Analytics';
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('export_report')
|
|
->label('Export Report')
|
|
->icon('heroicon-o-arrow-down-tray')
|
|
->color('success')
|
|
->action(fn () => $this->exportReport()),
|
|
|
|
Action::make('refresh')
|
|
->label('Refresh')
|
|
->icon('heroicon-o-arrow-path')
|
|
->color('gray')
|
|
->action(fn () => $this->resetTable()),
|
|
];
|
|
}
|
|
|
|
protected function getHeaderWidgets(): array
|
|
{
|
|
return [
|
|
\App\Filament\Widgets\CustomerAnalyticsOverview::class,
|
|
\App\Filament\Widgets\SubscriptionMetrics::class,
|
|
\App\Filament\Widgets\CouponPerformanceMetrics::class,
|
|
];
|
|
}
|
|
|
|
protected function getFormSchema(): array
|
|
{
|
|
return [
|
|
Grid::make(4)
|
|
->schema([
|
|
DatePicker::make('startDate')
|
|
->label('Start Date')
|
|
->default(fn () => now()->subDays(30))
|
|
->required(),
|
|
|
|
DatePicker::make('endDate')
|
|
->label('End Date')
|
|
->default(fn () => now())
|
|
->required(),
|
|
|
|
Select::make('selectedProvider')
|
|
->label('Provider')
|
|
->options([
|
|
'all' => 'All Providers',
|
|
'stripe' => 'Stripe',
|
|
'lemon_squeezy' => 'Lemon Squeezy',
|
|
'polar' => 'Polar.sh',
|
|
'oxapay' => 'OxaPay',
|
|
'crypto' => 'Crypto',
|
|
'activation_key' => 'Activation Key',
|
|
])
|
|
->default('all'),
|
|
|
|
Select::make('selectedPlan')
|
|
->label('Plan')
|
|
->options(function () {
|
|
$plans = DB::table('plans')->pluck('name', 'id');
|
|
|
|
return ['all' => 'All Plans'] + $plans->toArray();
|
|
})
|
|
->default('all'),
|
|
]),
|
|
];
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->query(
|
|
$this->getCustomerAnalyticsQuery()
|
|
)
|
|
->columns([
|
|
TextColumn::make('user.name')
|
|
->label('Customer')
|
|
->searchable()
|
|
->sortable()
|
|
->weight('medium'),
|
|
|
|
TextColumn::make('user.email')
|
|
->label('Email')
|
|
->searchable()
|
|
->copyable()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('plan.name')
|
|
->label('Plan')
|
|
->searchable()
|
|
->sortable()
|
|
->badge()
|
|
->color('primary'),
|
|
|
|
TextColumn::make('provider')
|
|
->label('Provider')
|
|
->badge()
|
|
->colors([
|
|
'blue' => 'stripe',
|
|
'green' => 'lemon_squeezy',
|
|
'purple' => 'polar',
|
|
'orange' => 'oxapay',
|
|
'gray' => 'crypto',
|
|
'pink' => 'activation_key',
|
|
]),
|
|
|
|
TextColumn::make('status')
|
|
->label('Status')
|
|
->badge()
|
|
->colors([
|
|
'success' => 'active',
|
|
'warning' => 'trialing',
|
|
'danger' => 'cancelled',
|
|
'secondary' => 'paused',
|
|
'gray' => 'incomplete',
|
|
]),
|
|
|
|
TextColumn::make('subscription_age')
|
|
->label('Age')
|
|
->state(function ($record) {
|
|
$started = $record->starts_at ?? $record->created_at;
|
|
|
|
return $started ? $started->diffForHumans() : 'Unknown';
|
|
})
|
|
->sortable(),
|
|
|
|
TextColumn::make('total_coupon_discount')
|
|
->label('Total Discount')
|
|
->money('USD')
|
|
->sortable()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('trial_extensions_count')
|
|
->label('Trial Extensions')
|
|
->state(fn ($record) => $record->trial_extensions_count ?? 0)
|
|
->alignCenter()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('subscription_changes_count')
|
|
->label('Changes')
|
|
->state(fn ($record) => $record->subscription_changes_count ?? 0)
|
|
->alignCenter()
|
|
->toggleable(),
|
|
|
|
TextColumn::make('mrr')
|
|
->label('MRR')
|
|
->money('USD')
|
|
->state(fn ($record) => $record->calculateMRR())
|
|
->sortable()
|
|
->toggleable(),
|
|
])
|
|
->defaultSort('created_at', 'desc')
|
|
->filters([
|
|
SelectFilter::make('provider')
|
|
->options([
|
|
'stripe' => 'Stripe',
|
|
'lemon_squeezy' => 'Lemon Squeezy',
|
|
'polar' => 'Polar.sh',
|
|
'oxapay' => 'OxaPay',
|
|
'crypto' => 'Crypto',
|
|
'activation_key' => 'Activation Key',
|
|
]),
|
|
|
|
SelectFilter::make('status')
|
|
->options([
|
|
'active' => 'Active',
|
|
'trialing' => 'Trial',
|
|
'cancelled' => 'Cancelled',
|
|
'paused' => 'Paused',
|
|
'incomplete' => 'Incomplete',
|
|
]),
|
|
|
|
Filter::make('date_range')
|
|
->schema([
|
|
DatePicker::make('start_date')
|
|
->label('Start Date')
|
|
->required(),
|
|
DatePicker::make('end_date')
|
|
->label('End Date')
|
|
->required(),
|
|
])
|
|
->query(function (Builder $query, array $data): Builder {
|
|
return $query
|
|
->when(
|
|
$data['start_date'],
|
|
fn (Builder $query, $date): Builder => $query->whereDate('subscriptions.created_at', '>=', $date)
|
|
)
|
|
->when(
|
|
$data['end_date'],
|
|
fn (Builder $query, $date): Builder => $query->whereDate('subscriptions.created_at', '<=', $date)
|
|
);
|
|
}),
|
|
|
|
Filter::make('has_coupon_usage')
|
|
->label('Has Coupon Usage')
|
|
->query(fn (Builder $query): Builder => $query->whereHas('couponUsages')),
|
|
|
|
Filter::make('has_trial_extension')
|
|
->label('Has Trial Extension')
|
|
->query(fn (Builder $query): Builder => $query->whereHas('trialExtensions')),
|
|
])
|
|
->emptyStateHeading('No customer data found')
|
|
->emptyStateDescription('No customer analytics data available for the selected filters.')
|
|
->emptyStateActions([
|
|
Action::make('reset_filters')
|
|
->label('Reset Filters')
|
|
->icon('heroicon-o-arrow-path')
|
|
->action(fn () => $this->resetTable()),
|
|
]);
|
|
}
|
|
|
|
protected function getCustomerAnalyticsQuery(): Builder
|
|
{
|
|
return Subscription::query()
|
|
->with(['user', 'plan', 'couponUsages', 'trialExtensions', 'subscriptionChanges'])
|
|
->withCount(['couponUsages', 'trialExtensions', 'subscriptionChanges'])
|
|
->when($this->selectedProvider !== 'all', function ($query) {
|
|
$query->where('provider', $this->selectedProvider);
|
|
})
|
|
->when($this->selectedPlan !== 'all', function ($query) {
|
|
$query->where('plan_id', $this->selectedPlan);
|
|
})
|
|
->when($this->startDate, function ($query) {
|
|
$query->whereDate('subscriptions.created_at', '>=', $this->startDate);
|
|
})
|
|
->when($this->endDate, function ($query) {
|
|
$query->whereDate('subscriptions.created_at', '<=', $this->endDate);
|
|
});
|
|
}
|
|
|
|
public function exportReport()
|
|
{
|
|
$subscriptions = $this->getCustomerAnalyticsQuery()->get();
|
|
|
|
$filename = 'customer_analytics_'.now()->format('Y_m_d_H_i_s').'.csv';
|
|
|
|
// Create a temporary file
|
|
$handle = fopen('php://temp', 'r+');
|
|
|
|
// Add BOM for Excel UTF-8 support
|
|
fwrite($handle, "\xEF\xBB\xBF");
|
|
|
|
// Write headers
|
|
$headers = [
|
|
'Customer', 'Email', 'Plan', 'Provider', 'Status', 'Subscription Age',
|
|
'Total Discount', 'Trial Extensions', 'Subscription Changes', 'MRR',
|
|
'Created At', 'Trial Ends At', 'Ends At',
|
|
];
|
|
fputcsv($handle, $headers);
|
|
|
|
// Write data rows
|
|
foreach ($subscriptions as $subscription) {
|
|
$started = $subscription->starts_at ?? $subscription->created_at;
|
|
fputcsv($handle, [
|
|
$subscription->user?->name ?? 'Unknown',
|
|
$subscription->user?->email ?? 'Unknown',
|
|
$subscription->plan?->name ?? 'Unknown',
|
|
$subscription->provider,
|
|
$subscription->status,
|
|
$started ? $started->diffForHumans() : 'Unknown',
|
|
$subscription->couponUsages()->sum('discount_amount'),
|
|
$subscription->trialExtensions()->count(),
|
|
$subscription->subscriptionChanges()->count(),
|
|
$subscription->calculateMRR(),
|
|
$subscription->created_at->toDateTimeString(),
|
|
$subscription->trial_ends_at?->toDateTimeString() ?? 'N/A',
|
|
$subscription->ends_at?->toDateTimeString() ?? 'N/A',
|
|
]);
|
|
}
|
|
|
|
// Rewind the file pointer
|
|
rewind($handle);
|
|
|
|
// Get the CSV content
|
|
$csvContent = stream_get_contents($handle);
|
|
|
|
// Close the file handle
|
|
fclose($handle);
|
|
|
|
// Return a download response
|
|
return response()->streamDownload(
|
|
function () use ($csvContent) {
|
|
echo $csvContent;
|
|
},
|
|
$filename,
|
|
[
|
|
'Content-Type' => 'text/csv',
|
|
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
|
|
]
|
|
);
|
|
}
|
|
}
|