From 1e8ec1f76f99b58a356706b1dfa6854d30921533 Mon Sep 17 00:00:00 2001 From: SajjadMahmoody <117562560+SajjadMahmoody@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:47:23 +0330 Subject: [PATCH] feat: improve visits filter UX and update currency label --- .../VisitsRelationManager.php | 1 - app/Filament/Resources/VisitResource.php | 17 +++- .../VisitResource/Pages/ListVisits.php | 36 +++++++ app/Filament/Widgets/VisitStatsWidget.php | 97 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 3 + lang/en/navigation.php | 1 + lang/fa/navigation.php | 1 + lang/fa/visits.php | 2 +- resources/views/prints/patient-lab.blade.php | 46 ++++++--- .../prints/prescription-admission.blade.php | 46 ++++++--- resources/views/prints/prescription.blade.php | 55 +++++++---- .../views/prints/surgery-admission.blade.php | 47 ++++++--- scripts/install.bat | 12 ++- 13 files changed, 296 insertions(+), 68 deletions(-) create mode 100644 app/Filament/Widgets/VisitStatsWidget.php diff --git a/app/Filament/Resources/PatientResource/RelationManagers/VisitsRelationManager.php b/app/Filament/Resources/PatientResource/RelationManagers/VisitsRelationManager.php index 2bcc405..8693c07 100644 --- a/app/Filament/Resources/PatientResource/RelationManagers/VisitsRelationManager.php +++ b/app/Filament/Resources/PatientResource/RelationManagers/VisitsRelationManager.php @@ -58,7 +58,6 @@ public function form(Schema $schema): Schema ->label(__('visits.fields.visit_time')) ->native(false) ->seconds(false) - ->minutesStep(15) ->afterStateHydrated(function ($component, $state) { if (blank($state)) { $component->state(now()->format('Y-m-d H:i:s')); diff --git a/app/Filament/Resources/VisitResource.php b/app/Filament/Resources/VisitResource.php index bf4a87e..d6330e8 100644 --- a/app/Filament/Resources/VisitResource.php +++ b/app/Filament/Resources/VisitResource.php @@ -5,6 +5,7 @@ namespace App\Filament\Resources; use App\Filament\Resources\VisitResource\Pages; +use App\Filament\Widgets\VisitStatsWidget; use App\Models\Doctor; use App\Models\Patient; use App\Models\PaymentType; @@ -29,6 +30,7 @@ use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Enums\FiltersLayout; use Filament\Tables\Filters\Filter; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; @@ -42,6 +44,13 @@ class VisitResource extends Resource protected static ?string $recordTitleAttribute = 'visit_date'; + public static function getWidgets(): array + { + return [ + VisitStatsWidget::class, + ]; + } + public static function getGloballySearchableAttributes(): array { return ['visit_date', 'patientRecord.first_name', 'patientRecord.last_name', 'treatment_description']; @@ -311,9 +320,11 @@ public static function table(Table $table): Table ->label(__('visits.filters.to_date')) ->jalali(), ]) + ->columns(2) + ->columnSpan(2) ->query(fn ($query, array $data) => $query - ->when($data['from_date'], fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date'])) - ->when($data['to_date'], fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date'])) + ->when($data['from_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date'])) + ->when($data['to_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date'])) ) ->indicateUsing(function (array $data): array { $indicators = []; @@ -344,6 +355,8 @@ public static function table(Table $table): Table fn ($p) => [$p->id => $p->payment_type] )->toArray()), ]) + ->filtersLayout(FiltersLayout::AboveContent) + ->filtersFormColumns(5) ->recordActions([ EditAction::make(), DeleteAction::make(), diff --git a/app/Filament/Resources/VisitResource/Pages/ListVisits.php b/app/Filament/Resources/VisitResource/Pages/ListVisits.php index b8b9f8c..7a59afd 100644 --- a/app/Filament/Resources/VisitResource/Pages/ListVisits.php +++ b/app/Filament/Resources/VisitResource/Pages/ListVisits.php @@ -19,4 +19,40 @@ protected function getHeaderActions(): array ->icon('heroicon-o-plus'), ]; } + + protected function getHeaderWidgets(): array + { + return VisitResource::getWidgets(); + } + + public function applyTableFilters(): void + { + parent::applyTableFilters(); + $this->dispatchStatsUpdate(); + } + + public function updatedTableFilters(): void + { + parent::updatedTableFilters(); + $this->dispatchStatsUpdate(); + } + + public function resetTableFiltersForm(): void + { + parent::resetTableFiltersForm(); + $this->dispatchStatsUpdate(); + } + + protected function dispatchStatsUpdate(): void + { + $filters = $this->tableFilters ?? []; + + $this->dispatch('visit-stats-updated', + fromDate: ($filters['visit_date_range']['from_date'] ?? null) ?: null, + toDate: ($filters['visit_date_range']['to_date'] ?? null) ?: null, + doctor: isset($filters['doctor']['value']) && $filters['doctor']['value'] !== '' ? (int) $filters['doctor']['value'] : null, + treatment: isset($filters['treatment']['value']) && $filters['treatment']['value'] !== '' ? (int) $filters['treatment']['value'] : null, + paymentType: isset($filters['payment_type']['value']) && $filters['payment_type']['value'] !== '' ? (int) $filters['payment_type']['value'] : null, + ); + } } \ No newline at end of file diff --git a/app/Filament/Widgets/VisitStatsWidget.php b/app/Filament/Widgets/VisitStatsWidget.php new file mode 100644 index 0000000..eda2725 --- /dev/null +++ b/app/Filament/Widgets/VisitStatsWidget.php @@ -0,0 +1,97 @@ +fromDate = now()->toDateString(); + $this->toDate = now()->toDateString(); + } + + #[On('visit-stats-updated')] + public function updateStats( + ?string $fromDate, + ?string $toDate, + ?int $doctor, + ?int $treatment, + ?int $paymentType, + ): void { + if (! $fromDate && ! $toDate) { + $this->fromDate = now()->toDateString(); + $this->toDate = null; + } else { + $this->fromDate = $fromDate; + $this->toDate = $toDate; + } + $this->doctor = $doctor; + $this->treatment = $treatment; + $this->paymentType = $paymentType; + } + + protected function buildQuery() + { + $query = Visit::query(); + + if ($this->fromDate) { + $query->whereDate('visit_date', '>=', $this->fromDate); + } + + if ($this->toDate) { + $query->whereDate('visit_date', '<=', $this->toDate); + } + + if ($this->doctor) { + $query->where('doctor', $this->doctor); + } + + if ($this->treatment) { + $query->where('treatment', $this->treatment); + } + + if ($this->paymentType) { + $query->where('payment_type', $this->paymentType); + } + + return $query; + } + + protected function getStats(): array + { + $query = $this->buildQuery(); + + $totalPaid = (int) (clone $query)->sum('paid_amount'); + $totalCost = (int) (clone $query)->sum('treatment_cost'); + $totalRemained = $totalCost - $totalPaid; + + $currency = __('visits.fields.currency'); + + return [ + Stat::make(__('visits.fields.paid_amount'), number_format($totalPaid) . ' ' . $currency) + ->color('success') + ->icon('heroicon-o-banknotes'), + + Stat::make(__('visits.fields.treatment_cost'), number_format($totalCost) . ' ' . $currency) + ->color('info') + ->icon('heroicon-o-clipboard-document-list'), + + Stat::make(__('visits.fields.remained_amount'), number_format($totalRemained) . ' ' . $currency) + ->color($totalRemained > 0 ? 'danger' : 'success') + ->icon('heroicon-o-arrow-trending-down'), + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0132447..23a0c77 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -16,10 +16,12 @@ use App\Observers\PatientIllnessObserver; use App\Observers\PatientObserver; use App\Observers\SyncObserver; +use App\Filament\Widgets\VisitStatsWidget; use Filament\Actions\DeleteAction; use Filament\Actions\DeleteBulkAction; use Filament\Support\Enums\Alignment; use Illuminate\Support\ServiceProvider; +use Livewire\Livewire; class AppServiceProvider extends ServiceProvider { @@ -27,6 +29,7 @@ public function register(): void {} public function boot(): void { + Livewire::component('app.filament.widgets.visit-stats-widget', VisitStatsWidget::class); $observer = new SyncObserver(); Patient::observe($observer); diff --git a/lang/en/navigation.php b/lang/en/navigation.php index aef34e0..7c02f30 100644 --- a/lang/en/navigation.php +++ b/lang/en/navigation.php @@ -42,6 +42,7 @@ 'title' => 'Patients', 'singular' => 'Patient', 'description' => 'Log & Manage Patients Information, Including surgery & payment logs', + 'last_name' => 'Last Name', ], 'medications' => [ 'title' => 'Medications', diff --git a/lang/fa/navigation.php b/lang/fa/navigation.php index b1e26e0..9b9e98a 100644 --- a/lang/fa/navigation.php +++ b/lang/fa/navigation.php @@ -42,6 +42,7 @@ 'title' => 'لیست بیماران', 'singular' => 'بیمار', 'description' => 'لیست بیماران کلینیک خود را در این نما ثبت و مدیریت کنید. می توانید برای هر بیمار تمامی مستندات ویزیت، جراحی و پرداختهای بیمار را در این لیست مشاهده نمایید.', + 'last_name' => 'نام خانوادگی', ], 'medications' => [ 'title' => 'داروها', diff --git a/lang/fa/visits.php b/lang/fa/visits.php index 834a342..8cd299a 100644 --- a/lang/fa/visits.php +++ b/lang/fa/visits.php @@ -17,7 +17,7 @@ 'remained_amount' => 'مانده بدهی', 'payment_type' => 'روش پرداخت', 'treatment_description' => 'شرح درمان', - 'currency' => 'ریال', + 'currency' => 'تومان', 'created_by' => 'ثبت‌کننده', 'created_at' => 'تاریخ ثبت', ], diff --git a/resources/views/prints/patient-lab.blade.php b/resources/views/prints/patient-lab.blade.php index 04f0160..43b1fa7 100644 --- a/resources/views/prints/patient-lab.blade.php +++ b/resources/views/prints/patient-lab.blade.php @@ -52,6 +52,16 @@ .toolbar-divider { flex: 1; } + .print-info-tooltip { position:relative;display:inline-flex;align-items:center; } + .print-info-tooltip .tip-icon { display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:#374151;border-radius:8px;cursor:pointer;color:#9ca3af;border:none;transition:background 0.15s,color 0.15s; } + .print-info-tooltip .tip-icon:hover { background:#4b5563;color:#fff; } + .print-info-tooltip .tip-box { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:#111827;border:1px solid #374151;border-radius:10px;padding:14px 18px;min-width:250px;box-shadow:0 4px 24px rgba(0,0,0,0.5);z-index:200;direction:rtl; } + .print-info-tooltip .tip-box.open { display:block; } + .print-info-tooltip .tip-box::after { content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#374151; } + .print-info-tooltip .tip-box h4 { margin:0 0 10px;font-size:12px;color:#9ca3af;border-bottom:1px solid #374151;padding-bottom:8px;font-weight:normal; } + .print-info-tooltip .tip-box ul { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px; } + .print-info-tooltip .tip-box ul li { font-size:12px;color:#e5e7eb; } + .page-body { padding: 20px; display: flex; @@ -181,6 +191,21 @@ ✓ پرینت آزمایش
+ ← بازگشت @@ -218,20 +243,13 @@ diff --git a/resources/views/prints/prescription-admission.blade.php b/resources/views/prints/prescription-admission.blade.php index 8f2036a..2c78956 100644 --- a/resources/views/prints/prescription-admission.blade.php +++ b/resources/views/prints/prescription-admission.blade.php @@ -57,6 +57,16 @@ .toolbar-divider { flex: 1; } + .print-info-tooltip { position:relative;display:inline-flex;align-items:center; } + .print-info-tooltip .tip-icon { display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:#374151;border-radius:8px;cursor:pointer;color:#9ca3af;border:none;transition:background 0.15s,color 0.15s; } + .print-info-tooltip .tip-icon:hover { background:#4b5563;color:#fff; } + .print-info-tooltip .tip-box { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:#111827;border:1px solid #374151;border-radius:10px;padding:14px 18px;min-width:250px;box-shadow:0 4px 24px rgba(0,0,0,0.5);z-index:200;direction:rtl; } + .print-info-tooltip .tip-box.open { display:block; } + .print-info-tooltip .tip-box::after { content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#374151; } + .print-info-tooltip .tip-box h4 { margin:0 0 10px;font-size:12px;color:#9ca3af;border-bottom:1px solid #374151;padding-bottom:8px;font-weight:normal; } + .print-info-tooltip .tip-box ul { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px; } + .print-info-tooltip .tip-box ul li { font-size:12px;color:#e5e7eb; } + .page-body { padding: 20px; display: flex; @@ -226,6 +236,21 @@
+
← بازگشت به لیست @@ -262,20 +287,13 @@ diff --git a/resources/views/prints/prescription.blade.php b/resources/views/prints/prescription.blade.php index 1f18ad2..d4d3c57 100644 --- a/resources/views/prints/prescription.blade.php +++ b/resources/views/prints/prescription.blade.php @@ -64,6 +64,16 @@ .toolbar-divider { flex: 1; } + .print-info-tooltip { position:relative;display:inline-flex;align-items:center; } + .print-info-tooltip .tip-icon { display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:#374151;border-radius:8px;cursor:pointer;color:#9ca3af;border:none;transition:background 0.15s,color 0.15s; } + .print-info-tooltip .tip-icon:hover { background:#4b5563;color:#fff; } + .print-info-tooltip .tip-box { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:#111827;border:1px solid #374151;border-radius:10px;padding:14px 18px;min-width:250px;box-shadow:0 4px 24px rgba(0,0,0,0.5);z-index:200;direction:rtl; } + .print-info-tooltip .tip-box.open { display:block; } + .print-info-tooltip .tip-box::after { content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#374151; } + .print-info-tooltip .tip-box h4 { margin:0 0 10px;font-size:12px;color:#9ca3af;border-bottom:1px solid #374151;padding-bottom:8px;font-weight:normal; } + .print-info-tooltip .tip-box ul { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px; } + .print-info-tooltip .tip-box ul li { font-size:12px;color:#e5e7eb; } + .edit-modal-overlay { display: none; position: fixed; @@ -338,15 +348,26 @@
- @if ($type === 'both') - - - - @else + @if ($type !== 'both') @endif +
← بازگشت به لیست @@ -539,21 +560,6 @@ function printSection(section) { window.print(); document.body.classList.remove('printing-prescription', 'printing-lab'); } - @else - window.onload = function () { - const userConfirmed = confirm( - 'لطفاً تنظیمات پرینت را به این صورت انجام دهید:\n\n' + - '✅ Paper size: A5\n' + - '✅ Orientation: Landscape (افقی)\n' + - '✅ Margins: None (بدون حاشیه)\n' + - '✅ Scale: 100%\n' + - '✅ Background graphics: ON (فعال)\n\n' + - 'آیا آماده پرینت هستید؟' - ); - if (userConfirmed) { - window.print(); - } - }; @endif const overlay = document.getElementById('editModalOverlay'); @@ -586,6 +592,15 @@ function closeEditModal() { }); document.addEventListener('keydown', e => { if (e.key === 'Escape') closeEditModal(); }); + + document.querySelector('.print-info-tooltip .tip-icon').addEventListener('click', function(e) { + e.stopPropagation(); + document.querySelector('.print-info-tooltip .tip-box').classList.toggle('open'); + }); + document.addEventListener('click', function() { + document.querySelector('.print-info-tooltip .tip-box').classList.remove('open'); + }); + diff --git a/resources/views/prints/surgery-admission.blade.php b/resources/views/prints/surgery-admission.blade.php index f6b9a13..b2c03b9 100644 --- a/resources/views/prints/surgery-admission.blade.php +++ b/resources/views/prints/surgery-admission.blade.php @@ -54,6 +54,17 @@ .btn-back { background: #374151; color: #d1d5db; } .toolbar-divider { flex: 1; } + + .print-info-tooltip { position:relative;display:inline-flex;align-items:center; } + .print-info-tooltip .tip-icon { display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;background:#374151;border-radius:8px;cursor:pointer;color:#9ca3af;border:none;transition:background 0.15s,color 0.15s; } + .print-info-tooltip .tip-icon:hover { background:#4b5563;color:#fff; } + .print-info-tooltip .tip-box { display:none;position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%);background:#111827;border:1px solid #374151;border-radius:10px;padding:14px 18px;min-width:250px;box-shadow:0 4px 24px rgba(0,0,0,0.5);z-index:200;direction:rtl; } + .print-info-tooltip .tip-box.open { display:block; } + .print-info-tooltip .tip-box::after { content:'';position:absolute;bottom:100%;left:50%;transform:translateX(-50%);border:6px solid transparent;border-bottom-color:#374151; } + .print-info-tooltip .tip-box h4 { margin:0 0 10px;font-size:12px;color:#9ca3af;border-bottom:1px solid #374151;padding-bottom:8px;font-weight:normal; } + .print-info-tooltip .tip-box ul { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px; } + .print-info-tooltip .tip-box ul li { font-size:12px;color:#e5e7eb; } + .page-body { padding: 20px; display: flex; @@ -215,6 +226,21 @@
+
← بازگشت به لیست @@ -254,20 +280,13 @@ diff --git a/scripts/install.bat b/scripts/install.bat index 29d9e58..6713a97 100644 --- a/scripts/install.bat +++ b/scripts/install.bat @@ -34,8 +34,8 @@ if not exist "!SCRIPTS!\php\php-cgi.exe" ( pause & exit /b 1 ) -REM --- [1/7] Clear bootstrap cache --- -echo [1/7] Clearing bootstrap cache... +REM --- [1/7] Clear bootstrap cache + OPcache file cache --- +echo [1/7] Clearing bootstrap cache and OPcache... cd /d "!PROJECT_DIR!" if exist "bootstrap\cache\config.php" del /F /Q "bootstrap\cache\config.php" >nul 2>&1 if exist "bootstrap\cache\routes-v7.php" del /F /Q "bootstrap\cache\routes-v7.php" >nul 2>&1 @@ -44,6 +44,11 @@ if exist "bootstrap\cache\packages.php" del /F /Q "bootstrap\cache\packages. if exist "bootstrap\cache\events.php" del /F /Q "bootstrap\cache\events.php" >nul 2>&1 if exist "bootstrap\cache\blade-icons.php" del /F /Q "bootstrap\cache\blade-icons.php" >nul 2>&1 if exist "bootstrap\cache\filament" rmdir /S /Q "bootstrap\cache\filament" >nul 2>&1 +REM Clear compiled Blade views (storage/framework/views/*.php) +for %%f in ("storage\framework\views\*.php") do del /F /Q "%%f" >nul 2>&1 +REM Clear OPcache file cache from disk (opcache.file_cache=C:/tmp/php-opcache in php.ini) +REM Without this, php-cgi reads stale bytecode after restart since validate_timestamps=0 +if exist "C:\tmp\php-opcache" rmdir /S /Q "C:\tmp\php-opcache" >nul 2>&1 echo [OK] Done. echo. @@ -55,6 +60,7 @@ echo. REM --- [3/7] Optimize Laravel --- echo [3/7] Optimizing Laravel... +"!SCRIPTS!\php\php.exe" artisan optimize:clear --quiet 2>nul "!SCRIPTS!\php\php.exe" artisan optimize --quiet 2>nul "!SCRIPTS!\php\php.exe" artisan filament:optimize --quiet 2>nul echo [OK] Done. @@ -86,9 +92,11 @@ echo [6/7] Firewall rules... netsh advfirewall firewall delete rule name="Matab Web 8000" >nul 2>&1 netsh advfirewall firewall delete rule name="Matab FTP 2121" >nul 2>&1 netsh advfirewall firewall delete rule name="Matab FTP Passive" >nul 2>&1 +netsh advfirewall firewall delete rule name="Matab Ping" >nul 2>&1 netsh advfirewall firewall add rule name="Matab Web 8000" dir=in action=allow protocol=TCP localport=8000 >nul netsh advfirewall firewall add rule name="Matab FTP 2121" dir=in action=allow protocol=TCP localport=2121 >nul netsh advfirewall firewall add rule name="Matab FTP Passive" dir=in action=allow protocol=TCP localport=60000-60020 >nul +netsh advfirewall firewall add rule name="Matab Ping" dir=in action=allow protocol=icmpv4:8,any >nul echo [OK] Done. echo.