From 930144f8f8daa00a0f54052564d1b75533523f86 Mon Sep 17 00:00:00 2001 From: Gitea Date: Fri, 20 Jun 2025 21:44:33 +0530 Subject: [PATCH] updated stats, add logs and other data in user resource --- app/Filament/Resources/UserResource.php | 65 +++++++- .../Resources/UserResource/Pages/EditUser.php | 69 +++++++++ .../RelationManagers/LogsRelationManager.php | 50 +++++++ .../UsageLogsRelationManager.php | 55 +++++++ app/Filament/Widgets/StatsOverview.php | 139 ++++++++++++++---- app/Models/User.php | 10 ++ 6 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 app/Filament/Resources/UserResource/RelationManagers/LogsRelationManager.php create mode 100644 app/Filament/Resources/UserResource/RelationManagers/UsageLogsRelationManager.php diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 192b894..eea7de3 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -3,8 +3,11 @@ namespace App\Filament\Resources; use App\Filament\Resources\UserResource\Pages; +use App\Filament\Resources\UserResource\RelationManagers\LogsRelationManager; +use App\Filament\Resources\UserResource\RelationManagers\UsageLogsRelationManager; use App\Models\User; use DB; +use Filament\Actions\Action; use Filament\Forms; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\Select; @@ -14,17 +17,22 @@ use Filament\Notifications\Notification; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Columns\BadgeColumn; +use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\SoftDeletingScope; +use Illuminate\Support\Facades\Response; class UserResource extends Resource { protected static ?string $model = User::class; - protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack'; + protected static ?string $navigationIcon = 'heroicon-o-users'; + + protected static ?string $navigationGroup = 'Admin'; public static function form(Form $form): Form { @@ -38,7 +46,13 @@ class UserResource extends Resource ->email() ->required() ->maxLength(255), - + TextInput::make('email_verified_at') + ->label('Email Verification Status') + ->disabled() + ->formatStateUsing(fn ($record) => $record->email_verified_at ?? '' + ? 'Verified at ' . $record->email_verified_at->toDateTimeString() + : 'Not Verified') + ->helperText('Shows whether the user has verified their email address.'), TextInput::make('stripe_id') ->label('Stripe ID') ->disabled() @@ -74,6 +88,15 @@ class UserResource extends Resource ->columns([ TextColumn::make('name')->sortable()->searchable(), TextColumn::make('email')->sortable()->searchable(), + IconColumn::make('email_verified_at') + ->label('Verified') + ->boolean() + ->trueIcon('heroicon-o-check-circle') + ->falseIcon('heroicon-o-x-circle') + ->trueColor('success') + ->falseColor('danger') + ->getStateUsing(fn ($record) => !is_null($record->email_verified_at)) + ->sortable(), BadgeColumn::make('level') ->label('User Level') ->getStateUsing(function ($record) { @@ -96,7 +119,38 @@ class UserResource extends Resource ]) ->defaultSort('created_at', 'desc') ->filters([ - // + SelectFilter::make('subscription_status') + ->label('Subscription Status') + ->options([ + 'subscribed' => 'Has Active Subscription', + 'not_subscribed' => 'No Active Subscription', + ]) + ->query(function ($query, array $data) { + if ($data['value'] === 'subscribed') { + $query->whereHas('subscriptions', function ($query) { + $query->where('stripe_status', 'active') + ->orWhere('stripe_status', 'trialing'); + }); + } elseif ($data['value'] === 'not_subscribed') { + $query->whereDoesntHave('subscriptions', function ($query) { + $query->where('stripe_status', 'active') + ->orWhere('stripe_status', 'trialing'); + }); + } + }), + SelectFilter::make('email_verified') + ->label('Email Verification') + ->options([ + 'verified' => 'Verified', + 'not_verified' => 'Not Verified', + ]) + ->query(function ($query, array $data) { + if ($data['value'] === 'verified') { + $query->whereNotNull('email_verified_at'); + } elseif ($data['value'] === 'not_verified') { + $query->whereNull('email_verified_at'); + } + }), ]) ->actions([ Tables\Actions\EditAction::make(), @@ -144,7 +198,8 @@ class UserResource extends Resource public static function getRelations(): array { return [ - // + LogsRelationManager::class, + UsageLogsRelationManager::class, ]; } @@ -156,4 +211,6 @@ class UserResource extends Resource 'edit' => Pages\EditUser::route('/{record}/edit'), ]; } + + } diff --git a/app/Filament/Resources/UserResource/Pages/EditUser.php b/app/Filament/Resources/UserResource/Pages/EditUser.php index b8770a9..114f355 100644 --- a/app/Filament/Resources/UserResource/Pages/EditUser.php +++ b/app/Filament/Resources/UserResource/Pages/EditUser.php @@ -3,8 +3,11 @@ namespace App\Filament\Resources\UserResource\Pages; use App\Filament\Resources\UserResource; +use App\Models\User; use Filament\Actions; +use Filament\Actions\Action; use Filament\Resources\Pages\EditRecord; +use Illuminate\Support\Facades\Response; class EditUser extends EditRecord { @@ -14,6 +17,72 @@ class EditUser extends EditRecord { return [ Actions\DeleteAction::make(), + Action::make('download_report') + ->label('Download User Report') + ->icon('heroicon-o-user') + ->action(function (User $record) { + $userData = [ + 'Name' => $record->name, + 'Email' => $record->email, + 'Stripe ID' => $record->stripe_id ?? 'N/A', + 'Payment Method Type' => $record->pm_type ?? 'N/A', + 'Card Last 4' => $record->pm_last_four ?? 'N/A', + 'Trial Ends At' => $record->trial_ends_at ? $record->trial_ends_at->toDateTimeString() : 'N/A', + 'User Level' => match ($record->level) { + 0 => 'Normal User', + 1 => 'Banned', + 9 => 'Super Admin', + default => 'Unknown', + }, + 'Email Verified At' => $record->email_verified_at ? $record->email_verified_at->toDateTimeString() : 'Not Verified', + ]; + + $csv = fopen('php://temp', 'r+'); + // User Details Header + fputcsv($csv, ['User Details']); + fputcsv($csv, array_keys($userData)); + fputcsv($csv, array_values($userData)); + fputcsv($csv, []); + + // Usage Logs Header + fputcsv($csv, ['Usage Logs']); + fputcsv($csv, ['IP Address', 'Emails Created Count', 'Emails Received Count', 'Emails Created History', 'Emails Received History', 'Created At']); + foreach ($record->usageLogs as $log) { + fputcsv($csv, [ + $log->ip_address, + $log->emails_created_count, + $log->emails_received_count, + is_array($log->emails_created_history) ? implode('; ', $log->emails_created_history) : 'None', + is_array($log->emails_received_history) ? implode('; ', $log->emails_received_history) : 'None', + $log->created_at->toDateTimeString(), + ]); + } + fputcsv($csv, []); + + // General Logs Header + fputcsv($csv, ['General Logs']); + fputcsv($csv, ['IP Address', 'Email', 'Created At']); + foreach ($record->logs as $log) { + fputcsv($csv, [ + $log->ip, + $log->email, + $log->created_at->toDateTimeString(), + ]); + } + + rewind($csv); + $csvContent = stream_get_contents($csv); + fclose($csv); + + return Response::streamDownload( + function () use ($csvContent) { + echo $csvContent; + }, + "user_{$record->id}_report_" . now()->format('Ymd_His') . '.csv', + ['Content-Type' => 'text/csv'] + ); + }) + ->color('primary'), ]; } } diff --git a/app/Filament/Resources/UserResource/RelationManagers/LogsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/LogsRelationManager.php new file mode 100644 index 0000000..b82f6d5 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/LogsRelationManager.php @@ -0,0 +1,50 @@ +columns([ + Tables\Columns\TextColumn::make('ip') + ->label('IP Address') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('email') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('created_at') + ->label('Logged At') + ->dateTime() + ->sortable(), + ]) + ->filters([ + // + ]) + ->headerActions([ + //Tables\Actions\CreateAction::make(), + ]) + ->actions([ + //Tables\Actions\EditAction::make(), + //Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + //Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/UserResource/RelationManagers/UsageLogsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/UsageLogsRelationManager.php new file mode 100644 index 0000000..4706850 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/UsageLogsRelationManager.php @@ -0,0 +1,55 @@ +columns([ + Tables\Columns\TextColumn::make('ip_address') + ->label('IP Address') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('emails_created_count') + ->label('Emails Created') + ->sortable(), + Tables\Columns\TextColumn::make('emails_received_count') + ->label('Emails Received') + ->sortable(), + Tables\Columns\TextColumn::make('updated_at') + ->label('Last Activity At') + ->dateTime() + ->sortable(), + ]) + ->filters([ + // + ]) + ->headerActions([ + //Tables\Actions\CreateAction::make(), + ]) + ->actions([ + //Tables\Actions\EditAction::make(), + //Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + //Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Widgets/StatsOverview.php b/app/Filament/Widgets/StatsOverview.php index 930eddc..792cb0e 100644 --- a/app/Filament/Widgets/StatsOverview.php +++ b/app/Filament/Widgets/StatsOverview.php @@ -7,6 +7,7 @@ use App\Models\Meta; use App\Models\PremiumEmail; use App\Models\Ticket; use App\Models\User; +use Carbon\Carbon; use DB; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; @@ -16,55 +17,137 @@ class StatsOverview extends BaseWidget protected function getStats(): array { return [ - Stat::make('Total Users', $this->getUser()), - Stat::make('Customers', $this->getCustomerCount()), - Stat::make('Paid Users', $this->getUserPaid()), - Stat::make('Logs Count', $this->getLogsCount()), - Stat::make('Total Mailbox', $this->getTotalMailbox()), - Stat::make('Emails Received', $this->getTotalEmailsReceived()), - Stat::make('Emails Stored', $this->getStoreEmailsCount()), - Stat::make('Open Tickets', $this->getOpenTicketsCount()), - Stat::make('Closed Tickets', $this->getClosedTicketsCount()), + Stat::make('Total Users', $this->getUser()) + ->description($this->getComparisonDescription($this->getUser(), $this->getUser('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getUser(), $this->getUser('yesterday'))) + ->color($this->getComparisonColor($this->getUser(), $this->getUser('yesterday'))), + Stat::make('Customers', $this->getCustomerCount()) + ->description($this->getComparisonDescription($this->getCustomerCount(), $this->getCustomerCount('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getCustomerCount(), $this->getCustomerCount('yesterday'))) + ->color($this->getComparisonColor($this->getCustomerCount(), $this->getCustomerCount('yesterday'))), + Stat::make('Paid Users', $this->getUserPaid()) + ->description($this->getComparisonDescription($this->getUserPaid(), $this->getUserPaid('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getUserPaid(), $this->getUserPaid('yesterday'))) + ->color($this->getComparisonColor($this->getUserPaid(), $this->getUserPaid('yesterday'))), + Stat::make('Logs Count', $this->getLogsCount()) + ->description($this->getComparisonDescription($this->getLogsCount(), $this->getLogsCount('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getLogsCount(), $this->getLogsCount('yesterday'))) + ->color($this->getComparisonColor($this->getLogsCount(), $this->getLogsCount('yesterday'))), + Stat::make('Total Mailbox', $this->getTotalMailbox()) + ->description($this->getComparisonDescription($this->getTotalMailbox(), $this->getTotalMailbox('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getTotalMailbox(), $this->getTotalMailbox('yesterday'))) + ->color($this->getComparisonColor($this->getTotalMailbox(), $this->getTotalMailbox('yesterday'))), + Stat::make('Emails Received', $this->getTotalEmailsReceived()) + ->description($this->getComparisonDescription($this->getTotalEmailsReceived(), $this->getTotalEmailsReceived('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getTotalEmailsReceived(), $this->getTotalEmailsReceived('yesterday'))) + ->color($this->getComparisonColor($this->getTotalEmailsReceived(), $this->getTotalEmailsReceived('yesterday'))), + Stat::make('Emails Stored', $this->getStoreEmailsCount()) + ->description($this->getComparisonDescription($this->getStoreEmailsCount(), $this->getStoreEmailsCount('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getStoreEmailsCount(), $this->getStoreEmailsCount('yesterday'))) + ->color($this->getComparisonColor($this->getStoreEmailsCount(), $this->getStoreEmailsCount('yesterday'))), + Stat::make('Open Tickets', $this->getOpenTicketsCount()) + ->description($this->getComparisonDescription($this->getOpenTicketsCount(), $this->getOpenTicketsCount('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getOpenTicketsCount(), $this->getOpenTicketsCount('yesterday'))) + ->color($this->getComparisonColor($this->getOpenTicketsCount(), $this->getOpenTicketsCount('yesterday'))), + Stat::make('Closed Tickets', $this->getClosedTicketsCount()) + ->description($this->getComparisonDescription($this->getClosedTicketsCount(), $this->getClosedTicketsCount('yesterday'))) + ->descriptionIcon($this->getComparisonIcon($this->getClosedTicketsCount(), $this->getClosedTicketsCount('yesterday'))) + ->color($this->getComparisonColor($this->getClosedTicketsCount(), $this->getClosedTicketsCount('yesterday'))), ]; } - private function getUser(): int + + private function getComparisonDescription(int $today, int $yesterday): string { - return User::all()->count(); + if ($today == $yesterday) { + return 'No change'; + } + + $difference = $today - $yesterday; + $percentage = $yesterday > 0 ? ($difference / $yesterday) * 100 : ($today > 0 ? 100 : 0); + + return sprintf( + 'Today: %d, Yesterday: %d (%s%.1f%%)', + $today, + $yesterday, + $difference >= 0 ? '+' : '-', + abs($percentage) + ); } - private function getUserPaid(): int + + private function getComparisonIcon(int $today, int $yesterday): ?string + { + if ($today == $yesterday) { + return null; + } + return $today > $yesterday ? 'heroicon-o-arrow-up' : 'heroicon-o-arrow-down'; + } + + private function getComparisonColor(int $today, int $yesterday): string + { + if ($today == $yesterday) { + return 'gray'; + } + return $today > $yesterday ? 'success' : 'danger'; + } + + private function getUser(string $period = 'today'): int + { + if ($period === 'yesterday') { + return User::where('created_at', '<', Carbon::today('UTC')->startOfDay())->count(); + } + return User::count(); + } + + private function getUserPaid(string $period = 'today'): int { return DB::table('subscriptions') - ->where(['stripe_status' => 'active']) + ->where('stripe_status', 'active') ->distinct('user_id') ->count('user_id'); } - private function getLogsCount(): int + + private function getLogsCount(string $period = 'today'): int { - return Log::all()->count(); - } - private function getTotalMailbox(): int - { - return Meta::select('value')->where(['key' => 'email_ids_created'])->first()->value; - } - private function getTotalEmailsReceived(): int - { - return Meta::select('value')->where(['key' => 'messages_received'])->first()->value; + if ($period === 'yesterday') { + return Log::where('created_at', '<', Carbon::today('UTC')->startOfDay())->count(); + } + return Log::count(); } - private function getCustomerCount(): int + private function getTotalMailbox(string $period = 'today'): int { + return Meta::select('value')->where('key', 'email_ids_created')->first()->value ?? 0; + } + + private function getTotalEmailsReceived(string $period = 'today'): int + { + return Meta::select('value')->where('key', 'messages_received')->first()->value ?? 0; + } + + private function getCustomerCount(string $period = 'today'): int + { + if ($period === 'yesterday') { + return User::whereNotNull('stripe_id') + ->where('created_at', '<', Carbon::today('UTC')->startOfDay()) + ->count(); + } return User::whereNotNull('stripe_id')->count(); } - private function getStoreEmailsCount(): int + private function getStoreEmailsCount(string $period = 'today'): int { - return PremiumEmail::all()->count(); + if ($period === 'yesterday') { + return PremiumEmail::where('created_at', '<', Carbon::today('UTC')->startOfDay())->count(); + } + return PremiumEmail::count(); } - private function getOpenTicketsCount(): int + + private function getOpenTicketsCount(string $period = 'today'): int { return Ticket::whereIn('status', ['open', 'pending'])->count(); } - private function getClosedTicketsCount(): int + + private function getClosedTicketsCount(string $period = 'today'): int { return Ticket::whereIn('status', ['closed'])->count(); } diff --git a/app/Models/User.php b/app/Models/User.php index 9537d7b..84dd8ba 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -69,4 +69,14 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail { return $this->hasMany(Ticket::class); } + + public function logs() + { + return $this->hasMany(Log::class); + } + + public function usageLogs() + { + return $this->hasMany(UsageLog::class); + } }