components([ Grid::make(3)->schema([ TextInput::make('name') ->label('Plan Name') ->required() ->maxLength(255), TextInput::make('price') ->label('Price') ->numeric() ->prefix('$') ->required(), Select::make('billing_cycle_days') ->label('Billing Cycle') ->options([ 1 => 'Daily', 7 => 'Weekly', 30 => 'Monthly', 60 => 'Bi-Monthly', 90 => 'Quarterly', 180 => 'Semi-Annual', 365 => 'Yearly', ]) ->default(30) ->required(), ]), Grid::make(2)->schema([ TextInput::make('product_id') ->label('Product ID') ->required() ->helperText('External product identifier'), TextInput::make('pricing_id') ->label('Pricing ID') ->required() ->helperText('External pricing identifier'), ]), Textarea::make('description') ->label('Description') ->rows(3) ->maxLength(500), Grid::make(3)->schema([ Select::make('plan_tier_id') ->label('Plan Tier') ->options(PlanTier::pluck('name', 'id')) ->nullable() ->searchable() ->helperText('Optional tier classification'), Toggle::make('is_active') ->label('Active') ->default(true) ->helperText('Plan is available for new subscriptions'), TextInput::make('sort_order') ->label('Sort Order') ->numeric() ->default(0) ->helperText('Display order in pricing tables'), ]), Section::make('Legacy Settings') ->description('Legacy payment provider settings (will be migrated to new system)') ->collapsible() ->schema([ Grid::make(3)->schema([ Toggle::make('monthly_billing') ->label('Monthly Billing (Legacy)') ->helperText('Legacy monthly billing flag'), TextInput::make('mailbox_limit') ->label('Mailbox Limit') ->numeric() ->default(10) ->helperText('Maximum number of mailboxes'), TextInput::make('shoppy_product_id') ->label('Shoppy Product ID') ->nullable(), ]), ]), ]); } public static function table(Table $table): Table { return $table ->columns([ Tables\Columns\TextColumn::make('name') ->label('Plan Name') ->searchable() ->sortable(), Tables\Columns\TextColumn::make('planTier.name') ->label('Tier') ->badge() ->sortable() ->placeholder('No Tier'), Tables\Columns\TextColumn::make('price') ->label('Price') ->money('USD') ->sortable(), Tables\Columns\TextColumn::make('billing_cycle_days') ->label('Billing Cycle') ->badge() ->color(fn (int $state): string => match ($state) { 1 => 'success', // Daily - green 7 => 'info', // Weekly - blue 14 => 'primary', // Bi-weekly - primary blue 30 => 'warning', // Monthly - orange 60 => 'gray', // Bi-monthly - gray 90 => 'purple', // Quarterly - purple 180 => 'danger', // Semi-annually - red 365 => 'primary', // Annually - primary blue 730 => 'gray', // Biennially - gray default => 'gray' // Custom cycles - gray }) ->formatStateUsing(fn (int $state): string => match ($state) { 1 => 'Daily', 7 => 'Weekly', 14 => 'Bi-weekly', 30 => 'Monthly', 60 => 'Bi-monthly', 90 => 'Quarterly', 180 => 'Semi-annually', 365 => 'Annually', 730 => 'Biennially', default => "{$state} days" }) ->sortable(), Tables\Columns\IconColumn::make('is_active') ->label('Active') ->boolean() ->trueColor('success') ->falseColor('danger'), Tables\Columns\TextColumn::make('plan_providers_count') ->label('Providers') ->getStateUsing(fn ($record) => $record->plan_providers_count ?? 0) ->badge() ->color(fn ($record) => match (true) { $record->plan_providers_count === 0 => 'danger', $record->plan_providers_count === 1 => 'warning', $record->plan_providers_count >= 3 => 'success', default => 'info' }) ->sortable(), Tables\Columns\TextColumn::make('plan_feature_limits_count') ->label('Features') ->getStateUsing(fn ($record) => $record->plan_feature_limits_count ?? 0) ->badge() ->color(fn ($record) => match (true) { $record->plan_feature_limits_count === 0 => 'danger', $record->plan_feature_limits_count >= 5 => 'success', $record->plan_feature_limits_count >= 3 => 'info', default => 'warning' }) ->sortable(), Tables\Columns\TextColumn::make('sort_order') ->label('Order') ->sortable() ->alignCenter(), ]) ->filters([ Tables\Filters\SelectFilter::make('plan_tier_id') ->label('Tier') ->options(PlanTier::pluck('name', 'id')) ->searchable(), Tables\Filters\TernaryFilter::make('is_active') ->label('Active Status') ->placeholder('All plans') ->trueLabel('Active only') ->falseLabel('Inactive only'), Tables\Filters\SelectFilter::make('billing_cycle_days') ->label('Billing Cycle') ->options([ 1 => 'Daily', 7 => 'Weekly', 30 => 'Monthly', 60 => 'Bi-Monthly', 90 => 'Quarterly', 180 => 'Semi-Annual', 365 => 'Yearly', ]), Tables\Filters\Filter::make('has_providers') ->label('Has Payment Providers') ->query(fn (Builder $query): Builder => $query->whereHas('planProviders')) ->toggle(), ]) ->recordActions([ ViewAction::make(), EditAction::make(), DeleteAction::make() ->requiresConfirmation() ->before(function (DeleteAction $action, Plan $record) { // Prevent deletion if plan has any subscriptions (active, cancelled, etc.) if ($record->subscriptions()->exists()) { Log::warning('Attempted to delete plan with existing subscriptions', [ 'plan_id' => $record->id, 'plan_name' => $record->name, 'subscription_count' => $record->subscriptions()->count(), ]); // Show warning notification \Filament\Notifications\Notification::make() ->warning() ->title('Cannot Delete Plan') ->body("Plan '{$record->name}' has {$record->subscriptions()->count()} subscription(s). Please cancel or remove all subscriptions first.") ->persistent() ->send(); // Halt the deletion process $action->halt(); } }) ->action(function (Plan $record) { // This action will only run if not halted $record->delete(); // Show success notification \Filament\Notifications\Notification::make() ->success() ->title('Plan Deleted') ->body("Plan '{$record->name}' has been deleted successfully.") ->send(); }), ]) ->bulkActions([ BulkActionGroup::make([ DeleteBulkAction::make() ->requiresConfirmation() ->before(function (DeleteBulkAction $action, $records) { foreach ($records as $record) { if ($record->subscriptions()->exists()) { Log::warning('Attempted to bulk delete plan with existing subscriptions', [ 'plan_id' => $record->id, 'plan_name' => $record->name, 'subscription_count' => $record->subscriptions()->count(), ]); \Filament\Notifications\Notification::make() ->warning() ->title('Cannot Delete Plans') ->body("Plan '{$record->name}' has {$record->subscriptions()->count()} subscription(s). Please cancel or remove all subscriptions first.") ->persistent() ->send(); // Halt the bulk deletion process $action->halt(); return; } } }) ->action(function ($records) { foreach ($records as $record) { $record->delete(); } \Filament\Notifications\Notification::make() ->success() ->title('Plans Deleted') ->body(count($records).' plan(s) have been deleted successfully.') ->send(); }), ]), ]) ->emptyStateActions([ CreateAction::make(), ]) ->defaultSort('sort_order', 'asc') ->groups([ Tables\Grouping\Group::make('planTier.name') ->label('Tier') ->collapsible(), ]); } public static function getEloquentQuery(): Builder { return parent::getEloquentQuery() ->withCount([ 'planProviders', 'planFeatureLimits', ]); } public static function getRelations(): array { return [ // ]; } public static function getPages(): array { return [ 'index' => ListPlans::route('/'), 'create' => CreatePlan::route('/create'), 'edit' => EditPlan::route('/{record}/edit'), ]; } public static function getNavigationBadge(): ?string { return static::getModel()::active()->count(); } public static function getNavigationBadgeColor(): ?string { return 'success'; } }