|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); $recipients = $service->resolveRecipients($sendToAll, $selectedIds, $excludedIds) ->filter(fn (Coupon $coupon): bool => is_valid_coupon_phone($coupon->phone)); $willReceive = $recipients->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; } $messages = $recipients->map(fn (Coupon $coupon): array => [ 'phone' => '+993'.$coupon->phone, 'message' => $data['message'], ])->values()->all(); $success = sendBulkSMS($messages); if ($success) { Notification::make() ->title(__('filament.send_sms.notifications.submitted')) ->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive])) ->success() ->send(); } else { Notification::make() ->title(__('filament.send_sms.notifications.failed')) ->body(__('filament.send_sms.notifications.failed_body')) ->danger() ->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); } }