feat: initial project setup with Laravel 12 and Filament v4
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<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',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'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">بیمار‎ ({{ $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,61 @@
|
||||
<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>
|
||||
|
||||
@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
|
||||
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,108 @@
|
||||
@php
|
||||
$photosBeforeRaw = $record->photos_before ?? [];
|
||||
$photosAfterRaw = $record->photos_after ?? [];
|
||||
$videosRaw = $record->videos ?? [];
|
||||
$audioRaw = $record->audio_files ?? [];
|
||||
|
||||
$toUrls = fn($files) => collect((array) $files)
|
||||
->filter()
|
||||
->map(fn($f) => Storage::disk('public')->url($f))
|
||||
->values();
|
||||
|
||||
$photosBefore = $toUrls($photosBeforeRaw);
|
||||
$photosAfter = $toUrls($photosAfterRaw);
|
||||
$videos = $toUrls($videosRaw);
|
||||
$audios = $toUrls($audioRaw);
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6 p-2" dir="rtl">
|
||||
|
||||
{{-- Photos Before --}}
|
||||
@if($photosBefore->isNotEmpty())
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ __('patients.fields.photos_before') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
@foreach($photosBefore as $url)
|
||||
<a href="{{ $url }}" target="_blank">
|
||||
<img src="{{ $url }}" class="w-full h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-700 hover:opacity-80 transition" />
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Photos After --}}
|
||||
@if($photosAfter->isNotEmpty())
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ __('patients.fields.photos_after') }}</h3>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
@foreach($photosAfter as $url)
|
||||
<a href="{{ $url }}" target="_blank">
|
||||
<img src="{{ $url }}" class="w-full h-32 object-cover rounded-lg border border-gray-200 dark:border-gray-700 hover:opacity-80 transition" />
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Videos --}}
|
||||
@if($videos->isNotEmpty())
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ __('patients.fields.videos') }}</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($videos as $i => $url)
|
||||
@php $vid = 'patient-video-'.$i.'-'.uniqid(); @endphp
|
||||
<div class="rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 bg-black">
|
||||
<video id="{{ $vid }}" src="{{ $url }}" controls class="w-full max-h-64"></video>
|
||||
<div class="flex items-center gap-2 bg-gray-900 px-3 py-1">
|
||||
<span class="text-xs text-gray-400">سرعت:</span>
|
||||
@foreach([0.5, 1, 1.5, 2] as $speed)
|
||||
<button
|
||||
onclick="(function(){ var v=document.getElementById('{{ $vid }}'); v.playbackRate={{ $speed }}; this.closest('.flex').querySelectorAll('[data-speed]').forEach(function(b){b.classList.remove('bg-blue-600','text-white'); b.classList.add('text-gray-300');}); this.classList.add('bg-blue-600','text-white'); this.classList.remove('text-gray-300'); }).call(this)"
|
||||
data-speed="{{ $speed }}"
|
||||
class="text-xs px-2 py-0.5 rounded {{ $speed == 1 ? 'bg-blue-600 text-white' : 'text-gray-300' }} hover:bg-blue-500 hover:text-white transition"
|
||||
>{{ $speed }}x</button>
|
||||
@endforeach
|
||||
<a href="{{ $url }}" download class="mr-auto text-gray-400 hover:text-white transition" title="دانلود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Audio Files --}}
|
||||
@if($audios->isNotEmpty())
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ __('patients.fields.audio_files') }}</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($audios as $i => $url)
|
||||
@php $aid = 'patient-audio-'.$i.'-'.uniqid(); @endphp
|
||||
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 p-3">
|
||||
<audio id="{{ $aid }}" src="{{ $url }}" controls class="w-full mb-2"></audio>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">سرعت:</span>
|
||||
@foreach([0.5, 1, 1.5, 2] as $speed)
|
||||
<button
|
||||
onclick="(function(){ var a=document.getElementById('{{ $aid }}'); a.playbackRate={{ $speed }}; this.closest('.flex').querySelectorAll('[data-speed]').forEach(function(b){b.classList.remove('bg-blue-600','text-white'); b.classList.add('text-gray-500');}); this.classList.add('bg-blue-600','text-white'); this.classList.remove('text-gray-500'); }).call(this)"
|
||||
data-speed="{{ $speed }}"
|
||||
class="text-xs px-2 py-0.5 rounded {{ $speed == 1 ? 'bg-blue-600 text-white' : 'text-gray-500' }} hover:bg-blue-500 hover:text-white transition"
|
||||
>{{ $speed }}x</button>
|
||||
@endforeach
|
||||
<a href="{{ $url }}" download class="mr-auto text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition" title="دانلود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user