338 lines
14 KiB
PHP
338 lines
14 KiB
PHP
<?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 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,
|
||
];
|
||
}
|
||
}
|