From 721ee3094822afeaa9df6cc2fbcd9867c8766327 Mon Sep 17 00:00:00 2001 From: SajjadMahmoody <117562560+SajjadMahmoody@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:31:19 +0330 Subject: [PATCH] fix: enforce unique national ID validation and fix sync updated_at corruption --- app/Filament/Resources/PatientResource.php | 12 +- app/Livewire/SettingsDropdown.php | 75 ------------- app/Models/Visit.php | 1 + app/Observers/SyncObserver.php | 6 +- bootstrap/app.php | 1 + lang/en/patients.php | 1 + lang/fa/patients.php | 1 + .../livewire/settings-dropdown.blade.php | 105 +----------------- routes/web.php | 61 ++++++++-- 9 files changed, 77 insertions(+), 186 deletions(-) diff --git a/app/Filament/Resources/PatientResource.php b/app/Filament/Resources/PatientResource.php index 080599b..1e17623 100644 --- a/app/Filament/Resources/PatientResource.php +++ b/app/Filament/Resources/PatientResource.php @@ -195,7 +195,15 @@ public static function form(Schema $schema): Schema TextInput::make('icno') ->label(__('patients.fields.icno')) ->required() - ->maxLength(11), + ->maxLength(11) + ->unique( + table: Patient::class, + column: 'icno', + ignorable: fn ($record) => $record, + ) + ->validationMessages([ + 'unique' => __('patients.validation.icno_unique'), + ]), TextInput::make('hand_phone') ->label(__('patients.fields.hand_phone')) @@ -807,7 +815,7 @@ public static function table(Table $table): Table ->iconColor(fn ($state) => $state ? 'success' : 'gray') ->placeholder('-'), ]) - ->defaultSort('created_at', 'desc') + ->defaultSort('updated_at', 'desc') ->recordActions([ Action::make('surgeryAppointment') ->label(__('patients.actions.surgery_appointment')) diff --git a/app/Livewire/SettingsDropdown.php b/app/Livewire/SettingsDropdown.php index 4d8a9a2..5f59920 100644 --- a/app/Livewire/SettingsDropdown.php +++ b/app/Livewire/SettingsDropdown.php @@ -26,81 +26,6 @@ public function checkPeerStatus(): void $this->peerPath = $result['path']; $this->peerChecked = true; } - public function runBackup(): void - { - $dbPath = database_path('database.sqlite'); - - if (! file_exists($dbPath)) { - Notification::make() - ->title(__('navigation.backup.failed')) - ->danger() - ->send(); - return; - } - - $timestamp = now()->format('Y-m-d_H-i-s'); - $zipFilename = "matab-backup-{$timestamp}.zip"; - $zipPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipFilename; - - $zip = new \ZipArchive(); - if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { - Notification::make() - ->title(__('navigation.backup.failed')) - ->danger() - ->send(); - return; - } - - $noCompressExts = [ - 'jpg', 'jpeg', 'png', 'gif', 'webp', - 'mp4', 'mov', 'avi', 'mkv', 'webm', - 'mp3', 'wav', 'ogg', 'opus', 'm4a', - 'zip', 'gz', 'rar', '7z', - ]; - - $addFile = function (string $realPath, string $zipEntry) use ($zip, $noCompressExts): void { - $zip->addFile($realPath, $zipEntry); - $index = $zip->locateName($zipEntry); - $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); - if (in_array($ext, $noCompressExts, true)) { - $zip->setCompressionIndex($index, \ZipArchive::CM_STORE); - } else { - $zip->setCompressionIndex($index, \ZipArchive::CM_DEFLATE, 9); - } - }; - - $addFile($dbPath, 'database/database.sqlite'); - - $publicPath = storage_path('app/public'); - foreach (['files', 'initials'] as $folder) { - $folderPath = $publicPath . '/' . $folder; - if (! is_dir($folderPath)) { - continue; - } - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::LEAVES_ONLY - ); - foreach ($iterator as $file) { - if ($file->isFile()) { - $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($folderPath) + 1)); - $addFile($file->getPathname(), $folder . '/' . $relativePath); - } - } - } - - $zip->close(); - - session(['backup_tmp_path' => $zipPath, 'backup_tmp_name' => $zipFilename]); - - $this->dispatch('backup-ready', url: route('backup.download')); - - Notification::make() - ->title(__('navigation.backup.success')) - ->success() - ->send(); - } - public function runSync(): void { $service = SyncService::fromConfig(); diff --git a/app/Models/Visit.php b/app/Models/Visit.php index 38cc11c..8d80639 100644 --- a/app/Models/Visit.php +++ b/app/Models/Visit.php @@ -13,6 +13,7 @@ class Visit extends Model { use HasUserStamps; + protected $touches = ['patientRecord']; protected $fillable = [ 'patient', 'visit_date', diff --git a/app/Observers/SyncObserver.php b/app/Observers/SyncObserver.php index 449fd6d..c254881 100644 --- a/app/Observers/SyncObserver.php +++ b/app/Observers/SyncObserver.php @@ -38,8 +38,10 @@ public function updated(Model $model): void } $changedData = []; - foreach ($dirty as $field => $newValue) { - $changedData[$field] = $model->getAttribute($field); + foreach ($dirty as $field => $rawValue) { + $changedData[$field] = $rawValue instanceof \BackedEnum + ? $rawValue->value + : (is_object($rawValue) ? (string) $rawValue : $rawValue); } SyncLog::create([ diff --git a/bootstrap/app.php b/bootstrap/app.php index 49b842f..6cd424d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -13,6 +13,7 @@ ) ->withMiddleware(function (Middleware $middleware): void { $middleware->trustHosts(at: fn () => ['.*'], subdomains: false); + $middleware->encryptCookies(except: ['backup_token']); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/lang/en/patients.php b/lang/en/patients.php index fb17ab1..b112630 100644 --- a/lang/en/patients.php +++ b/lang/en/patients.php @@ -81,5 +81,6 @@ 'last_name_required' => 'Last name is required.', 'father_name_required' => 'Father name is required.', 'hand_phone_required' => 'Mobile number is required.', + 'icno_unique' => 'This national ID is already registered for another patient.', ], ]; diff --git a/lang/fa/patients.php b/lang/fa/patients.php index 74e635f..e2981f4 100644 --- a/lang/fa/patients.php +++ b/lang/fa/patients.php @@ -83,5 +83,6 @@ 'last_name_required' => 'وارد کردن نام خانوادگی الزامی است.', 'father_name_required' => 'وارد کردن نام پدر الزامی است.', 'hand_phone_required' => 'وارد کردن شماره موبایل الزامی است.', + 'icno_unique' => 'این کد ملی قبلاً برای بیمار دیگری ثبت شده است.', ], ]; diff --git a/resources/views/livewire/settings-dropdown.blade.php b/resources/views/livewire/settings-dropdown.blade.php index 2573fc1..715596b 100644 --- a/resources/views/livewire/settings-dropdown.blade.php +++ b/resources/views/livewire/settings-dropdown.blade.php @@ -40,13 +40,6 @@ class="relative" failed: false, interval: null, - showBackupProgress: false, - backupProgress: 0, - backupStatusText: '', - backupDone: false, - backupInterval: null, - backupUrl: null, - confirmSync() { this.open = false; this.showConfirm = true; @@ -84,32 +77,6 @@ class="relative" } }, - startBackup() { - this.open = false; - this.showBackupProgress = true; - this.backupProgress = 5; - this.backupDone = false; - this.backupUrl = null; - this.backupStatusText = '{{ __('navigation.backup.status_preparing') }}'; - - this.backupInterval = setInterval(() => { - if (this.backupProgress < 80) { - this.backupProgress += Math.random() * 8; - if (this.backupProgress > 30) { - this.backupStatusText = '{{ __('navigation.backup.status_compressing') }}'; - } - } - }, 400); - - $wire.runBackup(); - }, - - closeBackupModal() { - this.showBackupProgress = false; - if (this.backupUrl) { - window.location.href = this.backupUrl; - } - } }" x-on:sync-finished.window=" clearInterval(interval); @@ -122,13 +89,6 @@ class="relative" failed = true; } " - x-on:backup-ready.window=" - clearInterval(backupInterval); - backupProgress = 100; - backupStatusText = '{{ __('navigation.backup.status_ready') }}'; - backupDone = true; - backupUrl = $event.detail.url; - " @click.outside="open = false" > {{ __('navigation.backup.label') }} - + @@ -277,61 +237,6 @@ class="px-4 py-2 text-sm font-medium text-white rounded-lg transition" - - - - {{-- Close button: top-left, completely independent of layout --}} - - - - - - - - - - - - - - - - - - {{ __('navigation.backup.label') }} - - - - - - - - - - {{ __('navigation.backup.download_button') }} - - - - - - @endcan diff --git a/routes/web.php b/routes/web.php index 1b082ce..4d899ef 100644 --- a/routes/web.php +++ b/routes/web.php @@ -39,17 +39,64 @@ return response()->json($result); })->name('sync.run'); - Route::get('/admin/backup/download', function () { - $zipPath = session('backup_tmp_path'); - $zipName = session('backup_tmp_name', 'matab-backup.zip'); + Route::get('/backup/download', function () { + set_time_limit(0); + ini_set('memory_limit', '256M'); - if (! $zipPath || ! file_exists($zipPath)) { - abort(404, 'Backup file not found.'); + $dbPath = database_path('database.sqlite'); + if (! file_exists($dbPath)) { + abort(404, 'Database not found.'); } - session()->forget(['backup_tmp_path', 'backup_tmp_name']); + $zipFilename = 'matab-backup-' . now()->format('Y-m-d_H-i-s') . '.zip'; + $zipPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipFilename; - return response()->download($zipPath, $zipName, [ + $zip = new \ZipArchive(); + if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { + abort(500, 'Could not create zip archive.'); + } + + $noCompressExts = [ + 'jpg', 'jpeg', 'png', 'gif', 'webp', + 'mp4', 'mov', 'avi', 'mkv', 'webm', + 'mp3', 'wav', 'ogg', 'opus', 'm4a', + 'zip', 'gz', 'rar', '7z', + ]; + + $addFile = function (string $realPath, string $zipEntry) use ($zip, $noCompressExts): void { + $zip->addFile($realPath, $zipEntry); + $index = $zip->locateName($zipEntry); + $ext = strtolower(pathinfo($realPath, PATHINFO_EXTENSION)); + if (in_array($ext, $noCompressExts, true)) { + $zip->setCompressionIndex($index, \ZipArchive::CM_STORE); + } else { + $zip->setCompressionIndex($index, \ZipArchive::CM_DEFLATE, 1); + } + }; + + $addFile($dbPath, 'database/database.sqlite'); + + $publicPath = storage_path('app/public'); + foreach (['files', 'initials'] as $folder) { + $folderPath = $publicPath . '/' . $folder; + if (! is_dir($folderPath)) { + continue; + } + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + foreach ($iterator as $file) { + if ($file->isFile()) { + $relativePath = str_replace('\\', '/', substr($file->getPathname(), strlen($folderPath) + 1)); + $addFile($file->getPathname(), $folder . '/' . $relativePath); + } + } + } + + $zip->close(); + + return response()->download($zipPath, $zipFilename, [ 'Content-Type' => 'application/zip', ])->deleteFileAfterSend(true); })->name('backup.download');