user()->can('View:SmsLogsPage'); } public static function getNavigationIcon(): \BackedEnum|string|null { return 'heroicon-o-chat-bubble-left-ellipsis'; } public static function getNavigationGroup(): ?string { return __('navigation.groups.settings'); } public static function getNavigationLabel(): string { return __('navigation.sms_logs.label'); } public function getTitle(): string { return __('navigation.sms_logs.title'); } public array $stats = ['patients' => 0, 'total' => 0, 'sent' => 0, 'pending' => 0, 'failed' => 0]; public function table(Table $table): Table { return $table ->records(fn (array $filters, ?string $search, int $page, int $recordsPerPage) => $this->fetchRecords($filters, $search, $page, $recordsPerPage) ) ->columns([ Tables\Columns\TextColumn::make('message_type') ->label('نوع پیام') ->getStateUsing(fn (array $record): string => $record['message_type'] ?? '') ->formatStateUsing(fn (string $state): string => match ($state) { 'initial' => 'پیام اولیه', 'reminder_7days' => 'یادآوری ۷ روز', 'reminder_3days' => 'یادآوری ۳ روز', 'reminder_after_registration' => 'یادآوری پس از ثبت', 'day_of_surgery' => 'روز عمل', default => $state, }) ->badge() ->color(fn (string $state): string => match ($state) { 'initial' => 'info', 'day_of_surgery' => 'success', 'reminder_3days' => 'pink', default => 'warning', }), Tables\Columns\TextColumn::make('status') ->label('وضعیت') ->getStateUsing(fn (array $record): string => $record['status'] ?? '') ->formatStateUsing(fn (string $state): string => match ($state) { 'sent' => 'ارسال شده', 'pending' => 'در انتظار', 'failed' => 'ناموفق', default => $state, }) ->badge() ->color(fn (string $state): string => match ($state) { 'sent' => 'success', 'pending' => 'warning', 'failed' => 'danger', default => 'gray', }), Tables\Columns\TextColumn::make('scheduled_at') ->label('زمان‌بندی شده') ->getStateUsing(fn (array $record): string => $this->toJalali($record['scheduled_at'] ?? null)), Tables\Columns\TextColumn::make('sent_at') ->label('زمان ارسال') ->getStateUsing(fn (array $record): string => $this->toJalali($record['sent_at'] ?? null)), ]) ->groups([ Group::make('patient_key') ->titlePrefixedWithLabel(false) ->getTitleFromRecordUsing(fn (array $record): string => $record['patient_name'] ?? '-') ->getDescriptionFromRecordUsing(fn (array $record): string => ($record['phone_number'] ?? '') . ($record['surgery_date'] ? ' | تاریخ عمل: ' . $this->toJalali($record['surgery_date'], 'Y/m/d') : '') ) ->collapsible(), ]) ->defaultGroup('patient_key') ->collapsedGroupsByDefault() ->groupingSettingsHidden() ->filters([ Tables\Filters\SelectFilter::make('status') ->label('وضعیت') ->options([ 'sent' => 'ارسال شده', 'pending' => 'در انتظار', 'failed' => 'ناموفق', ]), Tables\Filters\Filter::make('patient_name') ->label('نام بیمار') ->form([ TextInput::make('patient_name') ->label('نام بیمار') ->placeholder('جستجو...'), ]) ->indicateUsing(fn (array $data): ?string => filled($data['patient_name'] ?? null) ? 'نام: ' . $data['patient_name'] : null), Tables\Filters\Filter::make('phone_number') ->label('شماره موبایل') ->form([ TextInput::make('phone_number') ->label('شماره موبایل') ->placeholder('09...'), ]) ->indicateUsing(fn (array $data): ?string => filled($data['phone_number'] ?? null) ? 'موبایل: ' . $data['phone_number'] : null), ]) ->recordActions([ FilamentAction::make('delete_message') ->label('حذف پیام') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->modalHeading('حذف پیام') ->modalDescription('آیا از حذف این پیام مطمئن هستید؟') ->modalSubmitActionLabel('بله، حذف شود') ->action(fn (array $record) => $this->deleteSingleMessage((int) $record['id'])), FilamentAction::make('delete_patient') ->label('حذف همه پیام‌های بیمار') ->icon('heroicon-o-user-minus') ->color('danger') ->outlined() ->requiresConfirmation() ->modalHeading('حذف همه پیام‌های بیمار') ->modalDescription('آیا از حذف تمام پیام‌های این بیمار مطمئن هستید؟') ->modalSubmitActionLabel('بله، حذف شود') ->action(fn (array $record) => $this->deletePatientMessages( $record['phone_number'], $record['surgery_date'] )), ]) ->paginationPageOptions([5, 10, 25, 50]); } protected function fetchRecords( array $filters, ?string $search, int $page, int $recordsPerPage, ): LengthAwarePaginator { $query = array_filter([ 'status' => $filters['status']['value'] ?? null, 'patientName' => $filters['patient_name']['patient_name'] ?? $search, 'phoneNumber' => $filters['phone_number']['phone_number'] ?? null, 'limit' => 500, ]); try { $response = Http::timeout(15) ->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'))) ->get(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs-grouped', $query); if ($response->failed() || ! ($response->json('success') ?? false)) { \Illuminate\Support\Facades\Log::warning('SmsLogsPage: API returned failure', [ 'status' => $response->status(), 'body' => $response->body(), 'api_url' => \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')), ]); Notification::make() ->title('خطا در دریافت لاگ‌های پیامک') ->body('کد: ' . $response->status() . ' | ' . ($response->json('message') ?? $response->body())) ->danger() ->send(); return new LengthAwarePaginator([], 0, $recordsPerPage, $page); } $grouped = $response->json('data') ?? []; $this->recalcStats($grouped); $totalPatients = count($grouped); $patientsOnPage = array_slice($grouped, ($page - 1) * $recordsPerPage, $recordsPerPage); $rows = []; foreach ($patientsOnPage as $patient) { $patientKey = ($patient['phone_number'] ?? '') . '|' . ($patient['surgery_date'] ?? ''); foreach ($patient['messages'] ?? [] as $msg) { $rows[] = [ 'id' => $msg['id'], 'patient_key' => $patientKey, 'patient_name' => $patient['patient_name'] ?? '-', 'phone_number' => $patient['phone_number'] ?? '', 'surgery_date' => $patient['surgery_date'] ?? '', 'message_type' => $msg['message_type'] ?? '', 'status' => $msg['status'] ?? '', 'scheduled_at' => $msg['scheduled_at'] ?? null, 'sent_at' => $msg['sent_at'] ?? null, ]; } } return new LengthAwarePaginator( items: $rows, total: $totalPatients, perPage: $recordsPerPage, currentPage: $page, ); } catch (\Throwable $e) { \Illuminate\Support\Facades\Log::error('SmsLogsPage: API exception', [ 'error' => $e->getMessage(), 'api_url' => \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')), ]); Notification::make() ->title('خطا در اتصال به سرویس پیامک') ->body($e->getMessage()) ->danger() ->send(); return new LengthAwarePaginator([], 0, $recordsPerPage, $page); } } public function deleteSingleMessage(int $id): void { try { $response = Http::timeout(10) ->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'))) ->delete(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs/' . $id); if ($response->json('success') ?? false) { Notification::make()->title('پیام حذف شد')->success()->send(); $this->resetTable(); } else { Notification::make()->title($response->json('message') ?? 'خطا در حذف پیام')->danger()->send(); } } catch (\Throwable $e) { Notification::make()->title('خطا: ' . $e->getMessage())->danger()->send(); } } public function deletePatientMessages(string $phoneNumber, string $surgeryDate): void { try { $response = Http::timeout(10) ->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'))) ->post(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs/delete-patient', [ 'phoneNumber' => $phoneNumber, 'surgeryDate' => $surgeryDate, ]); if ($response->json('success') ?? false) { Notification::make()->title('همه پیام‌های بیمار حذف شد')->success()->send(); $this->resetTable(); } else { Notification::make()->title($response->json('message') ?? 'خطا در حذف پیام‌ها')->danger()->send(); } } catch (\Throwable $e) { Notification::make()->title('خطا: ' . $e->getMessage())->danger()->send(); } } public static function cancelAppointmentMessages(string $phoneNumber, string $localSurgeryDate): void { try { $apiKey = \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key')); $apiUrl = \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')); $localDate = \Carbon\Carbon::parse($localSurgeryDate)->format('Y-m-d'); $grouped = Http::timeout(10) ->withHeader('X-API-Key', $apiKey) ->get($apiUrl . '/sms-logs-grouped', [ 'phoneNumber' => $phoneNumber, 'limit' => 200, ]) ->json('data') ?? []; $serverSurgeryDate = null; foreach ($grouped as $group) { if (($group['phone_number'] ?? '') !== $phoneNumber) { continue; } $groupDate = $group['surgery_date'] ?? ''; if (! $groupDate) { continue; } try { if (\Carbon\Carbon::parse($groupDate)->format('Y-m-d') === $localDate) { $serverSurgeryDate = $groupDate; break; } } catch (\Throwable) {} } if (! $serverSurgeryDate) { return; } Http::timeout(10) ->withHeader('X-API-Key', $apiKey) ->asJson() ->post($apiUrl . '/sms-logs/delete-patient', [ 'phoneNumber' => $phoneNumber, 'surgeryDate' => $serverSurgeryDate, ]); } catch (\Throwable) { } } public function toJalali(?string $dateStr, string $format = 'Y/m/d H:i'): string { if (! $dateStr) { return '-'; } try { return Jalalian::fromCarbon( Carbon::parse($dateStr, 'UTC')->setTimezone('Asia/Tehran') )->format($format); } catch (\Throwable) { return $dateStr; } } private function recalcStats(array $grouped): void { $total = $sent = $pending = $failed = 0; foreach ($grouped as $p) { $total += $p['stats']['total'] ?? 0; $sent += $p['stats']['sent'] ?? 0; $pending += $p['stats']['pending'] ?? 0; $failed += $p['stats']['failed'] ?? 0; } $this->stats = [ 'patients' => count($grouped), 'total' => $total, 'sent' => $sent, 'pending' => $pending, 'failed' => $failed, ]; } }