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,89 @@
@props([
'heading' => 'تایید عملیات',
'description' => 'آیا از انجام این عملیات مطمئنید؟',
'confirmLabel' => 'بله، انجام شود',
'cancelLabel' => 'انصراف',
'event' => 'confirm-modal',
'color' => 'danger', {{-- danger | primary | warning --}}
])
@php
$colors = [
'danger' => ['bg' => 'bg-danger-50 dark:bg-danger-900/30', 'icon' => 'text-danger-500', 'btn' => 'bg-danger-600 hover:bg-danger-700'],
'primary' => ['bg' => 'bg-primary-50 dark:bg-primary-900/30', 'icon' => 'text-primary-500', 'btn' => 'bg-primary-600 hover:bg-primary-700'],
'warning' => ['bg' => 'bg-warning-50 dark:bg-warning-900/30', 'icon' => 'text-warning-500', 'btn' => 'bg-warning-600 hover:bg-warning-700'],
];
$c = $colors[$color] ?? $colors['danger'];
@endphp
<div
x-data="{
show: false,
payload: null,
open(e) { this.payload = e.detail ?? null; this.show = true; },
cancel() { this.show = false; this.payload = null; },
confirm() {
this.$dispatch('{{ $event }}-confirmed', this.payload);
this.show = false;
this.payload = null;
}
}"
x-on:{{ $event }}-open.window="open($event)"
x-on:{{ $event }}-open.document="open($event)"
>
<template x-teleport="body">
<div
x-show="show"
x-transition.opacity
class="fixed inset-0 z-999 flex items-center justify-center bg-black/50 backdrop-blur-sm"
style="display:none"
>
<div
@click.outside="cancel()"
class="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden"
>
{{-- Header --}}
<div class="px-6 pt-6 pb-4">
<div class="flex items-center gap-3 mb-4">
<div class="flex items-center justify-center w-10 h-10 rounded-full {{ $c['bg'] }}">
@if($color === 'danger')
<x-heroicon-o-trash class="w-5 h-5 {{ $c['icon'] }}" />
@elseif($color === 'warning')
<x-heroicon-o-exclamation-triangle class="w-5 h-5 {{ $c['icon'] }}" />
@else
<x-heroicon-o-check-circle class="w-5 h-5 {{ $c['icon'] }}" />
@endif
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ $heading }}
</h3>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ $description }}
</p>
{{-- Optional slot for extra content --}}
@if(isset($extra))
<div class="mt-3">{{ $extra }}</div>
@endif
</div>
{{-- Footer --}}
<div class="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-900/50">
<button
@click="cancel()"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition"
>
{{ $cancelLabel }}
</button>
<button
@click="confirm()"
class="px-4 py-2 text-sm font-medium text-white {{ $c['btn'] }} rounded-lg transition"
>
{{ $confirmLabel }}
</button>
</div>
</div>
</div>
</template>
</div>
@@ -0,0 +1,56 @@
@props([
'title',
'description' => null,
'icon' => 'heroicon-o-squares-2x2',
'url' => '#',
'count' => null,
'color' => 'blue',
])
@php
$headerClass = match ($color) {
'blue', 'primary' => 'bg-blue-600',
'green', 'success' => 'bg-green-600',
'red', 'danger' => 'bg-red-600',
'yellow', 'warning'=> 'bg-amber-500',
'purple' => 'bg-purple-600',
'orange' => 'bg-orange-500',
'teal' => 'bg-teal-600',
'slate' => 'bg-slate-600',
default => 'bg-blue-600',
};
$dir = app()->getLocale() === 'fa' ? 'rtl' : 'ltr';
@endphp
<a href="{{ $url }}" wire:navigate class="group block h-full">
<div class="nav-card group-hover:shadow-md">
<div class="flex items-center gap-4 px-6 py-5 {{ $headerClass }}" dir="{{ $dir }}">
<div class="shrink-0 rounded-lg bg-white/25 p-2">
<x-dynamic-component
:component="$icon"
class="h-5 w-5 text-white"
style="width:1.25rem;height:1.25rem;display:block;color:white"
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-xl font-bold text-white leading-snug">
{{ $title }}
</p>
@if ($count !== null)
<p class="mt-0.5 text-sm text-white/75">{{ number_format($count) }}</p>
@endif
</div>
</div>
<div class="flex-1 px-6 py-5" dir="{{ $dir }}">
@if ($description)
<p class="nav-card-body-text line-clamp-3">
{{ $description }}
</p>
@endif
</div>
</div>
</a>
@@ -0,0 +1,48 @@
<div style="padding-inline-start: 1rem; margin-inline-start: 1rem; border-inline-start: 1px solid rgb(229 231 235);">
<div
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400"
style="padding-block: 0.375rem;"
x-data="appClock('{{ app()->getLocale() }}')"
x-init="start()"
:dir="locale === 'fa' ? 'rtl' : 'ltr'"
>
<span x-text="weekday"></span>
<span x-text="date"></span>
<span x-text="time" class="tabular-nums"></span>
</div>
</div>
@once
<script>
function appClock(locale) {
const isPersian = locale === 'fa';
const fmt = isPersian ? {
weekday: new Intl.DateTimeFormat('fa-IR-u-ca-persian', { timeZone: 'Asia/Tehran', weekday: 'long' }),
date: new Intl.DateTimeFormat('fa-IR-u-ca-persian', { timeZone: 'Asia/Tehran', year: 'numeric', month: 'long', day: 'numeric' }),
time: new Intl.DateTimeFormat('fa-IR', { timeZone: 'Asia/Tehran', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }),
} : {
weekday: new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tehran', weekday: 'long' }),
date: new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tehran', year: 'numeric', month: 'long', day: 'numeric' }),
time: new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Tehran', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }),
};
return {
locale,
weekday: '',
date: '',
time: '',
start() {
this.tick();
setInterval(() => this.tick(), 1000);
},
tick() {
const now = new Date();
this.weekday = fmt.weekday.format(now);
this.date = fmt.date.format(now);
this.time = fmt.time.format(now);
}
};
}
</script>
@endonce
@@ -0,0 +1,248 @@
<div class="flex items-center gap-1">
<button
x-data
@click="$dispatch('theme-changed', $store.theme === 'dark' ? 'light' : 'dark')"
class="flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition"
>
<span x-show="$store.theme === 'dark'" x-cloak>
<x-heroicon-o-sun class="w-5 h-5" />
</span>
<span x-show="$store.theme !== 'dark'" x-cloak>
<x-heroicon-o-moon class="w-5 h-5" />
</span>
</button>
@can('View:SyncPage')
<div
class="relative"
x-data="{
open: false,
showConfirm: false,
showProgress: false,
progress: 0,
statusText: '',
done: false,
failed: false,
reportLines: [],
startSync() {
this.showConfirm = false;
this.showProgress = true;
this.progress = 0;
this.statusText = 'در حال اتصال به سیستم مقابل...';
this.done = false;
this.failed = false;
this.reportLines = [];
// Simulate initial progress
this.progress = 15;
let progressInterval = setInterval(() => {
if (this.progress < 85) {
this.progress += Math.random() * 10;
if (this.progress > 40 && this.progress < 60) {
this.statusText = 'در حال دریافت تغییرات...';
} else if (this.progress >= 60) {
this.statusText = 'در حال ارسال تغییرات...';
}
}
}, 500);
fetch('{{ route('sync.run') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || '{{ csrf_token() }}',
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(data => {
clearInterval(progressInterval);
this.progress = 100;
this.reportLines = data.report || [];
if (data.success) {
this.statusText = 'همگام‌سازی با موفقیت انجام شد';
this.done = true;
} else {
this.statusText = 'همگام‌سازی با خطا مواجه شد';
this.failed = true;
}
})
.catch(err => {
clearInterval(progressInterval);
this.progress = 100;
this.statusText = 'خطا در اتصال به سرور';
this.failed = true;
this.reportLines = [err.message || 'خطای ناشناخته'];
});
},
closeModal() {
this.showProgress = false;
this.showConfirm = false;
if (this.done) {
window.location.reload();
}
}
}"
@click.outside="open = false"
>
<button
@click="open = !open"
class="flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition"
title="{{ __('navigation.groups.system') }}"
>
<x-heroicon-o-cog-6-tooth class="w-5 h-5" />
</button>
<div
x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute inset-s-0 mt-1 w-52 rounded-xl bg-white dark:bg-gray-800 shadow-lg ring-1 ring-gray-200 dark:ring-gray-700 z-50 py-1"
style="display:none"
>
@can('View:SyncPage')
<button
type="button"
@click="open = false; showConfirm = true"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition w-full"
>
<x-heroicon-o-arrows-right-left class="w-4 h-4 text-primary-500 shrink-0" />
{{ __('navigation.sync.label') }}
</button>
@endcan
@can('View:SyncPage')
<a
href="{{ \App\Filament\Pages\SyncPage::getUrl() }}"
@click="open = false"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition"
>
<x-heroicon-o-document-magnifying-glass class="w-4 h-4 text-info-500 shrink-0" />
{{ __('navigation.sync.details') }}
</a>
@endcan
@can('View:SyncPage')
<a
href="{{ route('backup.download') }}"
@click="open = false"
class="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition"
>
<x-heroicon-o-arrow-down-tray class="w-4 h-4 text-success-500 shrink-0" />
{{ __('navigation.backup.label') }}
</a>
@endcan
</div>
<template x-teleport="body">
<div
x-show="showConfirm"
x-transition.opacity
class="fixed inset-0 z-999 flex items-center justify-center bg-black/50 backdrop-blur-sm"
style="display:none"
>
<div
@click.outside="showConfirm = false"
class="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden"
>
<div class="px-6 pt-6 pb-4">
<div class="flex items-center gap-3 mb-4">
<div class="flex items-center justify-center w-10 h-10 rounded-full bg-primary-50 dark:bg-primary-900/30">
<x-heroicon-o-arrows-right-left class="w-5 h-5 text-primary-500" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">همگام‌سازی دو طرفه</h3>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
اطلاعات این سیستم با سیستم مقابل همگام‌سازی خواهد شد. آیا مطمئنید؟
</p>
</div>
<div class="flex items-center justify-end gap-3 px-6 py-4 bg-gray-50 dark:bg-gray-900/50">
<button
@click="showConfirm = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition"
>
انصراف
</button>
<button
@click="startSync()"
class="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 transition"
>
بله، همگام کن
</button>
</div>
</div>
</div>
</template>
<template x-teleport="body">
<div
x-show="showProgress"
x-transition.opacity
class="fixed inset-0 z-999 flex items-center justify-center bg-black/50 backdrop-blur-sm"
style="display:none"
>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
<div class="px-6 pt-6 pb-4">
<div class="flex items-center gap-3 mb-5">
<div class="flex items-center justify-center w-10 h-10 rounded-full"
:class="failed ? 'bg-danger-50 dark:bg-danger-900/30' : (done ? 'bg-success-50 dark:bg-success-900/30' : 'bg-primary-50 dark:bg-primary-900/30')">
<template x-if="!done && !failed">
<svg class="w-5 h-5 text-primary-500 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
</template>
<template x-if="done">
<x-heroicon-o-check-circle class="w-5 h-5 text-success-500" />
</template>
<template x-if="failed">
<x-heroicon-o-x-circle class="w-5 h-5 text-danger-500" />
</template>
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">همگام‌سازی</h3>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 mb-3 overflow-hidden">
<div
class="h-3 rounded-full transition-all duration-500 ease-out"
:class="failed ? 'bg-danger-500' : (done ? 'bg-success-500' : 'bg-primary-500')"
:style="'width: ' + Math.min(progress, 100) + '%'"
></div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-1" x-text="statusText"></p>
<p class="text-xs text-gray-400 dark:text-gray-500" x-text="Math.round(Math.min(progress, 100)) + '%'"></p>
<template x-if="reportLines.length > 0">
<div class="mt-4 max-h-40 overflow-y-auto bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3">
<template x-for="(line, i) in reportLines" :key="i">
<p class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed" x-text="line"></p>
</template>
</div>
</template>
</div>
<div class="flex items-center justify-end px-6 py-4 bg-gray-50 dark:bg-gray-900/50">
<button
x-show="done || failed"
@click="closeModal()"
class="px-4 py-2 text-sm font-medium text-white rounded-lg transition"
:class="done ? 'bg-success-600 hover:bg-success-700' : 'bg-gray-600 hover:bg-gray-700'"
x-text="done ? 'بستن' : 'بستن'"
></button>
</div>
</div>
</div>
</template>
</div>
@endcan
</div>