feat: initialize Dr-Panel project with patient file number support

This commit is contained in:
SajjadMahmoody
2026-05-06 02:18:23 +03:30
parent f8f174690a
commit 8b84d1780f
460 changed files with 113960 additions and 1 deletions
@@ -0,0 +1,197 @@
<x-filament-panels::page>
@php
$user = auth()->user();
$groups = [
[
'label' => __('navigation.groups.patients'),
'color' => 'blue',
'cards' => [
[
'title' => __('navigation.patients.title'),
'description' => __('navigation.patients.description'),
'icon' => 'heroicon-o-user-group',
'url' => \App\Filament\Resources\PatientResource::getUrl('index'),
'permission' => 'ViewAny:Patient',
],
[
'title' => __('navigation.visits.title'),
'description' => __('navigation.visits.description'),
'icon' => 'heroicon-o-calendar',
'url' => \App\Filament\Resources\VisitResource::getUrl('index'),
'permission' => 'ViewAny:Visit',
],
[
'title' => __('navigation.prescriptions.title'),
'description' => __('navigation.prescriptions.description'),
'icon' => 'heroicon-o-document-text',
'url' => \App\Filament\Resources\PrescriptionResource::getUrl('index'),
'permission' => 'ViewAny:Prescription',
],
[
'title' => __('navigation.surgery_appointments.title'),
'description' => __('navigation.surgery_appointments.description'),
'icon' => 'heroicon-o-calendar-days',
'url' => \App\Filament\Resources\SurgeryAppointmentResource::getUrl('index'),
'permission' => 'ViewAny:SurgeryAppointment',
],
],
],
[
'label' => __('navigation.groups.diseases'),
'color' => 'orange',
'cards' => [
[
'title' => __('navigation.patient_illness.title'),
'description' => __('navigation.patient_illness.description'),
'icon' => 'heroicon-o-heart',
'url' => \App\Filament\Resources\PatientIllnessResource::getUrl('index'),
'permission' => 'ViewAny:PatientIllness',
],
[
'title' => __('navigation.treatments.title'),
'description' => __('navigation.treatments.description'),
'icon' => 'heroicon-o-clipboard-document-list',
'url' => \App\Filament\Resources\TreatmentResource::getUrl('index'),
'permission' => 'ViewAny:Treatment',
],
[
'title' => __('navigation.medications.title'),
'description' => __('navigation.medications.description'),
'icon' => 'heroicon-o-beaker',
'url' => \App\Filament\Resources\MedicationResource::getUrl('index'),
'permission' => 'ViewAny:Medication',
],
[
'title' => __('navigation.lab_tests.title'),
'description' => __('navigation.lab_tests.description'),
'icon' => 'heroicon-o-beaker',
'url' => \App\Filament\Resources\LabTestResource::getUrl('index'),
'permission' => 'ViewAny:LabTest',
],
],
],
[
'label' => __('navigation.groups.clinic'),
'color' => 'teal',
'cards' => [
[
'title' => __('navigation.doctors.title'),
'description' => __('navigation.doctors.description'),
'icon' => 'heroicon-o-user-circle',
'url' => \App\Filament\Resources\DoctorResource::getUrl('index'),
'permission' => 'ViewAny:Doctor',
],
[
'title' => __('navigation.payment_types.title'),
'description' => __('navigation.payment_types.description'),
'icon' => 'heroicon-o-credit-card',
'url' => \App\Filament\Resources\PaymentTypeResource::getUrl('index'),
'permission' => 'ViewAny:PaymentType',
],
[
'title' => __('navigation.surgery_centers.title'),
'description' => __('navigation.surgery_centers.description'),
'icon' => 'heroicon-o-building-office-2',
'url' => \App\Filament\Resources\SurgeryCenterResource::getUrl('index'),
'permission' => 'ViewAny:SurgeryCenter',
],
[
'title' => __('navigation.users.title'),
'description' => __('navigation.users.description'),
'icon' => 'heroicon-o-users',
'url' => \App\Filament\Resources\UserResource::getUrl('index'),
'permission' => 'ViewAny:User',
],
],
],
[
'label' => __('navigation.groups.settings'),
'color' => 'slate',
'cards' => [
[
'title' => __('navigation.osurg_audit.title'),
'description' => __('navigation.osurg_audit.description'),
'icon' => 'heroicon-o-clipboard-document-list',
'url' => \App\Filament\Resources\OsurgAuditResource::getUrl('index'),
'permission' => 'ViewAny:SyncLog',
],
[
'title' => __('navigation.osurg_initials.title'),
'description' => __('navigation.osurg_initials.description'),
'icon' => 'heroicon-o-adjustments-horizontal',
'url' => \App\Filament\Resources\OsurgInitialResource::getUrl('index'),
'permission' => 'ViewAny:OsurgInitial',
],
[
'title' => __('navigation.sms_logs.title'),
'description' => __('navigation.sms_logs.description'),
'icon' => 'heroicon-o-chat-bubble-left-ellipsis',
'url' => \App\Filament\Pages\SmsLogsPage::getUrl(),
'permission' => 'View:SmsLogsPage',
],
[
'title' => __('navigation.roles.title'),
'description' => __('navigation.roles.description'),
'icon' => 'heroicon-o-shield-check',
'url' => \App\Filament\Resources\RoleResource::getUrl('index'),
'permission' => 'ViewAny:Role',
],
],
],
];
$groups = array_filter(array_map(function ($group) use ($user) {
$group['cards'] = array_filter($group['cards'], fn ($card) =>
!isset($card['permission']) || $user->can($card['permission'])
);
return $group;
}, $groups), fn ($group) => !empty($group['cards']));
@endphp
<div class="space-y-8">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
@foreach (array_slice($groups, 0, 2) as $group)
<div>
<h2 class="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
{{ $group['label'] }}
</h2>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
@foreach ($group['cards'] as $card)
<x-navigation-card
:title="$card['title']"
:description="$card['description']"
:icon="$card['icon']"
:url="$card['url']"
:color="$group['color']"
/>
@endforeach
</div>
</div>
@endforeach
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
@foreach (array_slice($groups, 2) as $group)
<div>
<h2 class="mb-4 text-sm font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500">
{{ $group['label'] }}
</h2>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
@foreach ($group['cards'] as $card)
<x-navigation-card
:title="$card['title']"
:description="$card['description']"
:icon="$card['icon']"
:url="$card['url']"
:color="$group['color']"
/>
@endforeach
</div>
</div>
@endforeach
</div>
</div>
</x-filament-panels::page>
@@ -0,0 +1,39 @@
<x-filament-panels::page>
<div class="flex flex-col gap-6">
<div class="order-2">
{{ $this->table }}
</div>
<div class="order-1 grid grid-cols-2 gap-4 sm:grid-cols-4">
<x-filament::card>
<div class="text-center">
<p class="text-3xl font-bold text-primary-600 dark:text-primary-400">{{ $this->stats['patients'] }}</p>
<p class="text-sm text-gray-500 mt-1">بیمار&lrm; ({{ $this->stats['total'] }} پیام)</p>
</div>
</x-filament::card>
<x-filament::card>
<div class="text-center">
<p class="text-3xl font-bold text-success-600 dark:text-success-400">{{ $this->stats['sent'] }}</p>
<p class="text-sm text-gray-500 mt-1">ارسال شده</p>
</div>
</x-filament::card>
<x-filament::card>
<div class="text-center">
<p class="text-3xl font-bold text-warning-600 dark:text-warning-400">{{ $this->stats['pending'] }}</p>
<p class="text-sm text-gray-500 mt-1">در انتظار</p>
</div>
</x-filament::card>
<x-filament::card>
<div class="text-center">
<p class="text-3xl font-bold text-danger-600 dark:text-danger-400">{{ $this->stats['failed'] }}</p>
<p class="text-sm text-gray-500 mt-1">ناموفق</p>
</div>
</x-filament::card>
</div>
</div>
</x-filament-panels::page>
@@ -0,0 +1,187 @@
<x-filament-panels::page>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3 mb-6">
<x-filament::card>
<div class="flex items-center gap-3">
<x-heroicon-o-clock class="w-8 h-8 text-gray-400" />
<div>
<p class="text-sm text-gray-500">{{ __('navigation.sync.last_sync') }}</p>
<p class="text-lg font-bold">{{ $this->getLastSyncTime() }}</p>
</div>
</div>
</x-filament::card>
<x-filament::card>
<div class="flex items-center gap-3">
<x-heroicon-o-queue-list class="w-8 h-8 text-amber-400" />
<div>
<p class="text-sm text-gray-500">{{ __('navigation.sync.pending_changes') }}</p>
<p class="text-2xl font-bold text-amber-500">{{ $this->getPendingCount() }}</p>
</div>
</div>
</x-filament::card>
<x-filament::card>
<div class="flex items-center gap-3">
<x-heroicon-o-server class="w-8 h-8 text-blue-400" />
<div>
<p class="text-sm text-gray-500">{{ __('navigation.sync.peer_address') }}</p>
<p class="text-lg font-bold font-mono">
{{ \App\Models\OsurgInitial::val('sync_peer_ip', config('sync.peer_ip')) ?: '—' }}
@php($port = \App\Models\OsurgInitial::val('sync_peer_port', config('sync.peer_port')))
@if($port && $port != 80)
:{{ $port }}
@endif
</p>
</div>
</div>
</x-filament::card>
</div>
<x-filament::card>
<div class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<h3 class="font-semibold text-base text-gray-800 dark:text-gray-200">{{ __('navigation.sync.setup_guide') }}</h3>
<p>{{ __('navigation.sync.setup_description_dynamic') }}</p>
<ul class="list-disc list-inside space-y-1 text-xs">
<li><code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">sync_peer_ip</code> — {{ __('navigation.sync.field_peer_ip') }}</li>
<li><code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">sync_peer_port</code> — {{ __('navigation.sync.field_peer_port') }}</li>
<li><code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">sync_token</code> — {{ __('navigation.sync.field_sync_token') }}</li>
</ul>
<p class="text-amber-600 dark:text-amber-400">⚠ {{ __('navigation.sync.setup_warning') }}</p>
</div>
</x-filament::card>
<x-filament::card>
<div
x-data="{
dragging: false,
fileName: '',
fileSize: '',
uploading: false,
uploaded: false,
uploadProgress: 0,
setFile(file) {
this.fileName = file.name;
this.fileSize = (file.size / 1024 / 1024).toFixed(2) + ' MB';
this.uploading = true;
this.uploaded = false;
this.uploadProgress = 0;
$wire.upload(
'sqlFile',
file,
() => { this.uploading = false; this.uploaded = true; this.uploadProgress = 100; },
() => { this.uploading = false; this.uploaded = false; this.fileName = ''; this.uploadProgress = 0; },
(event) => { if (event && event.detail) this.uploadProgress = event.detail.progress; }
);
}
}"
class="space-y-4"
>
<div class="flex items-center gap-2">
<x-heroicon-o-arrow-up-tray class="w-5 h-5 text-amber-500" />
<h3 class="font-semibold text-base text-gray-800 dark:text-gray-200">
{{ __('navigation.import.label') }}
</h3>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ __('navigation.import.confirm_description') }}
</p>
{{-- File drop zone --}}
<div
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="
dragging = false;
const file = $event.dataTransfer.files[0];
if (file) setFile(file);
"
class="relative border-2 border-dashed rounded-xl p-6 text-center transition-colors"
:class="dragging
? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20'
: 'border-gray-300 dark:border-gray-600 hover:border-amber-400 dark:hover:border-amber-500'"
>
<input
x-ref="fileInput"
type="file"
accept=".sql,.txt"
class="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
x-on:change="
const file = $event.target.files[0];
if (file) setFile(file);
"
/>
{{-- Idle state --}}
<div x-show="!fileName" class="space-y-2">
<x-heroicon-o-document-arrow-up class="w-10 h-10 text-gray-400 mx-auto" />
<p class="text-sm font-medium text-gray-600 dark:text-gray-300">
{{ __('navigation.import.drop_or_click') }}
</p>
<p class="text-xs text-gray-400">{{ __('navigation.import.file_hint') }}</p>
</div>
{{-- File selected state --}}
<div x-show="fileName" class="space-y-1">
<x-heroicon-o-document-check class="w-10 h-10 text-amber-500 mx-auto" />
<p class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="fileName"></p>
<p class="text-xs text-gray-400" x-text="fileSize"></p>
<p class="text-xs" :class="uploading ? 'text-blue-500' : 'text-amber-600 dark:text-amber-400'">
<span x-show="uploading">{{ __('navigation.import.uploading') }}</span>
<span x-show="!uploading && uploaded">{{ __('navigation.import.file_ready') }}</span>
</p>
</div>
</div>
@error('sqlFile')
<p class="text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
{{-- Upload progress bar --}}
<div x-show="uploading" class="space-y-1">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-amber-500 h-2 rounded-full transition-all"
:style="'width: ' + uploadProgress + '%'"
></div>
</div>
</div>
{{-- Submit button --}}
<div class="flex justify-end">
<x-filament::button
wire:click="startImport"
wire:loading.attr="disabled"
wire:target="startImport"
color="warning"
icon="heroicon-o-arrow-up-tray"
:disabled="$this->importing"
x-bind:disabled="!uploaded || uploading"
>
<span wire:loading.remove wire:target="startImport">
{{ __('navigation.import.confirm_button') }}
</span>
<span wire:loading wire:target="startImport">
{{ __('navigation.import.processing') }}
</span>
</x-filament::button>
</div>
</div>
</x-filament::card>
@if($report)
<x-filament::card>
<h3 class="font-semibold text-base mb-2">{{ __('navigation.sync.last_report') }}</h3>
<pre class="bg-gray-50 dark:bg-gray-900 border rounded p-3 text-xs leading-loose max-h-64 overflow-y-auto whitespace-pre-wrap">{{ $report }}</pre>
</x-filament::card>
@endif
@if($importReport)
<x-filament::card>
<h3 class="font-semibold text-base mb-2">{{ __('navigation.import.last_report') }}</h3>
<pre class="bg-gray-50 dark:bg-gray-900 border rounded p-3 text-xs leading-loose max-h-64 overflow-y-auto whitespace-pre-wrap">{{ $importReport }}</pre>
</x-filament::card>
@endif
</x-filament-panels::page>