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 @@
+