feat: initialize Dr-Panel project with patient file number support
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages\Auth;
|
||||
|
||||
use Filament\Auth\Pages\Login as BaseLogin;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use SensitiveParameter;
|
||||
|
||||
class Login extends BaseLogin
|
||||
{
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('username')
|
||||
->label('نام کاربری')
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->autocomplete('username')
|
||||
->autofocus()
|
||||
->validationMessages([
|
||||
'required' => 'وارد کردن نام کاربری الزامی است.',
|
||||
'max' => 'نام کاربری نباید بیشتر از ۲۵۵ کاراکتر باشد.',
|
||||
]),
|
||||
$this->getPasswordFormComponent()
|
||||
->validationMessages([
|
||||
'required' => 'وارد کردن رمز عبور الزامی است.',
|
||||
]),
|
||||
$this->getRememberFormComponent(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getCredentialsFromFormData(#[SensitiveParameter] array $data): array
|
||||
{
|
||||
return [
|
||||
'username' => $data['username'],
|
||||
'password' => $data['password'],
|
||||
];
|
||||
}
|
||||
|
||||
protected function throwFailureValidationException(): never
|
||||
{
|
||||
throw ValidationException::withMessages([
|
||||
'data.username' => __('filament-panels::auth/pages/login.messages.failed'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use Filament\Pages\Dashboard as BaseDashboard;
|
||||
use Illuminate\Contracts\Support\Htmlable;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
class Dashboard extends BaseDashboard
|
||||
{
|
||||
protected string $view = 'filament.pages.dashboard';
|
||||
|
||||
public function getTitle(): string|Htmlable
|
||||
{
|
||||
return __('navigation.dashboard.title');
|
||||
}
|
||||
|
||||
public function getHeading(): string|Htmlable
|
||||
{
|
||||
return new HtmlString('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\OsurgInitial;
|
||||
use App\Models\SyncLog;
|
||||
use App\Services\OsurgImportService;
|
||||
use App\Services\SyncService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Livewire\WithFileUploads;
|
||||
use Morilog\Jalali\Jalalian;
|
||||
|
||||
class SyncPage extends Page
|
||||
{
|
||||
use WithFileUploads;
|
||||
protected string $view = 'filament.pages.sync-page';
|
||||
|
||||
protected static ?int $navigationSort = 99;
|
||||
|
||||
public static function canAccess(): bool
|
||||
{
|
||||
return auth()->user()->can('View:SyncPage');
|
||||
}
|
||||
|
||||
public static function shouldRegisterNavigation(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getNavigationIcon(): \BackedEnum|string|null
|
||||
{
|
||||
return 'heroicon-o-arrows-right-left';
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('navigation.groups.system');
|
||||
}
|
||||
|
||||
public string $report = '';
|
||||
public string $importReport = '';
|
||||
public bool $importing = false;
|
||||
|
||||
public $sqlFile = null;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('navigation.sync.label');
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('navigation.sync.title');
|
||||
}
|
||||
|
||||
public function getPendingCount(): int
|
||||
{
|
||||
return SyncLog::changesSince(OsurgInitial::val('our_sync_cursor') ?: null)->count();
|
||||
}
|
||||
|
||||
public function getLastSyncTime(): string
|
||||
{
|
||||
$last = SyncLog::where('action', 'synced')->latest('id')->first();
|
||||
if (! $last) {
|
||||
return __('navigation.sync.never');
|
||||
}
|
||||
|
||||
return Jalalian::fromCarbon($last->datetime->timezone('Asia/Tehran'))->format('Y/m/d H:i:s');
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'sqlFile' => ['required', 'file', 'max:307200'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('sync')
|
||||
->label(__('navigation.sync.sync_with_peer'))
|
||||
->icon('heroicon-o-arrows-right-left')
|
||||
->color('primary')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('navigation.sync.confirm_heading'))
|
||||
->modalDescription(__('navigation.sync.confirm_description'))
|
||||
->modalSubmitActionLabel(__('navigation.sync.confirm_button'))
|
||||
->action('runSync'),
|
||||
|
||||
Action::make('backup')
|
||||
->label(__('navigation.backup.label'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('success')
|
||||
->action('downloadBackup'),
|
||||
];
|
||||
}
|
||||
|
||||
public function runSync(): void
|
||||
{
|
||||
$service = SyncService::fromConfig();
|
||||
|
||||
if (! $service->isConfigured()) {
|
||||
Notification::make()
|
||||
->title(__('navigation.sync.error_title'))
|
||||
->body(__('navigation.sync.error_not_configured'))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $service->sync();
|
||||
|
||||
$this->report = implode("\n", $result['report']);
|
||||
|
||||
Notification::make()
|
||||
->title($result['success'] ? __('navigation.sync.success') : __('navigation.sync.failed'))
|
||||
->body(__('navigation.sync.see_report'))
|
||||
->color($result['success'] ? 'success' : 'warning')
|
||||
->send();
|
||||
}
|
||||
|
||||
|
||||
public function startImport(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$this->importing = true;
|
||||
|
||||
try {
|
||||
@set_time_limit(600);
|
||||
|
||||
$tmpPath = $this->sqlFile->getRealPath();
|
||||
|
||||
$service = new OsurgImportService();
|
||||
$result = $service->import($tmpPath);
|
||||
|
||||
$this->importReport = implode("\n", $result['report']);
|
||||
|
||||
Notification::make()
|
||||
->title($result['success']
|
||||
? __('navigation.import.success')
|
||||
: __('navigation.import.failed'))
|
||||
->body(__('navigation.sync.see_report'))
|
||||
->color($result['success'] ? 'success' : 'danger')
|
||||
->send();
|
||||
} finally {
|
||||
$this->sqlFile = null;
|
||||
$this->importing = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function downloadBackup(): \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
{
|
||||
$data = [
|
||||
'exported_at' => now()->toDateTimeString(),
|
||||
'last_sync_id' => SyncLog::lastSyncId(),
|
||||
'changes' => SyncLog::all()->toArray(),
|
||||
];
|
||||
|
||||
$filename = 'matab-backup-' . now()->format('Y-m-d_H-i-s') . '.json';
|
||||
|
||||
return response()->streamDownload(function () use ($data) {
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
}, $filename, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user