matab-panel/app/Filament/Pages/SmsLogsPage.php

385 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use Carbon\Carbon;
use Filament\Actions\Action as FilamentAction;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Enums\FontFamily;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Http;
use Morilog\Jalali\Jalalian;
class SmsLogsPage extends Page implements HasTable
{
use InteractsWithTable;
protected string $view = 'filament.pages.sms-logs';
protected static ?int $navigationSort = 10;
public static function canAccess(): bool
{
return auth()->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,
];
}
}