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

385 lines
16 KiB
PHP
Raw Normal View History

<?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,
];
}
}