From 8a108e960465668a548f3110d47bcd34f7ef69ce Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Sat, 20 Dec 2025 16:31:06 -0500 Subject: [PATCH] feat: implement services overview page with service status cards and counts --- .../Services/Pages/ServicesOverview.php | 33 +++ app/Admin/Resources/ActiveServiceResource.php | 217 ++++++++++++++++++ .../Pages/CreateActiveService.php | 18 ++ .../Pages/EditActiveService.php | 19 ++ .../Pages/ListActiveServices.php | 19 ++ .../pages/services-overview.blade.php | 81 +++++++ 6 files changed, 387 insertions(+) create mode 100644 app/Admin/Clusters/Services/Pages/ServicesOverview.php create mode 100644 app/Admin/Resources/ActiveServiceResource.php create mode 100644 app/Admin/Resources/ActiveServiceResource/Pages/CreateActiveService.php create mode 100644 app/Admin/Resources/ActiveServiceResource/Pages/EditActiveService.php create mode 100644 app/Admin/Resources/ActiveServiceResource/Pages/ListActiveServices.php create mode 100644 resources/views/admin/clusters/services/pages/services-overview.blade.php diff --git a/app/Admin/Clusters/Services/Pages/ServicesOverview.php b/app/Admin/Clusters/Services/Pages/ServicesOverview.php new file mode 100644 index 000000000..c37395756 --- /dev/null +++ b/app/Admin/Clusters/Services/Pages/ServicesOverview.php @@ -0,0 +1,33 @@ + Service::where('status', 'active')->count(), + 'pendingCount' => Service::where('status', 'pending')->count(), + 'suspendedCount' => Service::where('status', 'suspended')->count(), + 'cancelledCount' => Service::where('status', 'cancelled')->count(), + 'totalCount' => Service::count(), + ]; + } +} diff --git a/app/Admin/Resources/ActiveServiceResource.php b/app/Admin/Resources/ActiveServiceResource.php new file mode 100644 index 000000000..c2928f846 --- /dev/null +++ b/app/Admin/Resources/ActiveServiceResource.php @@ -0,0 +1,217 @@ +count() ?: null; + } + + public static function getNavigationBadgeColor(): ?string + { + return 'success'; + } + + protected static ?string $cluster = Services::class; + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery()->where('status', 'active'); + } + + public static function form(Form $form): Form + { + return $form + ->schema([ + Forms\Components\Select::make('product_id') + ->label('Product') + ->required() + ->options(Product::all()->pluck('name', 'id')->toArray()) + ->searchable() + ->live() + ->preload() + ->placeholder('Select the product'), + Forms\Components\Select::make('plan_id') + ->label('Plan') + ->required() + ->relationship('plan', 'name', fn (Builder $query, Get $get) => $query->where('priceable_id', $get('product_id'))->where('priceable_type', Product::class)) + ->searchable() + ->preload() + ->disabled(fn (Get $get) => !$get('product_id')) + ->placeholder('Select the plan'), + UserComponent::make('user_id'), + Forms\Components\Select::make('status') + ->label('Status') + ->required() + ->options([ + 'active' => 'Active', + 'pending' => 'Pending', + 'suspended' => 'Suspended', + 'cancelled' => 'Cancelled', + ]) + ->default('active'), + Forms\Components\TextInput::make('quantity') + ->label('Quantity') + ->required() + ->placeholder('Enter the quantity'), + Forms\Components\DatePicker::make('expires_at') + ->label('Expires At') + ->required() + ->placeholder('Select the expiration date'), + Forms\Components\Select::make('coupon_id') + ->label('Coupon') + ->relationship('coupon', 'code') + ->searchable() + ->placeholder('Select the coupon'), + Forms\Components\Select::make('currency_code') + ->options(function (Get $get, ?string $state) { + $pricing = collect($get('../../pricing'))->pluck('currency_code'); + if ($state !== null) { + $pricing = $pricing->filter(function ($code) use ($state) { + return $code !== $state; + }); + } + $pricing = $pricing->filter(function ($code) { + return $code !== null; + }); + + return Currency::whereNotIn('code', $pricing)->pluck('code', 'code'); + }) + ->live() + ->default(config('settings.default_currency')) + ->required(), + Forms\Components\TextInput::make('price') + ->required() + ->label('Price') + ->prefix(fn (Get $get) => Currency::where('code', $get('currency_code'))->first()?->prefix) + ->suffix(fn (Get $get) => Currency::where('code', $get('currency_code'))->first()?->suffix) + ->live(onBlur: true) + ->mask(RawJs::make( + <<<'JS' + $money($input, '.', '', 2) + JS + )) + ->numeric() + ->minValue(0), + Forms\Components\TextInput::make('subscription_id') + ->label('Subscription ID') + ->nullable() + ->placeholder('Enter the subscription ID') + ->hintAction( + Action::make('Cancel Subscription ID') + ->action(function (Component $component) { + if (ExtensionHelper::cancelSubscription($component->getRecord())) { + Notification::make('Subscription Cancelled') + ->title('The subscription has been successfully cancelled') + ->success() + ->send(); + } else { + Notification::make('Subscription Not Cancelled') + ->title('The subscription could not be cancelled') + ->error() + ->send(); + } + $component->getRecord()->update(['subscription_id' => null]); + }) + ->requiresConfirmation() + ->label('Cancel Subscription') + ->hidden(fn (Component $component) => !$component->getRecord()?->subscription_id), + ), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('id') + ->label('ID') + ->sortable() + ->searchable(), + Tables\Columns\TextColumn::make('user.name') + ->label('User') + ->searchable(true, fn (Builder $query, string $search) => $query->whereHas('user', fn (Builder $query) => $query->where('first_name', 'like', "%$search%")->orWhere('last_name', 'like', "%$search%"))), + Tables\Columns\TextColumn::make('product.name') + ->label('Product') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('status') + ->badge() + ->color('success') + ->formatStateUsing(fn (string $state) => ucfirst($state)) + ->label('Status'), + Tables\Columns\TextColumn::make('expires_at') + ->label('Expires At') + ->date() + ->searchable() + ->sortable(), + ]) + ->defaultSort(function (Builder $query): Builder { + return $query->orderBy('id', 'desc'); + }) + ->actions([ + Tables\Actions\EditAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + public static function getRelations(): array + { + return [ + RelationManagers\InvoicesRelationManager::class, + PropertiesRelationManager::class, + RelationManagers\ConfigOptionsRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + 'index' => \App\Admin\Resources\ActiveServiceResource\Pages\ListActiveServices::route('/'), + 'create' => \App\Admin\Resources\ActiveServiceResource\Pages\CreateActiveService::route('/create'), + 'edit' => \App\Admin\Resources\ActiveServiceResource\Pages\EditActiveService::route('/{record}/edit'), + ]; + } +} diff --git a/app/Admin/Resources/ActiveServiceResource/Pages/CreateActiveService.php b/app/Admin/Resources/ActiveServiceResource/Pages/CreateActiveService.php new file mode 100644 index 000000000..0a92f4356 --- /dev/null +++ b/app/Admin/Resources/ActiveServiceResource/Pages/CreateActiveService.php @@ -0,0 +1,18 @@ + +
+ {{-- Active Services Card --}} + + +
+
+ +
+
+

Active Services

+

{{ number_format($activeCount) }}

+
+
+
+
+ + {{-- Pending Services Card --}} + + +
+
+ +
+
+

Pending Services

+

{{ number_format($pendingCount) }}

+
+
+
+
+ + {{-- Suspended Services Card --}} + + +
+
+ +
+
+

Suspended Services

+

{{ number_format($suspendedCount) }}

+
+
+
+
+ + {{-- Cancelled Services Card --}} + + +
+
+ +
+
+

Cancelled Services

+

{{ number_format($cancelledCount) }}

+
+
+
+
+
+ + {{-- Total Summary --}} + +
+
+
+ +
+
+

Total Services

+

{{ number_format($totalCount) }}

+
+
+
+

Select a service type from the navigation or cards above to view and manage services.

+
+
+
+