feat: improve visits filter UX and update currency label

This commit is contained in:
SajjadMahmoody 2026-04-18 22:47:23 +03:30
parent 4abd856bf9
commit 1e8ec1f76f
13 changed files with 296 additions and 68 deletions

View File

@ -58,7 +58,6 @@ public function form(Schema $schema): Schema
->label(__('visits.fields.visit_time')) ->label(__('visits.fields.visit_time'))
->native(false) ->native(false)
->seconds(false) ->seconds(false)
->minutesStep(15)
->afterStateHydrated(function ($component, $state) { ->afterStateHydrated(function ($component, $state) {
if (blank($state)) { if (blank($state)) {
$component->state(now()->format('Y-m-d H:i:s')); $component->state(now()->format('Y-m-d H:i:s'));

View File

@ -5,6 +5,7 @@
namespace App\Filament\Resources; namespace App\Filament\Resources;
use App\Filament\Resources\VisitResource\Pages; use App\Filament\Resources\VisitResource\Pages;
use App\Filament\Widgets\VisitStatsWidget;
use App\Models\Doctor; use App\Models\Doctor;
use App\Models\Patient; use App\Models\Patient;
use App\Models\PaymentType; use App\Models\PaymentType;
@ -29,6 +30,7 @@
use Filament\Schemas\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\Filter; use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
@ -42,6 +44,13 @@ class VisitResource extends Resource
protected static ?string $recordTitleAttribute = 'visit_date'; protected static ?string $recordTitleAttribute = 'visit_date';
public static function getWidgets(): array
{
return [
VisitStatsWidget::class,
];
}
public static function getGloballySearchableAttributes(): array public static function getGloballySearchableAttributes(): array
{ {
return ['visit_date', 'patientRecord.first_name', 'patientRecord.last_name', 'treatment_description']; 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')) ->label(__('visits.filters.to_date'))
->jalali(), ->jalali(),
]) ])
->columns(2)
->columnSpan(2)
->query(fn ($query, array $data) => $query ->query(fn ($query, array $data) => $query
->when($data['from_date'], fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date'])) ->when($data['from_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date']))
->when($data['to_date'], fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date'])) ->when($data['to_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date']))
) )
->indicateUsing(function (array $data): array { ->indicateUsing(function (array $data): array {
$indicators = []; $indicators = [];
@ -344,6 +355,8 @@ public static function table(Table $table): Table
fn ($p) => [$p->id => $p->payment_type] fn ($p) => [$p->id => $p->payment_type]
)->toArray()), )->toArray()),
]) ])
->filtersLayout(FiltersLayout::AboveContent)
->filtersFormColumns(5)
->recordActions([ ->recordActions([
EditAction::make(), EditAction::make(),
DeleteAction::make(), DeleteAction::make(),

View File

@ -19,4 +19,40 @@ protected function getHeaderActions(): array
->icon('heroicon-o-plus'), ->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,
);
}
} }

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Visit;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Livewire\Attributes\On;
class VisitStatsWidget extends BaseWidget
{
public ?string $fromDate = null;
public ?string $toDate = null;
public ?int $doctor = null;
public ?int $treatment = null;
public ?int $paymentType = null;
public function mount(): void
{
$this->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'),
];
}
}

View File

@ -16,10 +16,12 @@
use App\Observers\PatientIllnessObserver; use App\Observers\PatientIllnessObserver;
use App\Observers\PatientObserver; use App\Observers\PatientObserver;
use App\Observers\SyncObserver; use App\Observers\SyncObserver;
use App\Filament\Widgets\VisitStatsWidget;
use Filament\Actions\DeleteAction; use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Support\Enums\Alignment; use Filament\Support\Enums\Alignment;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
@ -27,6 +29,7 @@ public function register(): void {}
public function boot(): void public function boot(): void
{ {
Livewire::component('app.filament.widgets.visit-stats-widget', VisitStatsWidget::class);
$observer = new SyncObserver(); $observer = new SyncObserver();
Patient::observe($observer); Patient::observe($observer);

View File

@ -42,6 +42,7 @@
'title' => 'Patients', 'title' => 'Patients',
'singular' => 'Patient', 'singular' => 'Patient',
'description' => 'Log & Manage Patients Information, Including surgery & payment logs', 'description' => 'Log & Manage Patients Information, Including surgery & payment logs',
'last_name' => 'Last Name',
], ],
'medications' => [ 'medications' => [
'title' => 'Medications', 'title' => 'Medications',

View File

@ -42,6 +42,7 @@
'title' => 'لیست بیماران', 'title' => 'لیست بیماران',
'singular' => 'بیمار', 'singular' => 'بیمار',
'description' => 'لیست بیماران کلینیک خود را در این نما ثبت و مدیریت کنید. می توانید برای هر بیمار تمامی مستندات ویزیت، جراحی و پرداختهای بیمار را در این لیست مشاهده نمایید.', 'description' => 'لیست بیماران کلینیک خود را در این نما ثبت و مدیریت کنید. می توانید برای هر بیمار تمامی مستندات ویزیت، جراحی و پرداختهای بیمار را در این لیست مشاهده نمایید.',
'last_name' => 'نام خانوادگی',
], ],
'medications' => [ 'medications' => [
'title' => 'داروها', 'title' => 'داروها',

View File

@ -17,7 +17,7 @@
'remained_amount' => 'مانده بدهی', 'remained_amount' => 'مانده بدهی',
'payment_type' => 'روش پرداخت', 'payment_type' => 'روش پرداخت',
'treatment_description' => 'شرح درمان', 'treatment_description' => 'شرح درمان',
'currency' => 'ریال', 'currency' => 'تومان',
'created_by' => 'ثبت‌کننده', 'created_by' => 'ثبت‌کننده',
'created_at' => 'تاریخ ثبت', 'created_at' => 'تاریخ ثبت',
], ],

View File

@ -52,6 +52,16 @@
.toolbar-divider { flex: 1; } .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 { .page-body {
padding: 20px; padding: 20px;
display: flex; display: flex;
@ -181,6 +191,21 @@
<span class="toolbar-btn btn-active"> پرینت آزمایش</span> <span class="toolbar-btn btn-active"> پرینت آزمایش</span>
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button> <button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button>
<div class="print-info-tooltip">
<button class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
<div class="tip-box">
<h4>تنظیمات پرینت</h4>
<ul>
<li> Paper size: A5</li>
<li> Orientation: Landscape (افقی)</li>
<li> Margins: None (بدون حاشیه)</li>
<li> Scale: 100%</li>
<li> Background graphics: ON (فعال)</li>
</ul>
</div>
</div>
<a class="toolbar-btn btn-back" <a class="toolbar-btn btn-back"
href="{{ url()->previous() }}"> href="{{ url()->previous() }}">
بازگشت بازگشت
@ -218,20 +243,13 @@
</div> </div>
<script> <script>
window.onload = function () { document.querySelector('.print-info-tooltip .tip-icon').addEventListener('click', function(e) {
const userConfirmed = confirm( e.stopPropagation();
'لطفاً تنظیمات پرینت را به این صورت انجام دهید:\n\n' + document.querySelector('.print-info-tooltip .tip-box').classList.toggle('open');
'✅ Paper size: A5\n' + });
'✅ Orientation: Landscape (افقی)\n' + document.addEventListener('click', function() {
'✅ Margins: None (بدون حاشیه)\n' + document.querySelector('.print-info-tooltip .tip-box').classList.remove('open');
'✅ Scale: 100%\n' + });
'✅ Background graphics: ON (فعال)\n\n' +
'آیا آماده پرینت هستید؟'
);
if (userConfirmed) {
window.print();
}
};
</script> </script>
</body> </body>
</html> </html>

View File

@ -57,6 +57,16 @@
.toolbar-divider { flex: 1; } .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 { .page-body {
padding: 20px; padding: 20px;
display: flex; display: flex;
@ -226,6 +236,21 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button> <button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button>
<div class="print-info-tooltip">
<button class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
<div class="tip-box">
<h4>تنظیمات پرینت</h4>
<ul>
<li> Paper size: A5</li>
<li> Orientation: Landscape (افقی)</li>
<li> Margins: None (بدون حاشیه)</li>
<li> Scale: 100%</li>
<li> Background graphics: ON (فعال)</li>
</ul>
</div>
</div>
<a class="toolbar-btn btn-back" <a class="toolbar-btn btn-back"
href="{{ route('filament.admin.resources.prescriptions.index') }}"> href="{{ route('filament.admin.resources.prescriptions.index') }}">
بازگشت به لیست بازگشت به لیست
@ -262,20 +287,13 @@
</div> </div>
<script> <script>
window.onload = function () { document.querySelector('.print-info-tooltip .tip-icon').addEventListener('click', function(e) {
const userConfirmed = confirm( e.stopPropagation();
'لطفاً تنظیمات پرینت را به این صورت انجام دهید:\n\n' + document.querySelector('.print-info-tooltip .tip-box').classList.toggle('open');
'✅ Paper size: A5\n' + });
'✅ Orientation: Landscape (افقی)\n' + document.addEventListener('click', function() {
'✅ Margins: None (بدون حاشیه)\n' + document.querySelector('.print-info-tooltip .tip-box').classList.remove('open');
'✅ Scale: 100%\n' + });
'✅ Background graphics: ON (فعال)\n\n' +
'آیا آماده پرینت هستید؟'
);
if (userConfirmed) {
window.print();
}
};
</script> </script>
</body> </body>
</html> </html>

View File

@ -64,6 +64,16 @@
.toolbar-divider { flex: 1; } .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 { .edit-modal-overlay {
display: none; display: none;
position: fixed; position: fixed;
@ -338,15 +348,26 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
@if ($type === 'both') @if ($type !== 'both')
<button class="toolbar-btn btn-print" onclick="printSection('prescription')">🖨 پرینت نسخه</button>
<button class="toolbar-btn btn-lab" onclick="printSection('lab')">🖨 پرینت آزمایش</button>
<button class="toolbar-btn btn-edit" onclick="openEditModal()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;flex-shrink:0;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> ویرایش نسخه</button>
@else
<button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button> <button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button>
<button class="toolbar-btn btn-edit" onclick="openEditModal()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;flex-shrink:0;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> ویرایش نسخه</button> <button class="toolbar-btn btn-edit" onclick="openEditModal()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;flex-shrink:0;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> ویرایش نسخه</button>
@endif @endif
<div class="print-info-tooltip">
<button class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
<div class="tip-box">
<h4>تنظیمات پرینت</h4>
<ul>
<li> Paper size: A5</li>
<li> Orientation: Landscape (افقی)</li>
<li> Margins: None (بدون حاشیه)</li>
<li> Scale: 100%</li>
<li> Background graphics: ON (فعال)</li>
</ul>
</div>
</div>
<a class="toolbar-btn btn-back" <a class="toolbar-btn btn-back"
href="{{ route('filament.admin.resources.prescriptions.index') }}"> href="{{ route('filament.admin.resources.prescriptions.index') }}">
بازگشت به لیست بازگشت به لیست
@ -539,21 +560,6 @@ function printSection(section) {
window.print(); window.print();
document.body.classList.remove('printing-prescription', 'printing-lab'); 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 @endif
const overlay = document.getElementById('editModalOverlay'); const overlay = document.getElementById('editModalOverlay');
@ -586,6 +592,15 @@ function closeEditModal() {
}); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') 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');
});
</script> </script>
</body> </body>
</html> </html>

View File

@ -54,6 +54,17 @@
.btn-back { background: #374151; color: #d1d5db; } .btn-back { background: #374151; color: #d1d5db; }
.toolbar-divider { flex: 1; } .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 { .page-body {
padding: 20px; padding: 20px;
display: flex; display: flex;
@ -215,6 +226,21 @@
<div class="toolbar-divider"></div> <div class="toolbar-divider"></div>
<button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button> <button class="toolbar-btn btn-print" onclick="window.print()">🖨 پرینت</button>
<div class="print-info-tooltip">
<button class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
</button>
<div class="tip-box">
<h4>تنظیمات پرینت</h4>
<ul>
<li> Paper size: A5</li>
<li> Orientation: Landscape (افقی)</li>
<li> Margins: None (بدون حاشیه)</li>
<li> Scale: 100%</li>
<li> Background graphics: ON (فعال)</li>
</ul>
</div>
</div>
<a class="toolbar-btn btn-back" href="{{ route('filament.admin.resources.patients.index') }}"> <a class="toolbar-btn btn-back" href="{{ route('filament.admin.resources.patients.index') }}">
بازگشت به لیست بازگشت به لیست
</a> </a>
@ -254,20 +280,13 @@
</div> </div>
<script> <script>
window.onload = function () { document.querySelector('.print-info-tooltip .tip-icon').addEventListener('click', function(e) {
const userConfirmed = confirm( e.stopPropagation();
'لطفاً تنظیمات پرینت را به این صورت انجام دهید:\n\n' + document.querySelector('.print-info-tooltip .tip-box').classList.toggle('open');
'✅ Paper size: A5\n' + });
'✅ Orientation: Landscape (افقی)\n' + document.addEventListener('click', function() {
'✅ Margins: None (بدون حاشیه)\n' + document.querySelector('.print-info-tooltip .tip-box').classList.remove('open');
'✅ Scale: 100%\n' + });
'✅ Background graphics: ON (فعال)\n\n' +
'آیا آماده پرینت هستید؟'
);
if (userConfirmed) {
window.print();
}
};
</script> </script>
</body> </body>
</html> </html>

View File

@ -34,8 +34,8 @@ if not exist "!SCRIPTS!\php\php-cgi.exe" (
pause & exit /b 1 pause & exit /b 1
) )
REM --- [1/7] Clear bootstrap cache --- REM --- [1/7] Clear bootstrap cache + OPcache file cache ---
echo [1/7] Clearing bootstrap cache... echo [1/7] Clearing bootstrap cache and OPcache...
cd /d "!PROJECT_DIR!" cd /d "!PROJECT_DIR!"
if exist "bootstrap\cache\config.php" del /F /Q "bootstrap\cache\config.php" >nul 2>&1 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 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\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\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 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 [OK] Done.
echo. echo.
@ -55,6 +60,7 @@ echo.
REM --- [3/7] Optimize Laravel --- REM --- [3/7] Optimize Laravel ---
echo [3/7] Optimizing 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 optimize --quiet 2>nul
"!SCRIPTS!\php\php.exe" artisan filament:optimize --quiet 2>nul "!SCRIPTS!\php\php.exe" artisan filament:optimize --quiet 2>nul
echo [OK] Done. 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 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 2121" >nul 2>&1
netsh advfirewall firewall delete rule name="Matab FTP Passive" >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 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 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 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 [OK] Done.
echo. echo.