From 8f6ee1a7bf6438b5aecfbb0fc50e0b31adeb611f Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Sat, 20 Dec 2025 16:38:46 -0500 Subject: [PATCH] Yes, add cancelled, pending, and suspended service resources to admin panel --- .../Services/Pages/ServicesOverview.php | 2 + .../Resources/CancelledServiceResource.php | 217 ++++++++++++++++++ .../Pages/CreateCancelledService.php | 18 ++ .../Pages/EditCancelledService.php | 19 ++ .../Pages/ListCancelledServices.php | 19 ++ .../Resources/PendingServiceResource.php | 217 ++++++++++++++++++ .../Pages/CreatePendingService.php | 18 ++ .../Pages/EditPendingService.php | 19 ++ .../Pages/ListPendingServices.php | 19 ++ app/Admin/Resources/ServiceResource.php | 4 + .../ServiceResource/Pages/ListService.php | 37 --- .../Resources/SuspendedServiceResource.php | 217 ++++++++++++++++++ .../Pages/CreateSuspendedService.php | 18 ++ .../Pages/EditSuspendedService.php | 19 ++ .../Pages/ListSuspendedServices.php | 19 ++ app/Providers/Filament/AdminPanelProvider.php | 1 + .../pages/services-overview.blade.php | 8 +- 17 files changed, 830 insertions(+), 41 deletions(-) create mode 100644 app/Admin/Resources/CancelledServiceResource.php create mode 100644 app/Admin/Resources/CancelledServiceResource/Pages/CreateCancelledService.php create mode 100644 app/Admin/Resources/CancelledServiceResource/Pages/EditCancelledService.php create mode 100644 app/Admin/Resources/CancelledServiceResource/Pages/ListCancelledServices.php create mode 100644 app/Admin/Resources/PendingServiceResource.php create mode 100644 app/Admin/Resources/PendingServiceResource/Pages/CreatePendingService.php create mode 100644 app/Admin/Resources/PendingServiceResource/Pages/EditPendingService.php create mode 100644 app/Admin/Resources/PendingServiceResource/Pages/ListPendingServices.php create mode 100644 app/Admin/Resources/SuspendedServiceResource.php create mode 100644 app/Admin/Resources/SuspendedServiceResource/Pages/CreateSuspendedService.php create mode 100644 app/Admin/Resources/SuspendedServiceResource/Pages/EditSuspendedService.php create mode 100644 app/Admin/Resources/SuspendedServiceResource/Pages/ListSuspendedServices.php diff --git a/app/Admin/Clusters/Services/Pages/ServicesOverview.php b/app/Admin/Clusters/Services/Pages/ServicesOverview.php index c37395756..a2e021501 100644 --- a/app/Admin/Clusters/Services/Pages/ServicesOverview.php +++ b/app/Admin/Clusters/Services/Pages/ServicesOverview.php @@ -14,6 +14,8 @@ class ServicesOverview extends Page protected static ?string $navigationLabel = 'Overview'; + protected static ?string $slug = '/'; + protected static ?int $navigationSort = -1; protected static ?string $cluster = Services::class; diff --git a/app/Admin/Resources/CancelledServiceResource.php b/app/Admin/Resources/CancelledServiceResource.php new file mode 100644 index 000000000..1446cd7b3 --- /dev/null +++ b/app/Admin/Resources/CancelledServiceResource.php @@ -0,0 +1,217 @@ +count() ?: null; + } + + public static function getNavigationBadgeColor(): ?string + { + return 'danger'; + } + + protected static ?string $cluster = Services::class; + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery()->where('status', 'cancelled'); + } + + 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('cancelled'), + 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('danger') + ->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\CancelledServiceResource\Pages\ListCancelledServices::route('/'), + 'create' => \App\Admin\Resources\CancelledServiceResource\Pages\CreateCancelledService::route('/create'), + 'edit' => \App\Admin\Resources\CancelledServiceResource\Pages\EditCancelledService::route('/{record}/edit'), + ]; + } +} diff --git a/app/Admin/Resources/CancelledServiceResource/Pages/CreateCancelledService.php b/app/Admin/Resources/CancelledServiceResource/Pages/CreateCancelledService.php new file mode 100644 index 000000000..ea9e80bc5 --- /dev/null +++ b/app/Admin/Resources/CancelledServiceResource/Pages/CreateCancelledService.php @@ -0,0 +1,18 @@ +count() ?: null; + } + + public static function getNavigationBadgeColor(): ?string + { + return 'warning'; + } + + protected static ?string $cluster = Services::class; + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery()->where('status', 'pending'); + } + + 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('pending'), + 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('gray') + ->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\PendingServiceResource\Pages\ListPendingServices::route('/'), + 'create' => \App\Admin\Resources\PendingServiceResource\Pages\CreatePendingService::route('/create'), + 'edit' => \App\Admin\Resources\PendingServiceResource\Pages\EditPendingService::route('/{record}/edit'), + ]; + } +} diff --git a/app/Admin/Resources/PendingServiceResource/Pages/CreatePendingService.php b/app/Admin/Resources/PendingServiceResource/Pages/CreatePendingService.php new file mode 100644 index 000000000..c8215d422 --- /dev/null +++ b/app/Admin/Resources/PendingServiceResource/Pages/CreatePendingService.php @@ -0,0 +1,18 @@ +count() ?: null; diff --git a/app/Admin/Resources/ServiceResource/Pages/ListService.php b/app/Admin/Resources/ServiceResource/Pages/ListService.php index 794d68a52..78a0764d2 100644 --- a/app/Admin/Resources/ServiceResource/Pages/ListService.php +++ b/app/Admin/Resources/ServiceResource/Pages/ListService.php @@ -3,11 +3,8 @@ namespace App\Admin\Resources\ServiceResource\Pages; use App\Admin\Resources\ServiceResource; -use App\Models\Service; use Filament\Actions; -use Filament\Resources\Components\Tab; use Filament\Resources\Pages\ListRecords; -use Illuminate\Database\Eloquent\Builder; class ListService extends ListRecords { @@ -19,38 +16,4 @@ class ListService extends ListRecords Actions\CreateAction::make(), ]; } - - public function getTabs(): array - { - return [ - 'all' => Tab::make('All Services') - ->icon('ri-apps-line') - ->badge(Service::count()), - 'active' => Tab::make('Active') - ->icon('ri-checkbox-circle-line') - ->badgeColor('success') - ->modifyQueryUsing(fn (Builder $query) => $query->where('status', 'active')) - ->badge(Service::where('status', 'active')->count()), - 'pending' => Tab::make('Pending') - ->icon('ri-time-line') - ->badgeColor('warning') - ->modifyQueryUsing(fn (Builder $query) => $query->where('status', 'pending')) - ->badge(Service::where('status', 'pending')->count()), - 'suspended' => Tab::make('Suspended') - ->icon('ri-pause-circle-line') - ->badgeColor('warning') - ->modifyQueryUsing(fn (Builder $query) => $query->where('status', 'suspended')) - ->badge(Service::where('status', 'suspended')->count()), - 'cancelled' => Tab::make('Cancelled') - ->icon('ri-close-circle-line') - ->badgeColor('danger') - ->modifyQueryUsing(fn (Builder $query) => $query->where('status', 'cancelled')) - ->badge(Service::where('status', 'cancelled')->count()), - ]; - } - - public function getDefaultActiveTab(): string|int|null - { - return 'active'; - } } diff --git a/app/Admin/Resources/SuspendedServiceResource.php b/app/Admin/Resources/SuspendedServiceResource.php new file mode 100644 index 000000000..317b7d830 --- /dev/null +++ b/app/Admin/Resources/SuspendedServiceResource.php @@ -0,0 +1,217 @@ +count() ?: null; + } + + public static function getNavigationBadgeColor(): ?string + { + return 'warning'; + } + + protected static ?string $cluster = Services::class; + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery()->where('status', 'suspended'); + } + + 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('suspended'), + 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('warning') + ->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\SuspendedServiceResource\Pages\ListSuspendedServices::route('/'), + 'create' => \App\Admin\Resources\SuspendedServiceResource\Pages\CreateSuspendedService::route('/create'), + 'edit' => \App\Admin\Resources\SuspendedServiceResource\Pages\EditSuspendedService::route('/{record}/edit'), + ]; + } +} diff --git a/app/Admin/Resources/SuspendedServiceResource/Pages/CreateSuspendedService.php b/app/Admin/Resources/SuspendedServiceResource/Pages/CreateSuspendedService.php new file mode 100644 index 000000000..6e28285cd --- /dev/null +++ b/app/Admin/Resources/SuspendedServiceResource/Pages/CreateSuspendedService.php @@ -0,0 +1,18 @@ +favicon(config('settings.logo') ? Storage::url(config('settings.logo')) : null) ->discoverResources(in: app_path('Admin/Resources'), for: 'App\\Admin\\Resources') ->discoverPages(in: app_path('Admin/Pages'), for: 'App\\Admin\\Pages') + ->discoverPages(in: app_path('Admin/Clusters/Services/Pages'), for: 'App\\Admin\\Clusters\\Services\\Pages') ->discoverClusters(in: app_path('Admin/Clusters'), for: 'App\\Admin\\Clusters') ->userMenuItems([ MenuItem::make() diff --git a/resources/views/admin/clusters/services/pages/services-overview.blade.php b/resources/views/admin/clusters/services/pages/services-overview.blade.php index 6935c09e4..f91ff19f5 100644 --- a/resources/views/admin/clusters/services/pages/services-overview.blade.php +++ b/resources/views/admin/clusters/services/pages/services-overview.blade.php @@ -1,7 +1,7 @@
{{-- Active Services Card --}} - +
@@ -16,7 +16,7 @@ {{-- Pending Services Card --}} - +
@@ -31,7 +31,7 @@ {{-- Suspended Services Card --}} - +
@@ -46,7 +46,7 @@ {{-- Cancelled Services Card --}} - +