267 lines
9.3 KiB
PHP
267 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Jobs\StartSmsCampaignJob;
|
|
use App\Models\Coupon;
|
|
use App\Services\SmsBroadcastService;
|
|
use App\Services\SmsMessageAnalyzer;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Checkbox;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Filament\Schemas\Components\Actions;
|
|
use Filament\Schemas\Components\EmbeddedSchema;
|
|
use Filament\Schemas\Components\Form;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Components\Utilities\Get;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Support\Icons\Heroicon;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
/**
|
|
* @property-read Schema $form
|
|
*/
|
|
class SendSms extends Page
|
|
{
|
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedChatBubbleLeftRight;
|
|
|
|
protected static ?string $navigationLabel = null;
|
|
|
|
protected static ?string $title = null;
|
|
|
|
protected static ?int $navigationSort = 2;
|
|
|
|
protected string $view = 'filament.pages.send-sms';
|
|
|
|
/**
|
|
* @var array<string, mixed>|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;
|
|
}
|
|
|
|
StartSmsCampaignJob::dispatch(
|
|
adminId: auth()->id(),
|
|
message: $data['message'],
|
|
sendToAll: $sendToAll,
|
|
selectedIds: $selectedIds,
|
|
excludedIds: $excludedIds,
|
|
);
|
|
|
|
Notification::make()
|
|
->title(__('filament.send_sms.notifications.submitted'))
|
|
->body(__('filament.send_sms.notifications.submitted_body', ['count' => $willReceive]))
|
|
->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<int, string>
|
|
*/
|
|
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<int, int|string> $values
|
|
* @return array<int, string>
|
|
*/
|
|
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);
|
|
}
|
|
}
|