|null */ public ?array $data = []; public static function getNavigationLabel(): string { return __('filament.send_sms.navigation'); } public function getTitle(): string { return __('filament.send_sms.title'); } public function mount(): void { $this->form->fill([ 'message' => '', 'send_to_all' => true, 'coupon_ids' => [], 'excluded_coupon_ids' => [], ]); } public function defaultForm(Schema $schema): Schema { return $schema ->statePath('data') ->columns(1); } public function form(Schema $schema): Schema { return $schema ->components([ Section::make(__('filament.send_sms.sections.message')) ->schema([ Textarea::make('message') ->label(__('filament.send_sms.fields.message')) ->required() ->rows(8) ->maxLength(1000) ->live(debounce: 300) ->helperText(fn (Get $get): string => app(SmsMessageAnalyzer::class)->summary((string) ($get('message') ?? ''))), ]), Section::make(__('filament.send_sms.sections.recipients')) ->description(__('filament.send_sms.sections.recipients_description')) ->schema([ Checkbox::make('send_to_all') ->label(__('filament.send_sms.fields.send_to_all')) ->helperText(__('filament.send_sms.fields.send_to_all_helper')) ->live(), Select::make('coupon_ids') ->label(__('filament.send_sms.fields.coupon_ids')) ->multiple() ->searchable() ->disabled(fn (Get $get): bool => (bool) $get('send_to_all')) ->dehydrated(fn (Get $get): bool => ! (bool) $get('send_to_all')) ->helperText(__('filament.send_sms.fields.coupon_ids_helper')) ->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search)) ->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)), Select::make('excluded_coupon_ids') ->label(__('filament.send_sms.fields.excluded_coupon_ids')) ->multiple() ->searchable() ->helperText(__('filament.send_sms.fields.excluded_coupon_ids_helper')) ->getSearchResultsUsing(fn (string $search): array => $this->searchCoupons($search)) ->getOptionLabelsUsing(fn (array $values): array => $this->couponLabels($values)), ]), ]); } public function content(Schema $schema): Schema { return $schema ->components([ Form::make([EmbeddedSchema::make('form')]) ->id('send-sms-form') ->livewireSubmitHandler('sendCampaign') ->footer([ Actions::make([ Action::make('send') ->label(__('filament.send_sms.actions.send')) ->icon(Heroicon::OutlinedPaperAirplane) ->requiresConfirmation() ->modalHeading(__('filament.send_sms.actions.send_confirm_heading')) ->modalDescription(fn (): string => $this->confirmationDescription()) ->modalSubmitActionLabel(__('filament.send_sms.actions.send_confirm_submit')) ->action('sendCampaign'), ]), ]), ]); } public function sendCampaign(): void { $data = $this->form->getState(); $validator = Validator::make($data, [ 'message' => ['required', 'string', 'max:1000'], 'send_to_all' => ['required', 'boolean'], 'coupon_ids' => [ Rule::requiredIf(fn (): bool => ! ($data['send_to_all'] ?? false)), 'array', ], 'coupon_ids.*' => ['integer', 'exists:coupons,id'], 'excluded_coupon_ids' => ['nullable', 'array'], 'excluded_coupon_ids.*' => ['integer', 'exists:coupons,id'], ]); $validator->validate(); $sendToAll = (bool) $data['send_to_all']; $selectedIds = array_map('intval', $data['coupon_ids'] ?? []); $excludedIds = array_map('intval', $data['excluded_coupon_ids'] ?? []); $service = app(SmsBroadcastService::class); $willReceive = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds) ->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone)) ->count(); if ($willReceive === 0) { Notification::make() ->title(__('filament.send_sms.notifications.no_valid_recipients')) ->body(__('filament.send_sms.notifications.no_valid_recipients_body')) ->danger() ->send(); return; } $campaign = $service->startCampaign( admin: auth()->user(), message: $data['message'], sendToAll: $sendToAll, selectedIds: $selectedIds, excludedIds: $excludedIds, ); if ($campaign->isComplete()) { $this->notifyCampaignFinished($campaign); } else { Notification::make() ->title(__('filament.send_sms.notifications.queued')) ->body(__('filament.send_sms.notifications.queued_body', ['count' => $campaign->recipient_count])) ->success() ->send(); } } protected function notifyCampaignFinished(SmsCampaign $campaign): void { $campaign->refresh(); if ($campaign->failed_count > 0 && $campaign->sent_count === 0) { Notification::make() ->title(__('filament.send_sms.notifications.failed')) ->body(__('filament.send_sms.notifications.failed_body', ['count' => $campaign->failed_count])) ->danger() ->send(); } elseif ($campaign->failed_count > 0) { Notification::make() ->title(__('filament.send_sms.notifications.finished_with_errors')) ->body(__('filament.send_sms.notifications.finished_with_errors_body', [ 'sent' => $campaign->sent_count, 'failed' => $campaign->failed_count, 'skipped' => $campaign->skipped_count, ])) ->warning() ->send(); } else { Notification::make() ->title(__('filament.send_sms.notifications.completed')) ->body(__('filament.send_sms.notifications.completed_body', ['count' => $campaign->sent_count])) ->success() ->send(); } } protected function confirmationDescription(): string { $message = (string) ($this->data['message'] ?? ''); $analyzer = app(SmsMessageAnalyzer::class); $willReceive = $this->getWillReceiveCount(); return __('filament.send_sms.confirmation', [ 'count' => $willReceive, 'summary' => $analyzer->summary($message), ]); } public function getWillReceiveCount(): int { $data = $this->data ?? []; return app(SmsBroadcastService::class) ->resolveRecipients( (bool) ($data['send_to_all'] ?? false), array_map('intval', $data['coupon_ids'] ?? []), array_map('intval', $data['excluded_coupon_ids'] ?? []), ) ->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone)) ->count(); } /** * @return array */ protected function searchCoupons(string $search): array { $search = trim($search); if ($search === '') { return []; } return Coupon::query() ->where(function ($query) use ($search): void { $query->where('phone', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%"); if (ctype_digit($search)) { $query->orWhere('id', (int) $search); } }) ->orderBy('id') ->limit(50) ->get() ->mapWithKeys(fn (Coupon $coupon): array => [ $coupon->id => $this->couponLabel($coupon), ]) ->all(); } /** * @param array $values * @return array */ protected function couponLabels(array $values): array { if ($values === []) { return []; } return Coupon::query() ->whereIn('id', $values) ->get() ->mapWithKeys(fn (Coupon $coupon): array => [ $coupon->id => $this->couponLabel($coupon), ]) ->all(); } protected function couponLabel(Coupon $coupon): string { return format_phone($coupon->phone); } }