Files
daragt-coupon/app/Filament/Pages/SendSms.php
Mekan1206 8e66883587 WIP
2026-06-03 22:16:10 +05:00

300 lines
11 KiB
PHP

<?php
namespace App\Filament\Pages;
use App\Models\Coupon;
use App\Models\SmsCampaign;
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;
}
$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<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);
}
}