feat: add lab tests, patient file management, and prescription improvements
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
use App\Models\SurgeryCenter;
|
||||
use App\Models\Treatment;
|
||||
use App\Models\Visit;
|
||||
use App\Observers\PatientIllnessObserver;
|
||||
use App\Observers\PatientObserver;
|
||||
use App\Observers\SyncObserver;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
@@ -27,12 +29,14 @@ public function boot(): void
|
||||
$observer = new SyncObserver();
|
||||
|
||||
Patient::observe($observer);
|
||||
Patient::observe(PatientObserver::class);
|
||||
Doctor::observe($observer);
|
||||
Treatment::observe($observer);
|
||||
Medication::observe($observer);
|
||||
PaymentType::observe($observer);
|
||||
SurgeryCenter::observe($observer);
|
||||
PatientIllness::observe($observer);
|
||||
PatientIllness::observe(PatientIllnessObserver::class);
|
||||
Prescription::observe($observer);
|
||||
SurgeryAppointment::observe($observer);
|
||||
Visit::observe($observer);
|
||||
|
||||
@@ -56,6 +56,21 @@ public function panel(Panel $panel): Panel
|
||||
->brandName('')
|
||||
->favicon(asset('favicon.svg'))
|
||||
->font('Vazirmatn', url: \Illuminate\Support\Facades\Vite::asset('resources/css/vazirmatn.css'), provider: \Filament\FontProviders\LocalFontProvider::class)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_START,
|
||||
fn (): string => <<<'HTML'
|
||||
<script>
|
||||
(function () {
|
||||
if (localStorage.getItem('isOpen') === null) {
|
||||
localStorage.setItem('isOpen', 'false');
|
||||
}
|
||||
if (localStorage.getItem('isOpenDesktop') === null) {
|
||||
localStorage.setItem('isOpenDesktop', 'false');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
HTML,
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::TOPBAR_LOGO_AFTER,
|
||||
fn (): string => view('components.persian-datetime')->render(),
|
||||
@@ -87,6 +102,212 @@ public function panel(Panel $panel): Panel
|
||||
</script>
|
||||
HTML,
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn (): string => <<<'HTML'
|
||||
<div id="fi-lightbox">
|
||||
<div id="fi-lb-wrap">
|
||||
<img id="fi-lb-img" src="" alt="">
|
||||
<button id="fi-lb-close" title="بستن">✕</button>
|
||||
<button id="fi-lb-prev" title="قبلی">‹</button>
|
||||
<button id="fi-lb-next" title="بعدی">›</button>
|
||||
<div id="fi-lb-counter">1 / 1</div>
|
||||
<div id="fi-lb-hint">اسکرول: زوم | دابلکلیک: ریست</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var lb = document.getElementById('fi-lightbox');
|
||||
var lbWrap = document.getElementById('fi-lb-wrap');
|
||||
var lbImg = document.getElementById('fi-lb-img');
|
||||
var lbCnt = document.getElementById('fi-lb-counter');
|
||||
var lbPrev = document.getElementById('fi-lb-prev');
|
||||
var lbNext = document.getElementById('fi-lb-next');
|
||||
var lbClose = document.getElementById('fi-lb-close');
|
||||
if (!lb) return;
|
||||
|
||||
var images = [], currentIdx = 0, zoom = 1;
|
||||
var isPanning = false, panStartX = 0, panStartY = 0, panX = 0, panY = 0;
|
||||
|
||||
function applyTransform() {
|
||||
lbImg.style.transform = 'scale(' + zoom + ') translate(' + panX + 'px, ' + panY + 'px)';
|
||||
lbImg.classList.toggle('lb-zoomed', zoom > 1);
|
||||
}
|
||||
|
||||
function showLightbox(imgs, idx) {
|
||||
images = imgs;
|
||||
currentIdx = idx;
|
||||
zoom = 1; panX = 0; panY = 0;
|
||||
applyTransform();
|
||||
updateCounter();
|
||||
loadImage();
|
||||
lb.classList.add('lb-open');
|
||||
document.addEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
function hideLightbox() {
|
||||
lb.classList.remove('lb-open');
|
||||
document.removeEventListener('keydown', onKey);
|
||||
lbImg.src = '';
|
||||
}
|
||||
|
||||
function navigate(dir) {
|
||||
if (images.length < 2) return;
|
||||
currentIdx = (currentIdx + dir + images.length) % images.length;
|
||||
zoom = 1; panX = 0; panY = 0;
|
||||
applyTransform();
|
||||
loadImage();
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
function loadImage() {
|
||||
lbImg.style.opacity = '0';
|
||||
var src = images[currentIdx];
|
||||
var tmp = new Image();
|
||||
tmp.onload = function () { lbImg.src = src; lbImg.style.opacity = '1'; };
|
||||
tmp.onerror = function () { lbImg.src = src; lbImg.style.opacity = '1'; };
|
||||
tmp.src = src;
|
||||
}
|
||||
|
||||
function updateCounter() {
|
||||
lbCnt.textContent = (currentIdx + 1) + ' / ' + images.length;
|
||||
var show = images.length > 1;
|
||||
lbPrev.style.visibility = show ? '' : 'hidden';
|
||||
lbNext.style.visibility = show ? '' : 'hidden';
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') hideLightbox();
|
||||
else if (e.key === 'ArrowLeft') navigate(-1);
|
||||
else if (e.key === 'ArrowRight') navigate(1);
|
||||
else if (e.key === '+' || e.key === '=') { zoom = Math.min(8, zoom + 0.2); applyTransform(); }
|
||||
else if (e.key === '-') { zoom = Math.max(0.3, zoom - 0.2); if (zoom <= 1) { panX = 0; panY = 0; } applyTransform(); }
|
||||
else if (e.key === '0') { zoom = 1; panX = 0; panY = 0; applyTransform(); }
|
||||
}
|
||||
|
||||
lbWrap.addEventListener('wheel', function (e) {
|
||||
e.preventDefault();
|
||||
zoom = Math.max(0.3, Math.min(8, zoom + (e.deltaY < 0 ? 0.1 : -0.1)));
|
||||
if (zoom <= 1) { panX = 0; panY = 0; }
|
||||
applyTransform();
|
||||
}, { passive: false });
|
||||
|
||||
lbImg.addEventListener('dblclick', function () {
|
||||
zoom = 1; panX = 0; panY = 0; applyTransform();
|
||||
});
|
||||
|
||||
lbImg.addEventListener('mousedown', function (e) {
|
||||
if (zoom <= 1) return;
|
||||
isPanning = true;
|
||||
panStartX = e.clientX - panX * zoom;
|
||||
panStartY = e.clientY - panY * zoom;
|
||||
lbImg.classList.add('lb-dragging');
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!isPanning) return;
|
||||
panX = (e.clientX - panStartX) / zoom;
|
||||
panY = (e.clientY - panStartY) / zoom;
|
||||
lbImg.style.transform = 'scale(' + zoom + ') translate(' + panX + 'px, ' + panY + 'px)';
|
||||
});
|
||||
document.addEventListener('mouseup', function () {
|
||||
if (isPanning) { isPanning = false; lbImg.classList.remove('lb-dragging'); }
|
||||
});
|
||||
|
||||
lbWrap.addEventListener('click', function (e) { if (e.target === lbWrap) hideLightbox(); });
|
||||
lbClose.addEventListener('click', hideLightbox);
|
||||
lbPrev.addEventListener('click', function () { navigate(-1); });
|
||||
lbNext.addEventListener('click', function () { navigate(1); });
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var thumb = e.target.closest('[data-lb-src]');
|
||||
if (!thumb) return;
|
||||
e.preventDefault();
|
||||
var group = thumb.getAttribute('data-lb-group');
|
||||
var all = group
|
||||
? Array.from(document.querySelectorAll('[data-lb-group="' + group + '"]'))
|
||||
: [thumb];
|
||||
var urls = all.map(function (el) { return el.getAttribute('data-lb-src'); });
|
||||
var idx = all.indexOf(thumb);
|
||||
showLightbox(urls, Math.max(0, idx));
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var preview = e.target.closest('.filepond--image-preview-wrapper, .filepond--image-preview');
|
||||
if (!preview) return;
|
||||
|
||||
var item = preview.closest('.filepond--item');
|
||||
var root = item && item.closest('.filepond--root');
|
||||
if (!item || !root) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var allItems = Array.from(root.querySelectorAll('.filepond--item'));
|
||||
var clickedIdx = allItems.indexOf(item);
|
||||
|
||||
var pond = null;
|
||||
var alpineEl = root.closest('[x-data]');
|
||||
if (alpineEl && window.Alpine) {
|
||||
try { var ad = Alpine.$data(alpineEl); if (ad && ad.pond) pond = ad.pond; } catch (e1) {}
|
||||
}
|
||||
if (!pond) {
|
||||
var inp = root.querySelector('input[type="file"], input.filepond');
|
||||
if (inp && inp._filepond) pond = inp._filepond;
|
||||
}
|
||||
if (pond) {
|
||||
try {
|
||||
var pondFiles = pond.getFiles();
|
||||
var urls1 = [];
|
||||
pondFiles.forEach(function (f) {
|
||||
var sid = f.serverId;
|
||||
if (!sid) return;
|
||||
if (typeof sid !== 'string') { try { sid = JSON.parse(sid); } catch(pe) {} }
|
||||
if (typeof sid === 'string') {
|
||||
urls1.push(sid.startsWith('http') || sid.startsWith('/') ? sid : '/storage/' + sid);
|
||||
}
|
||||
});
|
||||
if (urls1.length) { showLightbox(urls1, Math.min(clickedIdx, urls1.length - 1)); return; }
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
var wireInput = root.querySelector('input[wire\\:model], input[wire\\:model\\.live], input[wire\\:model\\.blur]');
|
||||
var wireModel = wireInput && (wireInput.getAttribute('wire:model') || wireInput.getAttribute('wire:model.live') || wireInput.getAttribute('wire:model.blur'));
|
||||
var wireEl = root.closest('[wire\\:id]');
|
||||
var wireId = wireEl && wireEl.getAttribute('wire:id');
|
||||
if (wireModel && wireId && window.Livewire) {
|
||||
try {
|
||||
var comp = window.Livewire.find(wireId);
|
||||
var files = comp.$wire.get(wireModel);
|
||||
if (files) {
|
||||
var arr = Array.isArray(files) ? files : Object.values(files);
|
||||
if (arr.length) {
|
||||
showLightbox(arr.map(function (f) { return '/storage/' + f; }), Math.min(clickedIdx, arr.length - 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e3) {}
|
||||
}
|
||||
|
||||
var imgs = [], fbIdx = 0;
|
||||
allItems.forEach(function (it) {
|
||||
var canvas = it.querySelector('canvas');
|
||||
if (canvas) {
|
||||
try {
|
||||
var src = canvas.toDataURL('image/jpeg', 0.95);
|
||||
if (it === item) fbIdx = imgs.length;
|
||||
imgs.push(src);
|
||||
} catch (ex) {}
|
||||
}
|
||||
});
|
||||
if (imgs.length) showLightbox(imgs, fbIdx);
|
||||
}, true);
|
||||
})();
|
||||
</script>
|
||||
HTML,
|
||||
)
|
||||
->userMenuItems([
|
||||
Action::make('switch_language')
|
||||
->label(fn () => app()->getLocale() === 'fa' ? 'English' : 'فارسی')
|
||||
|
||||
Reference in New Issue
Block a user