Compare commits
No commits in common. "main" and "dev" have entirely different histories.
12
.env.example
12
.env.example
|
|
@ -1,7 +1,7 @@
|
||||||
APP_NAME=Laravel
|
APP_NAME=Laravel
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=base64:Cwol1ZZBqRAY2Em+k6lDh8PvD2RkmfSCSR5c3t+ypjg=
|
APP_KEY=base64:Cwol1ZZBqRAY2Em+k6lDh8PvD2RkmfSCSR5c3t+ypjg=
|
||||||
APP_DEBUG=false
|
APP_DEBUG=true
|
||||||
|
|
||||||
APP_LOCALE=fa
|
APP_LOCALE=fa
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|
@ -10,14 +10,14 @@ APP_FAKER_LOCALE=en_US
|
||||||
APP_MAINTENANCE_DRIVER=file
|
APP_MAINTENANCE_DRIVER=file
|
||||||
# APP_MAINTENANCE_STORE=database
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
PHP_CLI_SERVER_WORKERS=4
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
LOG_CHANNEL=stack
|
||||||
LOG_STACK=single
|
LOG_STACK=single
|
||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=warning
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=sqlite
|
||||||
# DB_HOST=127.0.0.1
|
# DB_HOST=127.0.0.1
|
||||||
|
|
@ -26,7 +26,7 @@ DB_CONNECTION=sqlite
|
||||||
# DB_USERNAME=root
|
# DB_USERNAME=root
|
||||||
# DB_PASSWORD=
|
# DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=file
|
SESSION_DRIVER=database
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
|
|
@ -36,7 +36,7 @@ BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
CACHE_STORE=file
|
CACHE_STORE=database
|
||||||
# CACHE_PREFIX=
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
@ -63,7 +63,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
# APP_URL auto-detected from request — see AppServiceProvider
|
APP_URL=http://localhost:8000
|
||||||
|
|
||||||
SYNC_PEER_IP=192.168.1.8
|
SYNC_PEER_IP=192.168.1.8
|
||||||
SYNC_PEER_PORT=8000
|
SYNC_PEER_PORT=8000
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\Infolists\Components;
|
|
||||||
|
|
||||||
use Filament\Infolists\Components\Entry;
|
|
||||||
use Filament\Support\Components\Contracts\HasEmbeddedView;
|
|
||||||
|
|
||||||
class ExpandableHtmlEntry extends Entry implements HasEmbeddedView
|
|
||||||
{
|
|
||||||
protected int $limit = 200;
|
|
||||||
|
|
||||||
public function limit(int $limit): static
|
|
||||||
{
|
|
||||||
$this->limit = $limit;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toEmbeddedHtml(): string
|
|
||||||
{
|
|
||||||
$state = $this->getState() ?? '';
|
|
||||||
$plain = strip_tags($state);
|
|
||||||
|
|
||||||
if (! $plain) {
|
|
||||||
return $this->wrapEmbeddedHtml('<span class="text-sm text-gray-400">-</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mb_strlen($plain) <= $this->limit) {
|
|
||||||
return $this->wrapEmbeddedHtml('<div class="prose max-w-none">' . $state . '</div>');
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = view('livewire.expandable-text-embed', [
|
|
||||||
'html' => $state,
|
|
||||||
'key' => 'et-' . substr(md5($state), 0, 8),
|
|
||||||
])->render();
|
|
||||||
|
|
||||||
return $this->wrapEmbeddedHtml($html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Infolists\Components;
|
|
||||||
|
|
||||||
use Filament\Infolists\Components\Entry;
|
|
||||||
use Filament\Support\Components\Contracts\HasEmbeddedView;
|
|
||||||
|
|
||||||
class MediaFilesEntry extends Entry implements HasEmbeddedView
|
|
||||||
{
|
|
||||||
public function toEmbeddedHtml(): string
|
|
||||||
{
|
|
||||||
$record = $this->getRecord();
|
|
||||||
$field = $this->getName();
|
|
||||||
|
|
||||||
if (! $record) {
|
|
||||||
return $this->wrapEmbeddedHtml('<span class="text-sm text-gray-400">-</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = $record->{$field} ?? [];
|
|
||||||
$files = is_array($files) ? array_filter($files) : [];
|
|
||||||
|
|
||||||
if (empty($files)) {
|
|
||||||
return $this->wrapEmbeddedHtml('<span class="text-sm text-gray-400">-</span>');
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = view('filament.infolists.media-player-embed', [
|
|
||||||
'patientId' => $record->id,
|
|
||||||
'field' => $field,
|
|
||||||
])->render();
|
|
||||||
|
|
||||||
return $this->wrapEmbeddedHtml($html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Infolists\Components;
|
|
||||||
|
|
||||||
use Filament\Infolists\Components\Entry;
|
|
||||||
use Filament\Support\Components\Contracts\HasEmbeddedView;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
class PhotoGalleryEntry extends Entry implements HasEmbeddedView
|
|
||||||
{
|
|
||||||
public function toEmbeddedHtml(): string
|
|
||||||
{
|
|
||||||
$images = $this->getState() ?? [];
|
|
||||||
$images = is_array($images) ? array_values(array_filter($images)) : [];
|
|
||||||
$urls = array_values(array_map(
|
|
||||||
fn ($path) => Storage::disk('public')->url($path),
|
|
||||||
$images
|
|
||||||
));
|
|
||||||
|
|
||||||
if (empty($urls)) {
|
|
||||||
$html = '<span class="text-sm text-gray-400">-</span>';
|
|
||||||
|
|
||||||
return $this->wrapEmbeddedHtml($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
$record = $this->getRecord();
|
|
||||||
$caption = match ($this->getName()) {
|
|
||||||
'photos_before' => $record?->surgery_before,
|
|
||||||
'photos_after' => $record?->sergery_after,
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
$section = match ($this->getName()) {
|
|
||||||
'photos_before' => 'before',
|
|
||||||
'photos_after' => 'after',
|
|
||||||
default => '',
|
|
||||||
};
|
|
||||||
$captionAttr = $caption ? ' data-lb-caption="' . htmlspecialchars($caption, ENT_QUOTES, 'UTF-8') . '"' : '';
|
|
||||||
$sectionAttr = $section ? ' data-lb-section="' . $section . '"' : '';
|
|
||||||
|
|
||||||
$group = 'gallery-' . md5(implode(',', $urls));
|
|
||||||
$items = '';
|
|
||||||
|
|
||||||
foreach ($urls as $idx => $url) {
|
|
||||||
$esc = e($url);
|
|
||||||
$items .= <<<HTML
|
|
||||||
<div
|
|
||||||
class="relative cursor-pointer rounded-lg overflow-hidden border border-gray-200 hover:border-primary-400 hover:opacity-80 transition-all shadow-sm group"
|
|
||||||
style="aspect-ratio:16/9;"
|
|
||||||
data-lb-group="{$group}"
|
|
||||||
data-lb-idx="{$idx}"
|
|
||||||
data-lb-src="{$esc}"{$captionAttr}{$sectionAttr}
|
|
||||||
>
|
|
||||||
<img src="{$esc}" class="w-full h-full object-cover" alt="" loading="lazy" />
|
|
||||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition duration-200"></div>
|
|
||||||
</div>
|
|
||||||
HTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = '<div class="grid grid-cols-4 gap-2">' . $items . '</div>';
|
|
||||||
|
|
||||||
return $this->wrapEmbeddedHtml($html);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,26 +20,18 @@
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Actions\DeleteBulkAction;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Actions\ViewAction;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Filament\Forms\Components\CheckboxList;
|
use Filament\Forms\Components\CheckboxList;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Radio;
|
use Filament\Forms\Components\Radio;
|
||||||
use App\Filament\RichEditor\HighlightColorPlugin;
|
|
||||||
use Filament\Forms\Components\RichEditor;
|
use Filament\Forms\Components\RichEditor;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TimePicker;
|
use Filament\Forms\Components\TimePicker;
|
||||||
use Filament\Forms\Components\Placeholder;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Forms\Get;
|
|
||||||
use App\Filament\Infolists\Components\ExpandableHtmlEntry;
|
|
||||||
use App\Filament\Infolists\Components\MediaFilesEntry;
|
|
||||||
use App\Filament\Infolists\Components\PhotoGalleryEntry;
|
|
||||||
use Filament\Infolists\Components\TextEntry;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Components\Grid;
|
use Filament\Schemas\Components\Grid;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
|
@ -62,63 +54,9 @@ class PatientResource extends Resource
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'first_name';
|
protected static ?string $recordTitleAttribute = 'first_name';
|
||||||
|
|
||||||
public static function previewHighlightedHtml(string $state, int $limit = 30): string
|
|
||||||
{
|
|
||||||
$plain = strip_tags($state);
|
|
||||||
if (! $plain) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
$suffix = '<span class="text-primary-600 text-xs font-semibold cursor-pointer">... بیشتر</span>';
|
|
||||||
|
|
||||||
$preview = preg_replace('/<(?!\/?span[^>]*data-hbg|\/?mark\b)[^>]+>/i', '', $state);
|
|
||||||
$preview = trim(preg_replace('/\s+/', ' ', $preview));
|
|
||||||
|
|
||||||
if (mb_strlen($plain) <= $limit) {
|
|
||||||
return $preview;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = '';
|
|
||||||
$count = 0;
|
|
||||||
$inTag = false;
|
|
||||||
$tagBuf = '';
|
|
||||||
$openSpans = 0;
|
|
||||||
|
|
||||||
foreach (mb_str_split($preview) as $ch) {
|
|
||||||
if ($count >= $limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ($ch === '<') {
|
|
||||||
$inTag = true;
|
|
||||||
$tagBuf = '<';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($inTag) {
|
|
||||||
$tagBuf .= $ch;
|
|
||||||
if ($ch === '>') {
|
|
||||||
$inTag = false;
|
|
||||||
$result .= $tagBuf;
|
|
||||||
if (str_contains($tagBuf, '</')) {
|
|
||||||
$openSpans = max(0, $openSpans - 1);
|
|
||||||
} else {
|
|
||||||
$openSpans++;
|
|
||||||
}
|
|
||||||
$tagBuf = '';
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$result .= $ch;
|
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result .= str_repeat('</span>', $openSpans);
|
|
||||||
|
|
||||||
return $result . ' ' . $suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getGloballySearchableAttributes(): array
|
public static function getGloballySearchableAttributes(): array
|
||||||
{
|
{
|
||||||
return ['first_name', 'last_name', 'hand_phone', 'icno'];
|
return ['first_name', 'last_name', 'hand_phone'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
|
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
|
||||||
|
|
@ -195,15 +133,7 @@ public static function form(Schema $schema): Schema
|
||||||
TextInput::make('icno')
|
TextInput::make('icno')
|
||||||
->label(__('patients.fields.icno'))
|
->label(__('patients.fields.icno'))
|
||||||
->required()
|
->required()
|
||||||
->maxLength(11)
|
->maxLength(11),
|
||||||
->unique(
|
|
||||||
table: Patient::class,
|
|
||||||
column: 'icno',
|
|
||||||
ignorable: fn ($record) => $record,
|
|
||||||
)
|
|
||||||
->validationMessages([
|
|
||||||
'unique' => __('patients.validation.icno_unique'),
|
|
||||||
]),
|
|
||||||
|
|
||||||
TextInput::make('hand_phone')
|
TextInput::make('hand_phone')
|
||||||
->label(__('patients.fields.hand_phone'))
|
->label(__('patients.fields.hand_phone'))
|
||||||
|
|
@ -213,10 +143,6 @@ public static function form(Schema $schema): Schema
|
||||||
])
|
])
|
||||||
->maxLength(50),
|
->maxLength(50),
|
||||||
|
|
||||||
TextInput::make('home_phone')
|
|
||||||
->label(__('patients.fields.home_phone'))
|
|
||||||
->maxLength(50),
|
|
||||||
|
|
||||||
TextInput::make('age')
|
TextInput::make('age')
|
||||||
->label(__('patients.fields.age'))
|
->label(__('patients.fields.age'))
|
||||||
->numeric()
|
->numeric()
|
||||||
|
|
@ -252,6 +178,10 @@ public static function form(Schema $schema): Schema
|
||||||
->label(__('patients.fields.job'))
|
->label(__('patients.fields.job'))
|
||||||
->maxLength(50),
|
->maxLength(50),
|
||||||
|
|
||||||
|
TextInput::make('home_phone')
|
||||||
|
->label(__('patients.fields.home_phone'))
|
||||||
|
->maxLength(50),
|
||||||
|
|
||||||
TextInput::make('work_phone')
|
TextInput::make('work_phone')
|
||||||
->label(__('patients.fields.work_phone'))
|
->label(__('patients.fields.work_phone'))
|
||||||
->maxLength(50),
|
->maxLength(50),
|
||||||
|
|
@ -357,10 +287,9 @@ public static function form(Schema $schema): Schema
|
||||||
RichEditor::make('surgery_before')
|
RichEditor::make('surgery_before')
|
||||||
->label(__('patients.fields.surgery_before'))
|
->label(__('patients.fields.surgery_before'))
|
||||||
->extraInputAttributes(['style' => 'min-height: 150px'])
|
->extraInputAttributes(['style' => 'min-height: 150px'])
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
->toolbarButtons([
|
||||||
['bold', 'italic', 'underline', 'strike', 'link'],
|
['bold', 'italic', 'underline', 'strike', 'link'],
|
||||||
['highlightYellow', 'highlightRed'],
|
['highlight'],
|
||||||
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
|
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
|
||||||
['blockquote', 'bulletList', 'orderedList'],
|
['blockquote', 'bulletList', 'orderedList'],
|
||||||
['undo', 'redo'],
|
['undo', 'redo'],
|
||||||
|
|
@ -369,10 +298,9 @@ public static function form(Schema $schema): Schema
|
||||||
RichEditor::make('sergery_after')
|
RichEditor::make('sergery_after')
|
||||||
->label(__('patients.fields.sergery_after'))
|
->label(__('patients.fields.sergery_after'))
|
||||||
->extraInputAttributes(['style' => 'min-height: 150px'])
|
->extraInputAttributes(['style' => 'min-height: 150px'])
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
->toolbarButtons([
|
||||||
['bold', 'italic', 'underline', 'strike', 'link'],
|
['bold', 'italic', 'underline', 'strike', 'link'],
|
||||||
['highlightYellow', 'highlightRed'],
|
['highlight'],
|
||||||
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
|
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
|
||||||
['blockquote', 'bulletList', 'orderedList'],
|
['blockquote', 'bulletList', 'orderedList'],
|
||||||
['undo', 'redo'],
|
['undo', 'redo'],
|
||||||
|
|
@ -388,10 +316,7 @@ public static function form(Schema $schema): Schema
|
||||||
->multiple()
|
->multiple()
|
||||||
->image()
|
->image()
|
||||||
->panelLayout('grid')
|
->panelLayout('grid')
|
||||||
->extraAttributes(fn (string $operation) => $operation === 'view'
|
->extraAttributes(['class' => 'fi-photo-gallery'])
|
||||||
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
|
|
||||||
: ['class' => 'fi-photo-gallery']
|
|
||||||
)
|
|
||||||
->disk('public')
|
->disk('public')
|
||||||
->directory(function ($record) {
|
->directory(function ($record) {
|
||||||
$year = \Morilog\Jalali\Jalalian::now()->getYear();
|
$year = \Morilog\Jalali\Jalalian::now()->getYear();
|
||||||
|
|
@ -400,8 +325,6 @@ public static function form(Schema $schema): Schema
|
||||||
})
|
})
|
||||||
->downloadable()
|
->downloadable()
|
||||||
->openable()
|
->openable()
|
||||||
->disabled(fn (string $operation) => $operation === 'view')
|
|
||||||
->deletable(fn (string $operation) => $operation !== 'view')
|
|
||||||
->fetchFileInformation(false),
|
->fetchFileInformation(false),
|
||||||
|
|
||||||
FileUpload::make('photos_after')
|
FileUpload::make('photos_after')
|
||||||
|
|
@ -409,10 +332,7 @@ public static function form(Schema $schema): Schema
|
||||||
->multiple()
|
->multiple()
|
||||||
->image()
|
->image()
|
||||||
->panelLayout('grid')
|
->panelLayout('grid')
|
||||||
->extraAttributes(fn (string $operation) => $operation === 'view'
|
->extraAttributes(['class' => 'fi-photo-gallery'])
|
||||||
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
|
|
||||||
: ['class' => 'fi-photo-gallery']
|
|
||||||
)
|
|
||||||
->disk('public')
|
->disk('public')
|
||||||
->directory(function ($record) {
|
->directory(function ($record) {
|
||||||
$year = \Morilog\Jalali\Jalalian::now()->getYear();
|
$year = \Morilog\Jalali\Jalalian::now()->getYear();
|
||||||
|
|
@ -421,8 +341,6 @@ public static function form(Schema $schema): Schema
|
||||||
})
|
})
|
||||||
->downloadable()
|
->downloadable()
|
||||||
->openable()
|
->openable()
|
||||||
->disabled(fn (string $operation) => $operation === 'view')
|
|
||||||
->deletable(fn (string $operation) => $operation !== 'view')
|
|
||||||
->fetchFileInformation(false),
|
->fetchFileInformation(false),
|
||||||
|
|
||||||
FileUpload::make('videos')
|
FileUpload::make('videos')
|
||||||
|
|
@ -445,14 +363,8 @@ public static function form(Schema $schema): Schema
|
||||||
])
|
])
|
||||||
->maxSize(102400)
|
->maxSize(102400)
|
||||||
->downloadable()
|
->downloadable()
|
||||||
->openable()
|
|
||||||
->panelLayout('grid')
|
->panelLayout('grid')
|
||||||
->extraAttributes(fn (string $operation) => $operation === 'view'
|
->extraAttributes(['class' => 'fi-video-gallery'])
|
||||||
? ['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true']
|
|
||||||
: ['class' => 'fi-video-gallery']
|
|
||||||
)
|
|
||||||
->disabled(fn (string $operation) => $operation === 'view')
|
|
||||||
->deletable(fn (string $operation) => $operation !== 'view')
|
|
||||||
->fetchFileInformation(false),
|
->fetchFileInformation(false),
|
||||||
|
|
||||||
FileUpload::make('audio_files')
|
FileUpload::make('audio_files')
|
||||||
|
|
@ -472,13 +384,6 @@ public static function form(Schema $schema): Schema
|
||||||
'audio/aiff', 'audio/x-aiff',
|
'audio/aiff', 'audio/x-aiff',
|
||||||
])
|
])
|
||||||
->downloadable()
|
->downloadable()
|
||||||
->openable()
|
|
||||||
->extraAttributes(fn (string $operation) => $operation === 'view'
|
|
||||||
? ['data-hide-dropzone' => 'true']
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
->disabled(fn (string $operation) => $operation === 'view')
|
|
||||||
->deletable(fn (string $operation) => $operation !== 'view')
|
|
||||||
->fetchFileInformation(false),
|
->fetchFileInformation(false),
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
@ -516,186 +421,6 @@ public static function form(Schema $schema): Schema
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->visible($operation === 'edit'),
|
->visible($operation === 'edit'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function infolist(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
Grid::make()
|
|
||||||
->schema([
|
|
||||||
Section::make(__('patients.sections.patient_info'))
|
|
||||||
->schema([
|
|
||||||
TextEntry::make('first_name')
|
|
||||||
->label(__('patients.fields.first_name')),
|
|
||||||
TextEntry::make('last_name')
|
|
||||||
->label(__('patients.fields.last_name')),
|
|
||||||
TextEntry::make('father_name')
|
|
||||||
->label(__('patients.fields.father_name'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('icno')
|
|
||||||
->label(__('patients.fields.icno'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('hand_phone')
|
|
||||||
->label(__('patients.fields.hand_phone'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('home_phone')
|
|
||||||
->label(__('patients.fields.home_phone'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('age')
|
|
||||||
->label(__('patients.fields.age'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('birth_date')
|
|
||||||
->label(__('patients.fields.birth_date'))
|
|
||||||
->placeholder('-')
|
|
||||||
->formatStateUsing(fn ($state) => $state
|
|
||||||
? Jalalian::fromCarbon(\Carbon\Carbon::parse($state))->format('Y/m/d')
|
|
||||||
: '-'
|
|
||||||
),
|
|
||||||
TextEntry::make('gender')
|
|
||||||
->label(__('patients.fields.gender'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('marital_status')
|
|
||||||
->label(__('patients.fields.marital_status'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('insurance_no')
|
|
||||||
->label(__('patients.fields.insurance_no'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('insurance')
|
|
||||||
->label(__('patients.fields.insurance'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('education')
|
|
||||||
->label(__('patients.fields.education'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('job')
|
|
||||||
->label(__('patients.fields.job'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('work_phone')
|
|
||||||
->label(__('patients.fields.work_phone'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('other_phone')
|
|
||||||
->label(__('patients.fields.other_phone'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('home_address')
|
|
||||||
->label(__('patients.fields.home_address'))
|
|
||||||
->columnSpan(2)
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('work_address')
|
|
||||||
->label(__('patients.fields.work_address'))
|
|
||||||
->columnSpan(2)
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('refered_by')
|
|
||||||
->label(__('patients.fields.refered_by'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('doc_id')
|
|
||||||
->label(__('patients.fields.doc_id'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('referal_reason')
|
|
||||||
->label(__('patients.fields.referal_reason'))
|
|
||||||
->columnSpan(2)
|
|
||||||
->placeholder('-'),
|
|
||||||
])
|
|
||||||
->columns(4)
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Section::make(__('patients.sections.illness'))
|
|
||||||
->schema([
|
|
||||||
TextEntry::make('current_illness_1')
|
|
||||||
->label(__('patients.fields.current_illness_1'))
|
|
||||||
->listWithLineBreaks()
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('current_illness_2')
|
|
||||||
->label(__('patients.fields.current_illness_2'))
|
|
||||||
->listWithLineBreaks()
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('blood_sugar')
|
|
||||||
->label(__('patients.fields.blood_sugar'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('blood_pressure')
|
|
||||||
->label(__('patients.fields.blood_pressure'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('is_undercare')
|
|
||||||
->label(__('patients.fields.is_undercare'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('undercare_reason')
|
|
||||||
->label(__('patients.fields.undercare_reason'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('is_usingdrug')
|
|
||||||
->label(__('patients.fields.is_usingdrug'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('underdrug_reason')
|
|
||||||
->label(__('patients.fields.underdrug_reason'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('has_alergyto')
|
|
||||||
->label(__('patients.fields.has_alergyto'))
|
|
||||||
->listWithLineBreaks()
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('alergy_reason')
|
|
||||||
->label(__('patients.fields.alergy_reason'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('description')
|
|
||||||
->label(__('patients.fields.description'))
|
|
||||||
->columnSpanFull()
|
|
||||||
->placeholder('-'),
|
|
||||||
])
|
|
||||||
->columns(2)
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Section::make(__('patients.sections.surgery_notes'))
|
|
||||||
->schema([
|
|
||||||
ExpandableHtmlEntry::make('surgery_before')
|
|
||||||
->label(__('patients.fields.surgery_before')),
|
|
||||||
ExpandableHtmlEntry::make('sergery_after')
|
|
||||||
->label(__('patients.fields.sergery_after')),
|
|
||||||
])
|
|
||||||
->columns(2)
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Section::make(__('patients.sections.media'))
|
|
||||||
->schema([
|
|
||||||
PhotoGalleryEntry::make('photos_before')
|
|
||||||
->label(__('patients.fields.photos_before')),
|
|
||||||
PhotoGalleryEntry::make('photos_after')
|
|
||||||
->label(__('patients.fields.photos_after')),
|
|
||||||
MediaFilesEntry::make('videos')
|
|
||||||
->label(__('patients.fields.videos')),
|
|
||||||
MediaFilesEntry::make('audio_files')
|
|
||||||
->label(__('patients.fields.audio_files')),
|
|
||||||
])
|
|
||||||
->columns(2)
|
|
||||||
->columnSpanFull(),
|
|
||||||
|
|
||||||
Section::make(__('patients.sections.record_info'))
|
|
||||||
->schema([
|
|
||||||
TextEntry::make('creator.name')
|
|
||||||
->label(__('patients.fields.created_by'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('updator.name')
|
|
||||||
->label(__('patients.fields.updated_by'))
|
|
||||||
->placeholder('-'),
|
|
||||||
TextEntry::make('surgeryAppointment.surgery_date')
|
|
||||||
->label(__('patients.fields.surgery_appointment'))
|
|
||||||
->placeholder('-')
|
|
||||||
->formatStateUsing(function ($state) {
|
|
||||||
if (! $state) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Jalalian::fromCarbon(\Carbon\Carbon::parse($state))->format('Y/m/d - H:i');
|
|
||||||
} catch (\Exception) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
TextEntry::make('updated_at')
|
|
||||||
->label(__('patients.fields.updated_at'))
|
|
||||||
->placeholder('-'),
|
|
||||||
])
|
|
||||||
->columns(4)
|
|
||||||
->columnSpanFull(),
|
|
||||||
])
|
])
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
|
|
@ -717,19 +442,13 @@ public static function table(Table $table): Table
|
||||||
->searchable()
|
->searchable()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|
||||||
TextColumn::make('icno')
|
|
||||||
->label(__('patients.fields.icno'))
|
|
||||||
->searchable()
|
|
||||||
->placeholder('-'),
|
|
||||||
TextColumn::make('hand_phone')
|
TextColumn::make('hand_phone')
|
||||||
->label(__('patients.fields.hand_phone'))
|
->label(__('patients.fields.hand_phone'))
|
||||||
->searchable()
|
|
||||||
->placeholder('-'),
|
->placeholder('-'),
|
||||||
|
|
||||||
TextColumn::make('home_phone')
|
TextColumn::make('home_phone')
|
||||||
->label(__('patients.fields.home_phone'))
|
->label(__('patients.fields.home_phone'))
|
||||||
->placeholder('-')
|
->placeholder('-'),
|
||||||
->hidden(),
|
|
||||||
|
|
||||||
TextColumn::make('creator.name')
|
TextColumn::make('creator.name')
|
||||||
->label(__('patients.fields.created_by'))
|
->label(__('patients.fields.created_by'))
|
||||||
|
|
@ -756,7 +475,17 @@ public static function table(Table $table): Table
|
||||||
? ['class' => 'pointer-events-none']
|
? ['class' => 'pointer-events-none']
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
->formatStateUsing(fn ($state) => static::previewHighlightedHtml($state ?? ''))
|
->formatStateUsing(function ($state) {
|
||||||
|
$plain = strip_tags($state ?? '');
|
||||||
|
if (! $plain) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
if (mb_strlen($plain) > 30) {
|
||||||
|
return $state
|
||||||
|
. ' <span class="text-primary-600 text-xs font-semibold cursor-pointer">... بیشتر</span>';
|
||||||
|
}
|
||||||
|
return $state;
|
||||||
|
})
|
||||||
->html()
|
->html()
|
||||||
->action(
|
->action(
|
||||||
Action::make('viewSurgeryBefore')
|
Action::make('viewSurgeryBefore')
|
||||||
|
|
@ -781,7 +510,17 @@ public static function table(Table $table): Table
|
||||||
? ['class' => 'pointer-events-none']
|
? ['class' => 'pointer-events-none']
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
->formatStateUsing(fn ($state) => static::previewHighlightedHtml($state ?? ''))
|
->formatStateUsing(function ($state) {
|
||||||
|
$plain = strip_tags($state ?? '');
|
||||||
|
if (! $plain) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
if (mb_strlen($plain) > 30) {
|
||||||
|
return $state
|
||||||
|
. ' <span class="text-primary-600 text-xs font-semibold cursor-pointer">... بیشتر</span>';
|
||||||
|
}
|
||||||
|
return $state;
|
||||||
|
})
|
||||||
->html()
|
->html()
|
||||||
->action(
|
->action(
|
||||||
Action::make('viewSergeryAfter')
|
Action::make('viewSergeryAfter')
|
||||||
|
|
@ -1110,13 +849,6 @@ public static function table(Table $table): Table
|
||||||
|
|
||||||
\Filament\Forms\Components\RichEditor::make('content')
|
\Filament\Forms\Components\RichEditor::make('content')
|
||||||
->label(__('prescriptions.fields.content'))
|
->label(__('prescriptions.fields.content'))
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
|
||||||
['highlightYellow', 'highlightRed'],
|
|
||||||
['bulletList', 'orderedList'],
|
|
||||||
['undo', 'redo'],
|
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
|
@ -1157,13 +889,6 @@ public static function table(Table $table): Table
|
||||||
|
|
||||||
\Filament\Forms\Components\RichEditor::make('lab_content')
|
\Filament\Forms\Components\RichEditor::make('lab_content')
|
||||||
->label(__('prescriptions.fields.lab_content'))
|
->label(__('prescriptions.fields.lab_content'))
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
|
||||||
['highlightYellow', 'highlightRed'],
|
|
||||||
['bulletList', 'orderedList'],
|
|
||||||
['undo', 'redo'],
|
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
|
@ -1249,18 +974,14 @@ public static function table(Table $table): Table
|
||||||
$record->audio_files,
|
$record->audio_files,
|
||||||
]))
|
]))
|
||||||
)
|
)
|
||||||
->modalWidth(Width::ThreeExtraLarge)
|
->modalWidth(Width::ExtraLarge)
|
||||||
->fillForm(fn (Patient $record): array => [
|
->fillForm(fn (Patient $record): array => [
|
||||||
'photos_before' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_before),
|
'photos_before' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_before),
|
||||||
'photos_after' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_after),
|
'photos_after' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_after),
|
||||||
'videos' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->videos),
|
'videos' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->videos),
|
||||||
'audio_files' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->audio_files),
|
'audio_files' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->audio_files),
|
||||||
'surgery_before' => $record->surgery_before,
|
|
||||||
'sergery_after' => $record->sergery_after,
|
|
||||||
])
|
])
|
||||||
->schema([
|
->schema([
|
||||||
\Filament\Forms\Components\Hidden::make('surgery_before'),
|
|
||||||
\Filament\Forms\Components\Hidden::make('sergery_after'),
|
|
||||||
Grid::make(2)
|
Grid::make(2)
|
||||||
->schema([
|
->schema([
|
||||||
FileUpload::make('photos_before')
|
FileUpload::make('photos_before')
|
||||||
|
|
@ -1277,12 +998,7 @@ public static function table(Table $table): Table
|
||||||
->openable()
|
->openable()
|
||||||
->disabled()
|
->disabled()
|
||||||
->fetchFileInformation(false)
|
->fetchFileInformation(false)
|
||||||
->extraAttributes(fn ($get): array => [
|
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
|
||||||
'class' => 'fi-photo-gallery fi-photo-gallery-view',
|
|
||||||
'data-hide-dropzone' => 'true',
|
|
||||||
'data-lb-section' => 'before',
|
|
||||||
'data-lb-caption' => htmlspecialchars($get('surgery_before') ?? '', ENT_QUOTES, 'UTF-8'),
|
|
||||||
]),
|
|
||||||
|
|
||||||
FileUpload::make('photos_after')
|
FileUpload::make('photos_after')
|
||||||
->label(__('patients.fields.photos_after'))
|
->label(__('patients.fields.photos_after'))
|
||||||
|
|
@ -1298,12 +1014,7 @@ public static function table(Table $table): Table
|
||||||
->openable()
|
->openable()
|
||||||
->disabled()
|
->disabled()
|
||||||
->fetchFileInformation(false)
|
->fetchFileInformation(false)
|
||||||
->extraAttributes(fn ($get): array => [
|
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
|
||||||
'class' => 'fi-photo-gallery fi-photo-gallery-view',
|
|
||||||
'data-hide-dropzone' => 'true',
|
|
||||||
'data-lb-section' => 'after',
|
|
||||||
'data-lb-caption' => htmlspecialchars($get('sergery_after') ?? '', ENT_QUOTES, 'UTF-8'),
|
|
||||||
]),
|
|
||||||
|
|
||||||
FileUpload::make('videos')
|
FileUpload::make('videos')
|
||||||
->label(__('patients.fields.videos'))
|
->label(__('patients.fields.videos'))
|
||||||
|
|
@ -1326,8 +1037,7 @@ public static function table(Table $table): Table
|
||||||
->downloadable()
|
->downloadable()
|
||||||
->disabled()
|
->disabled()
|
||||||
->fetchFileInformation(false)
|
->fetchFileInformation(false)
|
||||||
->panelLayout('grid')
|
->extraAttributes(['data-hide-dropzone' => 'true']),
|
||||||
->extraAttributes(['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true']),
|
|
||||||
|
|
||||||
FileUpload::make('audio_files')
|
FileUpload::make('audio_files')
|
||||||
->label(__('patients.fields.audio_files'))
|
->label(__('patients.fields.audio_files'))
|
||||||
|
|
@ -1366,7 +1076,6 @@ public static function table(Table $table): Table
|
||||||
->modalSubmitAction(false)
|
->modalSubmitAction(false)
|
||||||
->modalCancelActionLabel('بستن'),
|
->modalCancelActionLabel('بستن'),
|
||||||
|
|
||||||
ViewAction::make()->iconButton()->color('gray'),
|
|
||||||
EditAction::make()->iconButton(),
|
EditAction::make()->iconButton(),
|
||||||
DeleteAction::make()->iconButton(),
|
DeleteAction::make()->iconButton(),
|
||||||
], position: \Filament\Tables\Enums\RecordActionsPosition::BeforeCells)
|
], position: \Filament\Tables\Enums\RecordActionsPosition::BeforeCells)
|
||||||
|
|
@ -1491,7 +1200,6 @@ public static function getPages(): array
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListPatients::route('/'),
|
'index' => Pages\ListPatients::route('/'),
|
||||||
'create' => Pages\CreatePatient::route('/create'),
|
'create' => Pages\CreatePatient::route('/create'),
|
||||||
'view' => Pages\ViewPatient::route('/{record}'),
|
|
||||||
'edit' => Pages\EditPatient::route('/{record}/edit'),
|
'edit' => Pages\EditPatient::route('/{record}/edit'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\PatientResource\Pages;
|
|
||||||
|
|
||||||
use App\Filament\Resources\PatientResource;
|
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
|
||||||
use Filament\Support\Enums\Width;
|
|
||||||
|
|
||||||
class ViewPatient extends ViewRecord
|
|
||||||
{
|
|
||||||
protected static string $resource = PatientResource::class;
|
|
||||||
|
|
||||||
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
Action::make('printPrescription')
|
|
||||||
->label(__('prescriptions.actions.print_prescription'))
|
|
||||||
->icon('heroicon-o-printer')
|
|
||||||
->color('info')
|
|
||||||
->visible(fn () => $this->getRecord()->prescriptions()->exists())
|
|
||||||
->url(fn () => route('prescription.print', $this->getRecord()->prescriptions()->latest()->first()))
|
|
||||||
->openUrlInNewTab(),
|
|
||||||
|
|
||||||
Action::make('printAdmission')
|
|
||||||
->label(__('prescriptions.actions.print_admission'))
|
|
||||||
->icon('heroicon-o-printer')
|
|
||||||
->color('warning')
|
|
||||||
->visible(fn () => $this->getRecord()->surgeryAppointment !== null)
|
|
||||||
->url(fn () => route('surgery-appointment.print.admission', $this->getRecord()))
|
|
||||||
->openUrlInNewTab(),
|
|
||||||
|
|
||||||
Action::make('printLab')
|
|
||||||
->label(__('prescriptions.actions.print_lab'))
|
|
||||||
->icon('heroicon-o-beaker')
|
|
||||||
->color('gray')
|
|
||||||
->url(fn () => route('patient.print.lab', $this->getRecord()))
|
|
||||||
->openUrlInNewTab(),
|
|
||||||
|
|
||||||
Action::make('edit')
|
|
||||||
->label(__('filament-actions::edit.single.label'))
|
|
||||||
->icon('heroicon-o-pencil-square')
|
|
||||||
->color('primary')
|
|
||||||
->url(fn () => PatientResource::getUrl('edit', ['record' => $this->getRecord()])),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
use App\Filament\Resources\PatientResource\Pages\CreatePatient;
|
use App\Filament\Resources\PatientResource\Pages\CreatePatient;
|
||||||
use App\Filament\Resources\PatientResource\Pages\EditPatient;
|
use App\Filament\Resources\PatientResource\Pages\EditPatient;
|
||||||
use App\Filament\Resources\PatientResource\Pages\ViewPatient;
|
|
||||||
use App\Models\Doctor;
|
use App\Models\Doctor;
|
||||||
use App\Models\PaymentType;
|
use App\Models\PaymentType;
|
||||||
use App\Models\Treatment;
|
use App\Models\Treatment;
|
||||||
|
|
@ -33,7 +32,7 @@ class VisitsRelationManager extends RelationManager
|
||||||
|
|
||||||
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
|
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
|
||||||
{
|
{
|
||||||
return in_array($pageClass, [CreatePatient::class, EditPatient::class, ViewPatient::class]);
|
return in_array($pageClass, [CreatePatient::class, EditPatient::class]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
|
||||||
|
|
@ -137,18 +136,6 @@ public function form(Schema $schema): Schema
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function jalaliDate(?string $date): string
|
|
||||||
{
|
|
||||||
if (! $date) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return \Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($date))->format('Y/m/d');
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
|
|
@ -232,10 +219,8 @@ public function table(Table $table): Table
|
||||||
->recordActions([
|
->recordActions([
|
||||||
EditAction::make()
|
EditAction::make()
|
||||||
->slideOver()
|
->slideOver()
|
||||||
->modalWidth('4xl')
|
->modalWidth('4xl'),
|
||||||
->modalHeading(fn () => __('filament-actions::edit.single.modal.heading', ['label' => $this->getOwnerRecord()->full_name])),
|
DeleteAction::make(),
|
||||||
DeleteAction::make()
|
|
||||||
->modalHeading(fn () => __('filament-actions::delete.single.modal.heading', ['label' => $this->getOwnerRecord()->full_name])),
|
|
||||||
])
|
])
|
||||||
->toolbarActions([]);
|
->toolbarActions([]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
use Filament\Forms\Components\CheckboxList;
|
use Filament\Forms\Components\CheckboxList;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\Hidden;
|
use Filament\Forms\Components\Hidden;
|
||||||
use App\Filament\RichEditor\HighlightColorPlugin;
|
|
||||||
use Filament\Forms\Components\RichEditor;
|
use Filament\Forms\Components\RichEditor;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
|
|
@ -165,13 +164,6 @@ public static function form(Schema $schema): Schema
|
||||||
|
|
||||||
RichEditor::make('content')
|
RichEditor::make('content')
|
||||||
->label(__('prescriptions.fields.content'))
|
->label(__('prescriptions.fields.content'))
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
|
||||||
['highlightYellow', 'highlightRed'],
|
|
||||||
['bulletList', 'orderedList'],
|
|
||||||
['undo', 'redo'],
|
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
|
@ -232,13 +224,6 @@ public static function form(Schema $schema): Schema
|
||||||
|
|
||||||
RichEditor::make('lab_content')
|
RichEditor::make('lab_content')
|
||||||
->label(__('prescriptions.fields.lab_content'))
|
->label(__('prescriptions.fields.lab_content'))
|
||||||
->plugins([HighlightColorPlugin::make()])
|
|
||||||
->toolbarButtons([
|
|
||||||
['bold', 'italic', 'underline', 'strike'],
|
|
||||||
['highlightYellow', 'highlightRed'],
|
|
||||||
['bulletList', 'orderedList'],
|
|
||||||
['undo', 'redo'],
|
|
||||||
])
|
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,6 @@ class VisitResource extends Resource
|
||||||
|
|
||||||
protected static ?string $recordTitleAttribute = 'visit_date';
|
protected static ?string $recordTitleAttribute = 'visit_date';
|
||||||
|
|
||||||
public static function getRecordTitle(?\Illuminate\Database\Eloquent\Model $record): \Illuminate\Contracts\Support\Htmlable|string|null
|
|
||||||
{
|
|
||||||
if (! $record) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return $record->patientRecord?->full_name ?? $record->visit_date ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getWidgets(): array
|
public static function getWidgets(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
@ -331,14 +323,8 @@ public static function table(Table $table): Table
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->columnSpan(2)
|
->columnSpan(2)
|
||||||
->query(fn ($query, array $data) => $query
|
->query(fn ($query, array $data) => $query
|
||||||
->when($data['from_date'] ?? null, function ($q) use ($data) {
|
->when($data['from_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date']))
|
||||||
$fromDate = \Carbon\Carbon::parse($data['from_date'])->format('Y-m-d');
|
->when($data['to_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date']))
|
||||||
return $q->whereRaw("SUBSTRING(visit_date, 1, 10) >= ?", [$fromDate]);
|
|
||||||
})
|
|
||||||
->when($data['to_date'] ?? null, function ($q) use ($data) {
|
|
||||||
$toDate = \Carbon\Carbon::parse($data['to_date'])->format('Y-m-d');
|
|
||||||
return $q->whereRaw("SUBSTRING(visit_date, 1, 10) <= ?", [$toDate]);
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
->indicateUsing(function (array $data): array {
|
->indicateUsing(function (array $data): array {
|
||||||
$indicators = [];
|
$indicators = [];
|
||||||
|
|
|
||||||
|
|
@ -28,31 +28,21 @@ protected function getHeaderWidgets(): array
|
||||||
public function applyTableFilters(): void
|
public function applyTableFilters(): void
|
||||||
{
|
{
|
||||||
parent::applyTableFilters();
|
parent::applyTableFilters();
|
||||||
$this->applySortFromFilters();
|
|
||||||
$this->dispatchStatsUpdate();
|
$this->dispatchStatsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedTableFilters(): void
|
public function updatedTableFilters(): void
|
||||||
{
|
{
|
||||||
parent::updatedTableFilters();
|
parent::updatedTableFilters();
|
||||||
$this->applySortFromFilters();
|
|
||||||
$this->dispatchStatsUpdate();
|
$this->dispatchStatsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetTableFiltersForm(): void
|
public function resetTableFiltersForm(): void
|
||||||
{
|
{
|
||||||
parent::resetTableFiltersForm();
|
parent::resetTableFiltersForm();
|
||||||
$this->tableSort = null;
|
|
||||||
$this->dispatchStatsUpdate();
|
$this->dispatchStatsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function applySortFromFilters(): void
|
|
||||||
{
|
|
||||||
$filters = $this->tableFilters ?? [];
|
|
||||||
$fromDate = ($filters['visit_date_range']['from_date'] ?? null) ?: null;
|
|
||||||
|
|
||||||
$this->tableSort = $fromDate ? 'visit_date:asc' : null;
|
|
||||||
}
|
|
||||||
protected function dispatchStatsUpdate(): void
|
protected function dispatchStatsUpdate(): void
|
||||||
{
|
{
|
||||||
$filters = $this->tableFilters ?? [];
|
$filters = $this->tableFilters ?? [];
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\RichEditor;
|
|
||||||
|
|
||||||
use Tiptap\Core\Mark;
|
|
||||||
use Tiptap\Utils\HTML;
|
|
||||||
|
|
||||||
class HighlightColorMark extends Mark
|
|
||||||
{
|
|
||||||
public static $name = 'highlightColor';
|
|
||||||
|
|
||||||
public function parseHTML(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
[
|
|
||||||
'tag' => 'span',
|
|
||||||
'getAttrs' => function ($DOMNode) {
|
|
||||||
$color = $DOMNode->getAttribute('data-hbg');
|
|
||||||
if (! $color) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ['data-hbg' => $color];
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function addAttributes(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'data-hbg' => [
|
|
||||||
'default' => null,
|
|
||||||
'parseHTML' => function ($DOMNode) {
|
|
||||||
return $DOMNode->getAttribute('data-hbg') ?: null;
|
|
||||||
},
|
|
||||||
'renderHTML' => function ($attributes) {
|
|
||||||
if (empty($attributes->{'data-hbg'})) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$color = $attributes->{'data-hbg'};
|
|
||||||
return [
|
|
||||||
'data-hbg' => $color,
|
|
||||||
'style' => "background-color: {$color};",
|
|
||||||
];
|
|
||||||
},
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function renderHTML($mark, $HTMLAttributes = []): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'span',
|
|
||||||
HTML::mergeAttributes($HTMLAttributes),
|
|
||||||
0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Filament\RichEditor;
|
|
||||||
|
|
||||||
use Filament\Actions\Action;
|
|
||||||
use Filament\Forms\Components\RichEditor\Plugins\Contracts\RichContentPlugin;
|
|
||||||
use Filament\Forms\Components\RichEditor\RichEditorTool;
|
|
||||||
|
|
||||||
class HighlightColorPlugin implements RichContentPlugin
|
|
||||||
{
|
|
||||||
public static function make(): static
|
|
||||||
{
|
|
||||||
return app(static::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTipTapPhpExtensions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
app(HighlightColorMark::class),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTipTapJsExtensions(): array
|
|
||||||
{
|
|
||||||
return [asset('js/highlight-colors.js')];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEditorTools(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
RichEditorTool::make('highlightYellow')
|
|
||||||
->label('برجسته زرد')
|
|
||||||
->icon('fi-o-highlight')
|
|
||||||
->activeJsExpression("editorUpdatedAt && \$getEditor()?.isActive('highlightColor', {'data-hbg': '#FFFF00'})")
|
|
||||||
->jsHandler("(function(){ var e=\$getEditor(); if(!e) return; e.isActive('highlightColor',{'data-hbg':'#FFFF00'}) ? e.chain().focus().unsetMark('highlightColor').run() : e.chain().focus().unsetMark('highlightColor').setMark('highlightColor',{'data-hbg':'#FFFF00'}).run(); })()"),
|
|
||||||
|
|
||||||
RichEditorTool::make('highlightRed')
|
|
||||||
->label('برجسته قرمز')
|
|
||||||
->icon('fi-o-highlight')
|
|
||||||
->extraAttributes(['style' => 'color: #ef4444'])
|
|
||||||
->activeJsExpression("editorUpdatedAt && \$getEditor()?.isActive('highlightColor', {'data-hbg': '#ef4444'})")
|
|
||||||
->jsHandler("(function(){ var e=\$getEditor(); if(!e) return; e.isActive('highlightColor',{'data-hbg':'#ef4444'}) ? e.chain().focus().unsetMark('highlightColor').run() : e.chain().focus().unsetMark('highlightColor').setMark('highlightColor',{'data-hbg':'#ef4444'}).run(); })()"),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEditorActions(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,13 +48,11 @@ protected function buildQuery()
|
||||||
$query = Visit::query();
|
$query = Visit::query();
|
||||||
|
|
||||||
if ($this->fromDate) {
|
if ($this->fromDate) {
|
||||||
$fromDate = \Carbon\Carbon::parse($this->fromDate)->format('Y-m-d');
|
$query->whereDate('visit_date', '>=', $this->fromDate);
|
||||||
$query->whereRaw("SUBSTRING(visit_date, 1, 10) >= ?", [$fromDate]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->toDate) {
|
if ($this->toDate) {
|
||||||
$toDate = \Carbon\Carbon::parse($this->toDate)->format('Y-m-d');
|
$query->whereDate('visit_date', '<=', $this->toDate);
|
||||||
$query->whereRaw("SUBSTRING(visit_date, 1, 10) <= ?", [$toDate]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->doctor) {
|
if ($this->doctor) {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class ExpandableText extends Component
|
|
||||||
{
|
|
||||||
public string $html = '';
|
|
||||||
public int $limit = 200;
|
|
||||||
public bool $expanded = false;
|
|
||||||
|
|
||||||
public function toggle(): void
|
|
||||||
{
|
|
||||||
$this->expanded = ! $this->expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): \Illuminate\View\View
|
|
||||||
{
|
|
||||||
return view('livewire.expandable-text');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
|
||||||
|
|
||||||
use App\Filament\Resources\PatientResource\Pages\EditPatient;
|
|
||||||
use App\Models\Patient;
|
|
||||||
use Filament\Forms\Components\FileUpload;
|
|
||||||
use Filament\Forms\Concerns\InteractsWithForms;
|
|
||||||
use Filament\Forms\Contracts\HasForms;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
class PatientMediaPlayer extends Component implements HasForms
|
|
||||||
{
|
|
||||||
use InteractsWithForms;
|
|
||||||
|
|
||||||
public int $patientId;
|
|
||||||
public string $field;
|
|
||||||
public ?array $data = [];
|
|
||||||
|
|
||||||
public function mount(int $patientId, string $field): void
|
|
||||||
{
|
|
||||||
$this->patientId = $patientId;
|
|
||||||
$this->field = $field;
|
|
||||||
|
|
||||||
$patient = Patient::find($patientId);
|
|
||||||
|
|
||||||
$this->form->fill([
|
|
||||||
$field => EditPatient::normalizeFileList($patient?->{$field}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function form(Schema $form): Schema
|
|
||||||
{
|
|
||||||
$component = match ($this->field) {
|
|
||||||
'videos' => FileUpload::make('videos')
|
|
||||||
->hiddenLabel()
|
|
||||||
->multiple()
|
|
||||||
->disk('public')
|
|
||||||
->acceptedFileTypes([
|
|
||||||
'video/mp4', 'video/quicktime', 'video/x-msvideo',
|
|
||||||
'video/x-matroska', 'video/webm',
|
|
||||||
'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav',
|
|
||||||
'audio/ogg', 'audio/opus', 'audio/mp4', 'audio/x-m4a',
|
|
||||||
'audio/aac', 'audio/x-aac', 'audio/flac', 'audio/x-flac',
|
|
||||||
'audio/webm', 'audio/amr', 'audio/3gpp', 'audio/3gpp2',
|
|
||||||
'audio/aiff', 'audio/x-aiff',
|
|
||||||
])
|
|
||||||
->maxSize(102400)
|
|
||||||
->downloadable()
|
|
||||||
->openable()
|
|
||||||
->panelLayout('grid')
|
|
||||||
->extraAttributes(['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true'])
|
|
||||||
->disabled()
|
|
||||||
->deletable(false)
|
|
||||||
->fetchFileInformation(false),
|
|
||||||
|
|
||||||
'audio_files' => FileUpload::make('audio_files')
|
|
||||||
->hiddenLabel()
|
|
||||||
->multiple()
|
|
||||||
->disk('public')
|
|
||||||
->acceptedFileTypes([
|
|
||||||
'audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav',
|
|
||||||
'audio/ogg', 'audio/opus', 'audio/mp4', 'audio/x-m4a',
|
|
||||||
'audio/aac', 'audio/x-aac', 'audio/flac', 'audio/x-flac',
|
|
||||||
'audio/webm', 'audio/amr', 'audio/3gpp', 'audio/3gpp2',
|
|
||||||
'audio/aiff', 'audio/x-aiff',
|
|
||||||
])
|
|
||||||
->downloadable()
|
|
||||||
->openable()
|
|
||||||
->extraAttributes(['data-hide-dropzone' => 'true'])
|
|
||||||
->disabled()
|
|
||||||
->deletable(false)
|
|
||||||
->fetchFileInformation(false),
|
|
||||||
|
|
||||||
default => throw new \InvalidArgumentException("Unknown field: {$this->field}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
return $form
|
|
||||||
->schema([$component])
|
|
||||||
->statePath('data');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function render(): \Illuminate\View\View
|
|
||||||
{
|
|
||||||
return view('livewire.patient-media-player');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,18 +14,37 @@ class SettingsDropdown extends Component
|
||||||
public bool $syncFailed = false;
|
public bool $syncFailed = false;
|
||||||
public array $reportLines = [];
|
public array $reportLines = [];
|
||||||
|
|
||||||
public bool $peerConnected = false;
|
public function runBackup(): void
|
||||||
public ?string $peerPath = null;
|
|
||||||
public bool $peerChecked = false;
|
|
||||||
|
|
||||||
public function checkPeerStatus(): void
|
|
||||||
{
|
{
|
||||||
$result = SyncService::fromConfig()->ping();
|
$dbPath = database_path('database.sqlite');
|
||||||
|
|
||||||
$this->peerConnected = $result['connected'];
|
if (! file_exists($dbPath)) {
|
||||||
$this->peerPath = $result['path'];
|
Notification::make()
|
||||||
$this->peerChecked = true;
|
->title(__('navigation.backup.failed'))
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$backupDir = storage_path('app/backups');
|
||||||
|
|
||||||
|
if (! is_dir($backupDir)) {
|
||||||
|
mkdir($backupDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($backupDir . '/matab-backup-*.sqlite') as $oldFile) {
|
||||||
|
unlink($oldFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = 'matab-backup-' . now()->format('Y-m-d_H-i-s') . '.sqlite';
|
||||||
|
copy($dbPath, $backupDir . '/' . $filename);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title(__('navigation.backup.success'))
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
public function runSync(): void
|
public function runSync(): void
|
||||||
{
|
{
|
||||||
$service = SyncService::fromConfig();
|
$service = SyncService::fromConfig();
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,8 @@ public function updated(Model $model): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$changedData = [];
|
$changedData = [];
|
||||||
foreach ($dirty as $field => $rawValue) {
|
foreach ($dirty as $field => $newValue) {
|
||||||
$changedData[$field] = $rawValue instanceof \BackedEnum
|
$changedData[$field] = $model->getAttribute($field);
|
||||||
? $rawValue->value
|
|
||||||
: (is_object($rawValue) ? (string) $rawValue : $rawValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncLog::create([
|
SyncLog::create([
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Observers;
|
|
||||||
|
|
||||||
use App\Models\Visit;
|
|
||||||
|
|
||||||
class VisitObserver
|
|
||||||
{
|
|
||||||
public function created(Visit $visit): void
|
|
||||||
{
|
|
||||||
$this->touchPatient($visit);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updated(Visit $visit): void
|
|
||||||
{
|
|
||||||
$this->touchPatient($visit);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function touchPatient(Visit $visit): void
|
|
||||||
{
|
|
||||||
$patient = $visit->patientRecord;
|
|
||||||
if (! $patient) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$patient->updated_at = now();
|
|
||||||
$patient->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,19 +12,15 @@
|
||||||
use App\Models\SurgeryCenter;
|
use App\Models\SurgeryCenter;
|
||||||
use App\Models\Treatment;
|
use App\Models\Treatment;
|
||||||
use App\Models\LabTest;
|
use App\Models\LabTest;
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Visit;
|
use App\Models\Visit;
|
||||||
use Spatie\Permission\Models\Role;
|
|
||||||
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\Observers\VisitObserver;
|
|
||||||
use App\Filament\Widgets\VisitStatsWidget;
|
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 Illuminate\Support\Facades\URL;
|
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
|
@ -33,18 +29,7 @@ public function register(): void {}
|
||||||
|
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
if (app()->runningInConsole() === false && isset($_SERVER['HTTP_HOST'])) {
|
|
||||||
$scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http';
|
|
||||||
$root = "{$scheme}://{$_SERVER['HTTP_HOST']}";
|
|
||||||
URL::forceRootUrl($root);
|
|
||||||
config([
|
|
||||||
'app.url' => $root,
|
|
||||||
'filesystems.disks.public.url' => $root . '/storage',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
Livewire::component('app.filament.widgets.visit-stats-widget', VisitStatsWidget::class);
|
Livewire::component('app.filament.widgets.visit-stats-widget', VisitStatsWidget::class);
|
||||||
Livewire::component('patient-media-player', \App\Livewire\PatientMediaPlayer::class);
|
|
||||||
Livewire::component('expandable-text', \App\Livewire\ExpandableText::class);
|
|
||||||
$observer = new SyncObserver();
|
$observer = new SyncObserver();
|
||||||
|
|
||||||
Patient::observe($observer);
|
Patient::observe($observer);
|
||||||
|
|
@ -60,9 +45,6 @@ public function boot(): void
|
||||||
Prescription::observe($observer);
|
Prescription::observe($observer);
|
||||||
SurgeryAppointment::observe($observer);
|
SurgeryAppointment::observe($observer);
|
||||||
Visit::observe($observer);
|
Visit::observe($observer);
|
||||||
Visit::observe(VisitObserver::class);
|
|
||||||
User::observe($observer);
|
|
||||||
Role::observe($observer);
|
|
||||||
|
|
||||||
DeleteAction::configureUsing(function (DeleteAction $action): void {
|
DeleteAction::configureUsing(function (DeleteAction $action): void {
|
||||||
$action->modalAlignment(Alignment::Start);
|
$action->modalAlignment(Alignment::Start);
|
||||||
|
|
|
||||||
|
|
@ -82,166 +82,6 @@ public function panel(Panel $panel): Panel
|
||||||
->renderHook(
|
->renderHook(
|
||||||
PanelsRenderHook::BODY_END,
|
PanelsRenderHook::BODY_END,
|
||||||
fn (): string => <<<'HTML'
|
fn (): string => <<<'HTML'
|
||||||
<style>
|
|
||||||
#fi-ucg-backdrop{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,.55);backdrop-filter:blur(3px)}
|
|
||||||
#fi-ucg-wrap{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:1rem}
|
|
||||||
#fi-ucg-card{background:#fff;border-radius:.75rem;box-shadow:0 20px 50px -10px rgba(0,0,0,.18);padding:1.5rem;max-width:26rem;width:100%;border:1px solid #fecaca}
|
|
||||||
:root.dark #fi-ucg-card{background:#1e2533;border-color:#7f1d1d}
|
|
||||||
#fi-ucg-icon{width:2.75rem;height:2.75rem;border-radius:50%;background:#fee2e2;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
||||||
:root.dark #fi-ucg-icon{background:rgba(127,29,15,.35)}
|
|
||||||
#fi-ucg-title{font-weight:700;font-size:.9375rem;color:#991b1b;margin:0 0 .35rem}
|
|
||||||
:root.dark #fi-ucg-title{color:#fca5a5}
|
|
||||||
#fi-ucg-desc{font-size:.875rem;color:#6b7280;line-height:1.6;margin:0}
|
|
||||||
:root.dark #fi-ucg-desc{color:#9ca3af}
|
|
||||||
#fi-ucg-footer{display:flex;gap:.625rem;justify-content:flex-start;margin-top:1.5rem;flex-wrap:wrap}
|
|
||||||
.fi-ucg-btn{display:inline-flex;align-items:center;justify-content:center;padding:.5rem 1rem;border-radius:.5rem;font-size:.875rem;font-weight:500;cursor:pointer;border:none;transition:background .15s,opacity .15s;outline:none}
|
|
||||||
.fi-ucg-btn-cancel{background:#f3f4f6;color:#374151;border:1px solid #d1d5db}
|
|
||||||
.fi-ucg-btn-cancel:hover{background:#e5e7eb}
|
|
||||||
:root.dark .fi-ucg-btn-cancel{background:#374151;color:#d1d5db;border-color:#4b5563}
|
|
||||||
:root.dark .fi-ucg-btn-cancel:hover{background:#4b5563}
|
|
||||||
.fi-ucg-btn-discard{background:#dc2626;color:#fff}
|
|
||||||
.fi-ucg-btn-discard:hover{background:#b91c1c}
|
|
||||||
:root.dark .fi-ucg-btn-discard{background:#dc2626}
|
|
||||||
:root.dark .fi-ucg-btn-discard:hover{background:#b91c1c}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/* Unsaved-changes guard — runs synchronously before Livewire @script */
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
/* ── i18n ── */
|
|
||||||
function texts() {
|
|
||||||
var fa = document.documentElement.lang === 'fa';
|
|
||||||
return fa ? {
|
|
||||||
dir:'rtl', title:'تغییرات ذخیره نشده',
|
|
||||||
desc:'اطلاعات وارد شده هنوز ذخیره نشدهاند.<br>در صورت خروج، تمام تغییرات از دست خواهد رفت.',
|
|
||||||
cancel:'بازگشت به ویرایش', discard:'خروج بدون ذخیره',
|
|
||||||
} : {
|
|
||||||
dir:'ltr', title:'Unsaved Changes',
|
|
||||||
desc:'You have unsaved changes.<br>Leaving now will permanently discard them.',
|
|
||||||
cancel:'Keep Editing', discard:'Leave Without Saving',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── dirty-state helper ── */
|
|
||||||
/* savedDataHash is #[Locked] in Livewire — never try to set it from JS.
|
|
||||||
Instead use a bypass flag so the next navigate event is skipped. */
|
|
||||||
var bypassNextNavigate = false;
|
|
||||||
|
|
||||||
function isDirty(wire) {
|
|
||||||
try {
|
|
||||||
if (!wire || wire?.__instance?.effects?.redirect) return false;
|
|
||||||
return window.jsMd5(JSON.stringify(wire.data).replace(/\\/g, '')) !== wire.savedDataHash;
|
|
||||||
} catch (_) { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── registered wires (for beforeunload) ── */
|
|
||||||
var registeredWires = [];
|
|
||||||
function registerWire(w) { if (!registeredWires.includes(w)) registeredWires.push(w); }
|
|
||||||
document.addEventListener('livewire:navigated', function () {
|
|
||||||
bypassNextNavigate = false;
|
|
||||||
registeredWires = registeredWires.filter(function (w) {
|
|
||||||
try { return typeof w.savedDataHash !== 'undefined'; } catch (_) { return false; }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Alpine component (registered before Alpine starts) ── */
|
|
||||||
document.addEventListener('alpine:init', function () {
|
|
||||||
Alpine.data('fiUnsavedGuard', function () {
|
|
||||||
return {
|
|
||||||
show:false, pendingUrl:null, pendingWire:null,
|
|
||||||
pendingIsSpa:false, navigating:false, t:texts(),
|
|
||||||
init() {
|
|
||||||
window.$fiUnsavedGuard = this;
|
|
||||||
document.addEventListener('livewire:navigated', () => {
|
|
||||||
this.navigating = false;
|
|
||||||
this.t = texts();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
open(url, wire, isSpa) {
|
|
||||||
this.pendingUrl=url; this.pendingWire=wire;
|
|
||||||
this.pendingIsSpa=!!isSpa; this.show=true;
|
|
||||||
},
|
|
||||||
cancel() { this.show=false; this.pendingUrl=null; this.pendingWire=null; },
|
|
||||||
discard() {
|
|
||||||
var url=this.pendingUrl;
|
|
||||||
this.show=false; this.pendingUrl=null; this.pendingWire=null;
|
|
||||||
this.navigating=true;
|
|
||||||
if (!url) { history.back(); return; }
|
|
||||||
window.location.href = url;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── override Filament's hooks (must happen before @script runs) ── */
|
|
||||||
window.setUpSpaModeUnsavedDataChangesAlert = function ({ resolveLivewireComponentUsing, $wire }) {
|
|
||||||
registerWire($wire);
|
|
||||||
document.addEventListener('livewire:navigate', function (event) {
|
|
||||||
if (bypassNextNavigate) { return; } /* reset is handled by livewire:navigated so all listeners see the flag */
|
|
||||||
if (typeof resolveLivewireComponentUsing() === 'undefined') return;
|
|
||||||
if (!isDirty($wire)) return;
|
|
||||||
event.preventDefault();
|
|
||||||
var url = event.detail?.url;
|
|
||||||
window.$fiUnsavedGuard?.open(url ? url.toString() : null, $wire, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.setUpUnsavedDataChangesAlert = function ({ $wire }) {
|
|
||||||
registerWire($wire);
|
|
||||||
document.addEventListener('click', function (e) {
|
|
||||||
var link = e.target.closest('a[href]:not([target="_blank"]):not([href^="javascript"]):not([href^="#"])');
|
|
||||||
if (!link) return;
|
|
||||||
var href = link.getAttribute('href');
|
|
||||||
if (!href || href === window.location.pathname) return;
|
|
||||||
if (!isDirty($wire)) return;
|
|
||||||
e.preventDefault(); e.stopPropagation();
|
|
||||||
window.$fiUnsavedGuard?.open(link.href, $wire, false);
|
|
||||||
}, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ── beforeunload: only for real tab/browser close ── */
|
|
||||||
window.addEventListener('beforeunload', function (event) {
|
|
||||||
if (window.$fiUnsavedGuard?.navigating) return;
|
|
||||||
for (var i = 0; i < registeredWires.length; i++) {
|
|
||||||
if (isDirty(registeredWires[i])) { event.preventDefault(); event.returnValue = true; return; }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
x-data="fiUnsavedGuard"
|
|
||||||
x-cloak
|
|
||||||
@keydown.escape.window="if (show) cancel()"
|
|
||||||
>
|
|
||||||
<template x-if="show">
|
|
||||||
<div>
|
|
||||||
<div id="fi-ucg-backdrop" @click="cancel()"></div>
|
|
||||||
<div id="fi-ucg-wrap">
|
|
||||||
<div id="fi-ucg-card" :dir="t.dir" role="dialog" aria-modal="true">
|
|
||||||
<div style="display:flex;align-items:flex-start;gap:.875rem">
|
|
||||||
<div id="fi-ucg-icon">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.75" stroke="#dc2626" style="width:1.25rem;height:1.25rem">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p id="fi-ucg-title" x-text="t.title"></p>
|
|
||||||
<p id="fi-ucg-desc" x-html="t.desc"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="fi-ucg-footer">
|
|
||||||
<button @click="cancel()" type="button" class="fi-ucg-btn fi-ucg-btn-cancel" x-text="t.cancel"></button>
|
|
||||||
<button @click="discard()" type="button" class="fi-ucg-btn fi-ucg-btn-discard" x-text="t.discard"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var speeds = [0.5, 1, 1.5, 2];
|
var speeds = [0.5, 1, 1.5, 2];
|
||||||
|
|
@ -260,74 +100,15 @@ function registerWire(w) { if (!registeredWires.includes(w)) registeredWires.pus
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
/* Fix FilePond gallery: remove inline styles set by filepond JS that conflict with our CSS */
|
|
||||||
function fixGalleryItems() {
|
|
||||||
document.querySelectorAll('.fi-photo-gallery .filepond--item').forEach(function (item) {
|
|
||||||
item.style.removeProperty('width');
|
|
||||||
item.style.removeProperty('height');
|
|
||||||
item.style.removeProperty('transform');
|
|
||||||
item.style.removeProperty('left');
|
|
||||||
item.style.removeProperty('top');
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.fi-photo-gallery .filepond--image-preview-wrapper').forEach(function (w) {
|
|
||||||
w.style.removeProperty('transform');
|
|
||||||
w.style.removeProperty('margin-top');
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.fi-photo-gallery .filepond--list-scroller').forEach(function (el) {
|
|
||||||
el.style.removeProperty('height');
|
|
||||||
el.style.removeProperty('top');
|
|
||||||
el.style.removeProperty('transform');
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.fi-photo-gallery .filepond--root').forEach(function (root) {
|
|
||||||
root.style.removeProperty('height');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var _fixTimer = null;
|
|
||||||
var _observer = new MutationObserver(function (mutations) {
|
|
||||||
if (_fixTimer !== null) return;
|
|
||||||
var relevant = mutations.some(function (m) {
|
|
||||||
return m.target && m.target.closest &&
|
|
||||||
m.target.closest('.fi-photo-gallery');
|
|
||||||
});
|
|
||||||
if (!relevant) return;
|
|
||||||
_fixTimer = requestAnimationFrame(function () {
|
|
||||||
_fixTimer = null;
|
|
||||||
fixGalleryItems();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
_observer.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['style'] });
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', fixGalleryItems);
|
|
||||||
document.addEventListener('livewire:navigated', fixGalleryItems);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
HTML,
|
HTML,
|
||||||
)
|
)
|
||||||
->renderHook(
|
->renderHook(
|
||||||
PanelsRenderHook::BODY_END,
|
PanelsRenderHook::BODY_END,
|
||||||
fn (): string => <<<'HTML'
|
fn (): string => <<<'HTML'
|
||||||
<style>
|
|
||||||
#fi-lb-download{position:absolute;top:1rem;left:4.5rem;right:auto;z-index:10;width:2.5rem;height:2.5rem;border-radius:50%;background:rgba(255,255,255,0.15);border:none;color:white;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s,transform .15s}
|
|
||||||
#fi-lb-download:hover{background:rgba(255,255,255,0.35);transform:translateY(1px)}
|
|
||||||
#fi-lb-download:active{transform:translateY(2px)}
|
|
||||||
</style>
|
|
||||||
<div id="fi-lightbox">
|
<div id="fi-lightbox">
|
||||||
<div id="fi-lb-wrap">
|
<div id="fi-lb-wrap">
|
||||||
<img id="fi-lb-img" src="" alt="">
|
<img id="fi-lb-img" src="" alt="">
|
||||||
<div id="fi-lb-caption" style="display:none">
|
|
||||||
<div id="fi-lb-cap-header">
|
|
||||||
<div id="fi-lb-cap-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="rgba(20,30,60,0.85)" stroke-width="1.5" style="width:14px;height:14px;"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h6M7 11h4m1 7l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v5a2 2 0 01-2 2h-2l-3 3z"/></svg></div>
|
|
||||||
<span id="fi-lb-cap-title">یادداشت پزشک</span>
|
|
||||||
<button id="fi-lb-cap-close" title="پنهان کردن">✕</button>
|
|
||||||
</div>
|
|
||||||
<div id="fi-lb-cap-body"></div>
|
|
||||||
</div>
|
|
||||||
<button id="fi-lb-cap-toggle" title="نمایش یادداشت"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" style="width:20px;height:20px;"><path stroke-linecap="round" stroke-linejoin="round" d="M7 8h6M7 11h4m1 7l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v5a2 2 0 01-2 2h-2l-3 3z"/></svg></button>
|
|
||||||
<button id="fi-lb-close" title="بستن">✕</button>
|
<button id="fi-lb-close" title="بستن">✕</button>
|
||||||
<button id="fi-lb-download" title="دانلود همه تصاویر"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" style="width:18px;height:18px;"><path stroke-linecap="round" stroke-linejoin="round" d="M10 3v9m0 0l-3-3m3 3l3-3M4 16h12"/></svg></button>
|
|
||||||
<button id="fi-lb-prev" title="قبلی">‹</button>
|
<button id="fi-lb-prev" title="قبلی">‹</button>
|
||||||
<button id="fi-lb-next" title="بعدی">›</button>
|
<button id="fi-lb-next" title="بعدی">›</button>
|
||||||
<div id="fi-lb-counter">1 / 1</div>
|
<div id="fi-lb-counter">1 / 1</div>
|
||||||
|
|
@ -345,76 +126,22 @@ function fixGalleryItems() {
|
||||||
var lbPrev = document.getElementById('fi-lb-prev');
|
var lbPrev = document.getElementById('fi-lb-prev');
|
||||||
var lbNext = document.getElementById('fi-lb-next');
|
var lbNext = document.getElementById('fi-lb-next');
|
||||||
var lbClose = document.getElementById('fi-lb-close');
|
var lbClose = document.getElementById('fi-lb-close');
|
||||||
var lbDownload = document.getElementById('fi-lb-download');
|
|
||||||
var lbCaption = document.getElementById('fi-lb-caption');
|
|
||||||
var lbCapBody = document.getElementById('fi-lb-cap-body');
|
|
||||||
var lbCapClose = document.getElementById('fi-lb-cap-close');
|
|
||||||
var lbCapToggle = document.getElementById('fi-lb-cap-toggle');
|
|
||||||
var lbCapTitleEl = document.getElementById('fi-lb-cap-title');
|
|
||||||
if (!lb) return;
|
if (!lb) return;
|
||||||
|
|
||||||
var images = [], captions = [], sections = [], currentIdx = 0, zoom = 1;
|
var images = [], currentIdx = 0, zoom = 1;
|
||||||
var isPanning = false, panStartX = 0, panStartY = 0, panX = 0, panY = 0;
|
var isPanning = false, panStartX = 0, panStartY = 0, panX = 0, panY = 0;
|
||||||
var manuallyHidden = false, autoHidden = false;
|
|
||||||
|
|
||||||
var capTitles = {
|
|
||||||
before: { fa: 'توضیحات قبل از عمل', en: 'Pre-Op Notes' },
|
|
||||||
after: { fa: 'توضیحات بعد از عمل', en: 'Post-Op Notes' },
|
|
||||||
default: { fa: 'یادداشت پزشک', en: 'Doctor\'s Notes' },
|
|
||||||
};
|
|
||||||
function updateTitle() {
|
|
||||||
if (!lbCapTitleEl) return;
|
|
||||||
var sec = sections[currentIdx] || 'default';
|
|
||||||
var locale = document.documentElement.lang === 'fa' ? 'fa' : 'en';
|
|
||||||
lbCapTitleEl.textContent = (capTitles[sec] || capTitles['default'])[locale];
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTransform() {
|
function applyTransform() {
|
||||||
lbImg.style.transform = 'scale(' + zoom + ') translate(' + panX + 'px, ' + panY + 'px)';
|
lbImg.style.transform = 'scale(' + zoom + ') translate(' + panX + 'px, ' + panY + 'px)';
|
||||||
lbImg.classList.toggle('lb-zoomed', zoom > 1);
|
lbImg.classList.toggle('lb-zoomed', zoom > 1);
|
||||||
var hasCaption = lbCaption && lbCaption.style.display !== 'none';
|
|
||||||
if (hasCaption && zoom > 1 && !lbCaption.classList.contains('lb-cap-hidden')) {
|
|
||||||
lbCaption.classList.add('lb-cap-hidden');
|
|
||||||
lb.classList.add('lb-cap-collapsed');
|
|
||||||
if (lbCapToggle) lbCapToggle.classList.add('lb-cap-toggle-visible');
|
|
||||||
autoHidden = true;
|
|
||||||
} else if (zoom <= 1 && autoHidden && !manuallyHidden) {
|
|
||||||
lbCaption.classList.remove('lb-cap-hidden');
|
|
||||||
lb.classList.remove('lb-cap-collapsed');
|
|
||||||
if (lbCapToggle) lbCapToggle.classList.remove('lb-cap-toggle-visible');
|
|
||||||
autoHidden = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCaption() {
|
function showLightbox(imgs, idx) {
|
||||||
var cap = captions[currentIdx] || '';
|
|
||||||
if (cap) {
|
|
||||||
if (lbCapBody) lbCapBody.innerHTML = cap;
|
|
||||||
lbCaption.style.display = '';
|
|
||||||
lb.classList.remove('lb-no-caption');
|
|
||||||
/* Start open by default */
|
|
||||||
lbCaption.classList.remove('lb-cap-hidden');
|
|
||||||
lb.classList.remove('lb-cap-collapsed');
|
|
||||||
if (lbCapToggle) lbCapToggle.classList.remove('lb-cap-toggle-visible');
|
|
||||||
} else {
|
|
||||||
lbCaption.style.display = 'none';
|
|
||||||
if (lbCapBody) lbCapBody.innerHTML = '';
|
|
||||||
lb.classList.add('lb-no-caption');
|
|
||||||
if (lbCapToggle) lbCapToggle.classList.remove('lb-cap-toggle-visible');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLightbox(imgs, idx, caps, secs) {
|
|
||||||
images = imgs;
|
images = imgs;
|
||||||
captions = caps || [];
|
|
||||||
sections = secs || [];
|
|
||||||
currentIdx = idx;
|
currentIdx = idx;
|
||||||
zoom = 1; panX = 0; panY = 0;
|
zoom = 1; panX = 0; panY = 0;
|
||||||
manuallyHidden = false; autoHidden = false;
|
|
||||||
applyTransform();
|
applyTransform();
|
||||||
updateCounter();
|
updateCounter();
|
||||||
updateCaption();
|
|
||||||
updateTitle();
|
|
||||||
loadImage();
|
loadImage();
|
||||||
lb.classList.add('lb-open');
|
lb.classList.add('lb-open');
|
||||||
document.addEventListener('keydown', onKey);
|
document.addEventListener('keydown', onKey);
|
||||||
|
|
@ -433,8 +160,6 @@ function navigate(dir) {
|
||||||
applyTransform();
|
applyTransform();
|
||||||
loadImage();
|
loadImage();
|
||||||
updateCounter();
|
updateCounter();
|
||||||
updateCaption();
|
|
||||||
updateTitle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadImage() {
|
function loadImage() {
|
||||||
|
|
@ -493,44 +218,8 @@ function onKey(e) {
|
||||||
|
|
||||||
lbWrap.addEventListener('click', function (e) { if (e.target === lbWrap) hideLightbox(); });
|
lbWrap.addEventListener('click', function (e) { if (e.target === lbWrap) hideLightbox(); });
|
||||||
lbClose.addEventListener('click', hideLightbox);
|
lbClose.addEventListener('click', hideLightbox);
|
||||||
if (lbDownload) lbDownload.addEventListener('click', function () {
|
|
||||||
var toDownload = images.slice();
|
|
||||||
toDownload.forEach(function (url, i) {
|
|
||||||
setTimeout(function () {
|
|
||||||
fetch(url)
|
|
||||||
.then(function (r) {
|
|
||||||
if (!r.ok) throw new Error('fetch failed');
|
|
||||||
return r.blob();
|
|
||||||
})
|
|
||||||
.then(function (blob) {
|
|
||||||
var objUrl = URL.createObjectURL(blob);
|
|
||||||
var a = document.createElement('a');
|
|
||||||
var ext = (url.split('?')[0].split('.').pop() || 'jpg').toLowerCase();
|
|
||||||
a.href = objUrl;
|
|
||||||
a.download = 'image-' + (i + 1) + '.' + ext;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
setTimeout(function () { URL.revokeObjectURL(objUrl); }, 10000);
|
|
||||||
})
|
|
||||||
.catch(function () { window.open(url, '_blank'); });
|
|
||||||
}, i * 700);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
lbPrev.addEventListener('click', function () { navigate(-1); });
|
lbPrev.addEventListener('click', function () { navigate(-1); });
|
||||||
lbNext.addEventListener('click', function () { navigate(1); });
|
lbNext.addEventListener('click', function () { navigate(1); });
|
||||||
if (lbCapClose) lbCapClose.addEventListener('click', function () {
|
|
||||||
manuallyHidden = true;
|
|
||||||
lbCaption.classList.add('lb-cap-hidden');
|
|
||||||
lb.classList.add('lb-cap-collapsed');
|
|
||||||
if (lbCapToggle) lbCapToggle.classList.add('lb-cap-toggle-visible');
|
|
||||||
});
|
|
||||||
if (lbCapToggle) lbCapToggle.addEventListener('click', function () {
|
|
||||||
manuallyHidden = false; autoHidden = false;
|
|
||||||
lbCaption.classList.remove('lb-cap-hidden');
|
|
||||||
lb.classList.remove('lb-cap-collapsed');
|
|
||||||
lbCapToggle.classList.remove('lb-cap-toggle-visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var thumb = e.target.closest('[data-lb-src]');
|
var thumb = e.target.closest('[data-lb-src]');
|
||||||
|
|
@ -541,10 +230,8 @@ function onKey(e) {
|
||||||
? Array.from(document.querySelectorAll('[data-lb-group="' + group + '"]'))
|
? Array.from(document.querySelectorAll('[data-lb-group="' + group + '"]'))
|
||||||
: [thumb];
|
: [thumb];
|
||||||
var urls = all.map(function (el) { return el.getAttribute('data-lb-src'); });
|
var urls = all.map(function (el) { return el.getAttribute('data-lb-src'); });
|
||||||
var caps = all.map(function (el) { return el.getAttribute('data-lb-caption') || ''; });
|
|
||||||
var secs = all.map(function (el) { return el.getAttribute('data-lb-section') || ''; });
|
|
||||||
var idx = all.indexOf(thumb);
|
var idx = all.indexOf(thumb);
|
||||||
showLightbox(urls, Math.max(0, idx), caps, secs);
|
showLightbox(urls, Math.max(0, idx));
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
|
|
@ -561,13 +248,6 @@ function onKey(e) {
|
||||||
var allItems = Array.from(root.querySelectorAll('.filepond--item'));
|
var allItems = Array.from(root.querySelectorAll('.filepond--item'));
|
||||||
var clickedIdx = allItems.indexOf(item);
|
var clickedIdx = allItems.indexOf(item);
|
||||||
|
|
||||||
var capWrapper = root.closest('[data-lb-section]');
|
|
||||||
var lbSec = capWrapper ? capWrapper.getAttribute('data-lb-section') : '';
|
|
||||||
var lbCap = capWrapper ? capWrapper.getAttribute('data-lb-caption') : '';
|
|
||||||
function mkArrays(urls) {
|
|
||||||
return { caps: urls.map(function() { return lbCap; }), secs: urls.map(function() { return lbSec; }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
var pond = null;
|
var pond = null;
|
||||||
var alpineEl = root.closest('[x-data]');
|
var alpineEl = root.closest('[x-data]');
|
||||||
if (alpineEl && window.Alpine) {
|
if (alpineEl && window.Alpine) {
|
||||||
|
|
@ -589,7 +269,7 @@ function mkArrays(urls) {
|
||||||
urls1.push(sid.startsWith('http') || sid.startsWith('/') ? sid : '/storage/' + sid);
|
urls1.push(sid.startsWith('http') || sid.startsWith('/') ? sid : '/storage/' + sid);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (urls1.length) { var a1=mkArrays(urls1); showLightbox(urls1, Math.min(clickedIdx, urls1.length - 1), a1.caps, a1.secs); return; }
|
if (urls1.length) { showLightbox(urls1, Math.min(clickedIdx, urls1.length - 1)); return; }
|
||||||
} catch (e2) {}
|
} catch (e2) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -604,8 +284,7 @@ function mkArrays(urls) {
|
||||||
if (files) {
|
if (files) {
|
||||||
var arr = Array.isArray(files) ? files : Object.values(files);
|
var arr = Array.isArray(files) ? files : Object.values(files);
|
||||||
if (arr.length) {
|
if (arr.length) {
|
||||||
var arr2=arr.map(function(f){return '/storage/'+f;}); var a2=mkArrays(arr2);
|
showLightbox(arr.map(function (f) { return '/storage/' + f; }), Math.min(clickedIdx, arr.length - 1));
|
||||||
showLightbox(arr2, Math.min(clickedIdx, arr2.length-1), a2.caps, a2.secs);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -623,7 +302,7 @@ function mkArrays(urls) {
|
||||||
} catch (ex) {}
|
} catch (ex) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (imgs.length) { var a3=mkArrays(imgs); showLightbox(imgs, fbIdx, a3.caps, a3.secs); }
|
if (imgs.length) showLightbox(imgs, fbIdx);
|
||||||
}, true);
|
}, true);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -633,11 +312,7 @@ function mkArrays(urls) {
|
||||||
Action::make('switch_language')
|
Action::make('switch_language')
|
||||||
->label(fn () => app()->getLocale() === 'fa' ? 'English' : 'فارسی')
|
->label(fn () => app()->getLocale() === 'fa' ? 'English' : 'فارسی')
|
||||||
->icon('heroicon-o-language')
|
->icon('heroicon-o-language')
|
||||||
->url(fn () => route('locale.switch', app()->getLocale() === 'fa' ? 'en' : 'fa'))
|
->url(fn () => route('locale.switch', app()->getLocale() === 'fa' ? 'en' : 'fa')),
|
||||||
->openUrlInNewTab(false)
|
|
||||||
->extraAttributes(fn () => [
|
|
||||||
'x-on:click.prevent' => 'window.location.href = \'' . route('locale.switch', app()->getLocale() === 'fa' ? 'en' : 'fa') . '\'',
|
|
||||||
]),
|
|
||||||
])
|
])
|
||||||
->navigationGroups([
|
->navigationGroups([
|
||||||
NavigationGroup::make('بیماران و ویزیت'),
|
NavigationGroup::make('بیماران و ویزیت'),
|
||||||
|
|
@ -646,8 +321,6 @@ function mkArrays(urls) {
|
||||||
NavigationGroup::make('تنظیمات'),
|
NavigationGroup::make('تنظیمات'),
|
||||||
])
|
])
|
||||||
->sidebarFullyCollapsibleOnDesktop()
|
->sidebarFullyCollapsibleOnDesktop()
|
||||||
->spa()
|
|
||||||
->unsavedChangesAlerts()
|
|
||||||
->maxContentWidth(Width::Full)
|
->maxContentWidth(Width::Full)
|
||||||
->viteTheme('resources/css/filament/admin/theme.css')
|
->viteTheme('resources/css/filament/admin/theme.css')
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
|
||||||
|
|
|
||||||
|
|
@ -36,38 +36,6 @@ public static function fromConfig(): static
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ping(): array
|
|
||||||
{
|
|
||||||
if (! $this->isConfigured()) {
|
|
||||||
return ['connected' => false, 'path' => null];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ip = escapeshellarg($this->peerIp);
|
|
||||||
$cmd = PHP_OS_FAMILY === 'Windows'
|
|
||||||
? "ping -n 1 -w 1000 {$ip}"
|
|
||||||
: "ping -c 1 -W 1 {$ip}";
|
|
||||||
|
|
||||||
exec($cmd . ' 2>&1', $output, $exitCode);
|
|
||||||
|
|
||||||
if ($exitCode !== 0) {
|
|
||||||
return ['connected' => false, 'path' => null];
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = null;
|
|
||||||
try {
|
|
||||||
$response = Http::timeout(3)
|
|
||||||
->withHeader('X-Sync-Token', $this->token)
|
|
||||||
->get("http://{$this->peerIp}:{$this->peerPort}/api/sync/ping");
|
|
||||||
|
|
||||||
if ($response->successful() && $response->json('status') === 'ok') {
|
|
||||||
$path = $response->json('path');
|
|
||||||
}
|
|
||||||
} catch (\Throwable) {
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['connected' => true, 'path' => $path];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isConfigured(): bool
|
public function isConfigured(): bool
|
||||||
{
|
{
|
||||||
return $this->peerIp !== '' && $this->token !== '';
|
return $this->peerIp !== '' && $this->token !== '';
|
||||||
|
|
@ -80,20 +48,9 @@ public function isFtpConfigured(): bool
|
||||||
|
|
||||||
public function sync(): array
|
public function sync(): array
|
||||||
{
|
{
|
||||||
set_time_limit(0);
|
|
||||||
$report = [];
|
$report = [];
|
||||||
$baseUrl = "http://{$this->peerIp}:{$this->peerPort}";
|
$baseUrl = "http://{$this->peerIp}:{$this->peerPort}";
|
||||||
|
|
||||||
$pruned = $this->pruneOrphanedPending();
|
|
||||||
if ($pruned > 0) {
|
|
||||||
$report[] = "{$pruned} مسیر فایل یتیم از صف retry حذف شد.";
|
|
||||||
}
|
|
||||||
|
|
||||||
$healed = $this->healMissingFiles();
|
|
||||||
if ($healed > 0) {
|
|
||||||
$report[] = "{$healed} فایل گمشده شناسایی شد و در صف دریافت قرار گرفت.";
|
|
||||||
}
|
|
||||||
|
|
||||||
$peerCursor = \App\Models\OsurgInitial::val('sync_peer_cursor') ?: null;
|
$peerCursor = \App\Models\OsurgInitial::val('sync_peer_cursor') ?: null;
|
||||||
$ourLastSync = \App\Models\OsurgInitial::val('our_sync_cursor') ?: null;
|
$ourLastSync = \App\Models\OsurgInitial::val('our_sync_cursor') ?: null;
|
||||||
|
|
||||||
|
|
@ -129,15 +86,15 @@ public function sync(): array
|
||||||
} else {
|
} else {
|
||||||
$report[] = 'هشدار: peer_cursor پیش نرفت — برخی تغییرات دریافتی اعمال نشد و دفعه بعد retry میشه.';
|
$report[] = 'هشدار: peer_cursor پیش نرفت — برخی تغییرات دریافتی اعمال نشد و دفعه بعد retry میشه.';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isFtpConfigured()) {
|
if ($this->isFtpConfigured()) {
|
||||||
$this->transferFilesViaFtp($peerChanges ?? [], 's2c', $report);
|
$this->transferFilesViaFtp($peerChanges, 's2c', $report);
|
||||||
} else {
|
} else {
|
||||||
$this->syncFilesViaHttp($peerChanges ?? [], $baseUrl, $report);
|
$this->syncFilesViaHttp($peerChanges, $baseUrl, $report);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$report[] = count($peerChanges ?? []) . ' تغییر از سیستم مقابل دریافت شد.';
|
$report[] = count($peerChanges) . ' تغییر از سیستم مقابل دریافت شد.';
|
||||||
} else {
|
} else {
|
||||||
$report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status();
|
$report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status();
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +123,6 @@ public function sync(): array
|
||||||
$ourDatetimes = array_filter(array_column($ourChanges, 'datetime'));
|
$ourDatetimes = array_filter(array_column($ourChanges, 'datetime'));
|
||||||
$maxOurDatetime = \Carbon\Carbon::parse(max($ourDatetimes))->format('Y-m-d H:i:s');
|
$maxOurDatetime = \Carbon\Carbon::parse(max($ourDatetimes))->format('Y-m-d H:i:s');
|
||||||
if ($ourLastSync === null || $maxOurDatetime > $ourLastSync) {
|
if ($ourLastSync === null || $maxOurDatetime > $ourLastSync) {
|
||||||
$this->preQueueOutgoingFiles($ourChanges);
|
|
||||||
$this->saveOurCursor($maxOurDatetime);
|
$this->saveOurCursor($maxOurDatetime);
|
||||||
}
|
}
|
||||||
} elseif ($hasErrors) {
|
} elseif ($hasErrors) {
|
||||||
|
|
@ -206,16 +162,11 @@ public function applyChanges(array $changes): array
|
||||||
return $report;
|
return $report;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function transferFilesViaFtp(array $changes, string $side, array &$report): void
|
private function transferFilesViaFtp(array $changes, string $side, array &$report): void
|
||||||
{
|
{
|
||||||
$currentPaths = $this->extractFilePaths($changes);
|
if (empty($changes)) {
|
||||||
$pendingPaths = $this->getPendingFiles($side);
|
|
||||||
$allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths)));
|
|
||||||
|
|
||||||
if (empty($allPaths)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ftp = $this->connectFtp();
|
$ftp = $this->connectFtp();
|
||||||
if (! $ftp) {
|
if (! $ftp) {
|
||||||
$report[] = 'خطا در اتصال FTP به سیستم مقابل';
|
$report[] = 'خطا در اتصال FTP به سیستم مقابل';
|
||||||
|
|
@ -223,18 +174,31 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||||
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
|
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
|
||||||
$remoteBase = rtrim($this->ftpRemotePath, '/\\');
|
$remoteBase = rtrim($this->ftpRemotePath, '/\\');
|
||||||
$failedPaths = [];
|
|
||||||
|
|
||||||
foreach ($allPaths as $filePath) {
|
foreach ($changes as $change) {
|
||||||
|
$data = $change['changed_data'] ?? [];
|
||||||
|
foreach ($fileFields as $field) {
|
||||||
|
if (empty($data[$field])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||||
|
foreach ($files as $filePath) {
|
||||||
|
if (! $filePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = ltrim(str_replace('\\', '/', $filePath), '/');
|
||||||
$localFile = $localBase . '/' . $filePath;
|
$localFile = $localBase . '/' . $filePath;
|
||||||
$remoteFile = $remoteBase . '/' . $filePath;
|
$remoteFile = $remoteBase . '/' . $filePath;
|
||||||
$remoteFolder = dirname($remoteFile);
|
$remoteFolder = dirname($remoteFile);
|
||||||
|
|
||||||
if ($side === 'c2s') {
|
if ($side === 'c2s') {
|
||||||
if (! file_exists($localFile)) {
|
if (! file_exists($localFile)) {
|
||||||
$report[] = "فایل [{$filePath}] در سیستم محلی یافت نشد ✗";
|
$errorMsg = "فایل [{$filePath}] در سیستم محلی یافت نشد";
|
||||||
|
$report[] = $errorMsg . ' ✗';
|
||||||
Log::warning('FTP upload skipped - local file not found', [
|
Log::warning('FTP upload skipped - local file not found', [
|
||||||
'file' => $filePath,
|
'file' => $filePath,
|
||||||
'local_path' => $localFile,
|
'local_path' => $localFile,
|
||||||
|
|
@ -243,31 +207,40 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->createFtpDirectory($ftp, $remoteFolder)) {
|
if (! $this->createFtpDirectory($ftp, $remoteFolder)) {
|
||||||
$report[] = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد ✗";
|
$errorMsg = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد";
|
||||||
Log::error('FTP mkdir failed', ['file' => $filePath, 'remote_folder' => $remoteFolder]);
|
$report[] = $errorMsg . ' ✗';
|
||||||
$failedPaths[] = $filePath;
|
Log::error('FTP mkdir failed', [
|
||||||
|
'file' => $filePath,
|
||||||
|
'remote_folder' => $remoteFolder,
|
||||||
|
]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->ftpPut($ftp, $remoteFile, $localFile)) {
|
if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) {
|
||||||
$report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓";
|
$report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓";
|
||||||
Log::info('FTP file uploaded', ['file' => $filePath, 'size' => filesize($localFile)]);
|
Log::info('FTP file uploaded', [
|
||||||
|
'file' => $filePath,
|
||||||
|
'size' => filesize($localFile),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
$errorDetail = error_get_last();
|
$errorDetail = error_get_last();
|
||||||
$errorMsg = "فایل [{$filePath}] ارسال نشد (FTP)";
|
$errorMsg = "فایل [{$filePath}] ارسال نشد (FTP)";
|
||||||
if ($errorDetail) {
|
if ($errorDetail) {
|
||||||
$errorMsg .= ' - ' . $errorDetail['message'];
|
$errorMsg .= " - " . $errorDetail['message'];
|
||||||
}
|
}
|
||||||
$report[] = $errorMsg . ' ✗';
|
$report[] = $errorMsg . ' ✗';
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
Log::error('FTP upload failed', [
|
Log::error('FTP upload failed', [
|
||||||
'file' => $filePath, 'local_file' => $localFile,
|
'file' => $filePath,
|
||||||
'remote_file' => $remoteFile, 'error' => $errorDetail,
|
'local_file' => $localFile,
|
||||||
|
'remote_file' => $remoteFile,
|
||||||
|
'error' => $errorDetail,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (file_exists($localFile)) {
|
if (file_exists($localFile)) {
|
||||||
Log::debug('FTP download skipped - file already exists', ['file' => $filePath]);
|
Log::debug('FTP download skipped - file already exists', [
|
||||||
|
'file' => $filePath,
|
||||||
|
]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,72 +248,44 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
|
||||||
if (! is_dir($localFolder)) {
|
if (! is_dir($localFolder)) {
|
||||||
if (! mkdir($localFolder, 0755, true)) {
|
if (! mkdir($localFolder, 0755, true)) {
|
||||||
$report[] = "ایجاد پوشه [{$localFolder}] شکست خورد ✗";
|
$report[] = "ایجاد پوشه [{$localFolder}] شکست خورد ✗";
|
||||||
$failedPaths[] = $filePath;
|
Log::error('Local mkdir failed', [
|
||||||
Log::error('Local mkdir failed', ['folder' => $localFolder, 'error' => error_get_last()]);
|
'folder' => $localFolder,
|
||||||
|
'error' => error_get_last(),
|
||||||
|
]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->ftpGet($ftp, $localFile, $remoteFile)) {
|
if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) {
|
||||||
$report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓";
|
$report[] = "فایل [{$filePath}] دریافت شد (FTP) ✓";
|
||||||
Log::info('FTP file downloaded', ['file' => $filePath, 'size' => file_exists($localFile) ? filesize($localFile) : 0]);
|
Log::info('FTP file downloaded', [
|
||||||
|
'file' => $filePath,
|
||||||
|
'size' => filesize($localFile),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
$errorDetail = error_get_last();
|
$errorDetail = error_get_last();
|
||||||
$errorMsg = "فایل [{$filePath}] دریافت نشد (FTP)";
|
$errorMsg = "فایل [{$filePath}] دریافت نشد (FTP)";
|
||||||
if ($errorDetail) {
|
if ($errorDetail) {
|
||||||
$errorMsg .= ' - ' . $errorDetail['message'];
|
$errorMsg .= " - " . $errorDetail['message'];
|
||||||
}
|
}
|
||||||
$report[] = $errorMsg . ' ✗';
|
$report[] = $errorMsg . ' ✗';
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
Log::error('FTP download failed', [
|
Log::error('FTP download failed', [
|
||||||
'file' => $filePath, 'local_file' => $localFile,
|
'file' => $filePath,
|
||||||
'remote_file' => $remoteFile, 'error' => $errorDetail,
|
'local_file' => $localFile,
|
||||||
|
'remote_file' => $remoteFile,
|
||||||
|
'error' => $errorDetail,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$this->savePendingFiles($side, $failedPaths);
|
|
||||||
if (! empty($failedPaths)) {
|
|
||||||
$report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->ftpClose($ftp);
|
ftp_close($ftp);
|
||||||
Log::info('FTP connection closed');
|
Log::info('FTP connection closed');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function ftpGet(mixed $ftp, string $localFile, string $remoteFile): bool
|
private function connectFtp(): mixed
|
||||||
{
|
|
||||||
$tmpFile = $localFile . '.part';
|
|
||||||
|
|
||||||
if (file_exists($tmpFile)) {
|
|
||||||
@unlink($tmpFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ok = @ftp_get($ftp, $tmpFile, $remoteFile, FTP_BINARY);
|
|
||||||
|
|
||||||
if ($ok && file_exists($tmpFile)) {
|
|
||||||
return rename($tmpFile, $localFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($tmpFile)) {
|
|
||||||
@unlink($tmpFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function ftpPut(mixed $ftp, string $remoteFile, string $localFile): bool
|
|
||||||
{
|
|
||||||
return ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function ftpClose(mixed $ftp): void
|
|
||||||
{
|
|
||||||
ftp_close($ftp);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function connectFtp(): mixed
|
|
||||||
{
|
{
|
||||||
if (! function_exists('ftp_connect')) {
|
if (! function_exists('ftp_connect')) {
|
||||||
Log::error('FTP extension is not available');
|
Log::error('FTP extension is not available');
|
||||||
|
|
@ -377,7 +322,7 @@ protected function connectFtp(): mixed
|
||||||
return $ftp;
|
return $ftp;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createFtpDirectory($ftp, string $path): bool
|
private function createFtpDirectory($ftp, string $path): bool
|
||||||
{
|
{
|
||||||
$parts = explode('/', trim($path, '/'));
|
$parts = explode('/', trim($path, '/'));
|
||||||
$currentPath = '';
|
$currentPath = '';
|
||||||
|
|
@ -410,10 +355,10 @@ protected function createFtpDirectory($ftp, string $path): bool
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractFilePaths(array $changes): array
|
private function pushFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
||||||
{
|
{
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||||
$paths = [];
|
|
||||||
foreach ($changes as $change) {
|
foreach ($changes as $change) {
|
||||||
$data = $change['changed_data'] ?? [];
|
$data = $change['changed_data'] ?? [];
|
||||||
foreach ($fileFields as $field) {
|
foreach ($fileFields as $field) {
|
||||||
|
|
@ -422,109 +367,57 @@ private function extractFilePaths(array $changes): array
|
||||||
}
|
}
|
||||||
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||||
foreach ($files as $filePath) {
|
foreach ($files as $filePath) {
|
||||||
if ($filePath) {
|
if (! $filePath || ! Storage::disk('public')->exists($filePath)) {
|
||||||
$paths[] = ltrim(str_replace('\\', '/', $filePath), '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function pushFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
|
||||||
{
|
|
||||||
$currentPaths = $this->extractFilePaths($changes);
|
|
||||||
$pendingPaths = $this->getPendingFiles('c2s');
|
|
||||||
$allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths)));
|
|
||||||
$failedPaths = [];
|
|
||||||
|
|
||||||
foreach ($allPaths as $filePath) {
|
|
||||||
$localPath = Storage::disk('public')->path($filePath);
|
|
||||||
|
|
||||||
if (! file_exists($localPath)) {
|
|
||||||
$report[] = "فایل [{$filePath}] در سیستم محلی یافت نشد ✗";
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stream = fopen($localPath, 'rb');
|
|
||||||
try {
|
try {
|
||||||
$response = Http::timeout(600)
|
$response = Http::timeout(600)
|
||||||
->withHeader('X-Sync-Token', $this->token)
|
->withHeader('X-Sync-Token', $this->token)
|
||||||
->attach('file', $stream, basename($filePath))
|
->attach('file', Storage::disk('public')->get($filePath), basename($filePath))
|
||||||
->post("{$baseUrl}/api/sync/receive-file", ['path' => $filePath]);
|
->post("{$baseUrl}/api/sync/receive-file", ['path' => $filePath]);
|
||||||
|
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
$report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓";
|
$report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓";
|
||||||
} else {
|
} else {
|
||||||
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status();
|
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status();
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $e->getMessage();
|
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $e->getMessage();
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
} finally {
|
|
||||||
if (is_resource($stream)) {
|
|
||||||
fclose($stream);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->savePendingFiles('c2s', $failedPaths);
|
|
||||||
if (! empty($failedPaths)) {
|
|
||||||
$report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
private function syncFilesViaHttp(array $changes, string $baseUrl, array &$report): void
|
||||||
{
|
{
|
||||||
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
|
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
||||||
$currentPaths = $this->extractFilePaths($changes);
|
|
||||||
$pendingPaths = $this->getPendingFiles('s2c');
|
|
||||||
$allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths)));
|
|
||||||
$failedPaths = [];
|
|
||||||
|
|
||||||
foreach ($allPaths as $filePath) {
|
foreach ($changes as $change) {
|
||||||
if (Storage::disk('public')->exists($filePath)) {
|
$data = $change['changed_data'] ?? [];
|
||||||
|
foreach ($fileFields as $field) {
|
||||||
|
if (empty($data[$field])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
|
||||||
|
foreach ($files as $filePath) {
|
||||||
|
if (! $filePath || Storage::disk('public')->exists($filePath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$localFile = $localBase . '/' . $filePath;
|
|
||||||
$partFile = $localFile . '.part';
|
|
||||||
$localDir = dirname($localFile);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! is_dir($localDir)) {
|
|
||||||
mkdir($localDir, 0755, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($partFile)) {
|
|
||||||
@unlink($partFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = Http::timeout(60)
|
$response = Http::timeout(60)
|
||||||
->withHeader('X-Sync-Token', $this->token)
|
->withHeader('X-Sync-Token', $this->token)
|
||||||
->get("{$baseUrl}/api/sync/file", ['path' => $filePath]);
|
->get("{$baseUrl}/api/sync/file", ['path' => $filePath]);
|
||||||
|
|
||||||
if ($response->successful()) {
|
if ($response->successful()) {
|
||||||
file_put_contents($partFile, $response->body());
|
Storage::disk('public')->put($filePath, $response->body());
|
||||||
rename($partFile, $localFile);
|
|
||||||
$report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓";
|
$report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓";
|
||||||
} else {
|
|
||||||
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $response->status();
|
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if (file_exists($partFile)) {
|
|
||||||
@unlink($partFile);
|
|
||||||
}
|
|
||||||
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage();
|
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage();
|
||||||
$failedPaths[] = $filePath;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$this->savePendingFiles('s2c', $failedPaths);
|
|
||||||
if (! empty($failedPaths)) {
|
|
||||||
$report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -539,10 +432,6 @@ private function applyChange(array $change, string $prefix, array &$report): voi
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($action === 'deleted') {
|
if ($action === 'deleted') {
|
||||||
if ($table === 'patients') {
|
|
||||||
$this->deletePatientFilesFromDisk((int) $recordId);
|
|
||||||
$this->removePatientFilesFromPending((int) $recordId);
|
|
||||||
}
|
|
||||||
DB::table($table)->where('id', $recordId)->delete();
|
DB::table($table)->where('id', $recordId)->delete();
|
||||||
$report[] = "{$label} حذف ✓";
|
$report[] = "{$label} حذف ✓";
|
||||||
} elseif ($action === 'created') {
|
} elseif ($action === 'created') {
|
||||||
|
|
@ -555,9 +444,6 @@ private function applyChange(array $change, string $prefix, array &$report): voi
|
||||||
$report[] = "{$label} ایجاد ✓";
|
$report[] = "{$label} ایجاد ✓";
|
||||||
}
|
}
|
||||||
} elseif ($action === 'updated') {
|
} elseif ($action === 'updated') {
|
||||||
if ($table === 'patients') {
|
|
||||||
$this->deleteRemovedPatientFiles((int) $recordId, $data);
|
|
||||||
}
|
|
||||||
DB::table($table)->where('id', $recordId)->update($this->sanitize($data));
|
DB::table($table)->where('id', $recordId)->update($this->sanitize($data));
|
||||||
$report[] = "{$label} بروزرسانی ✓";
|
$report[] = "{$label} بروزرسانی ✓";
|
||||||
}
|
}
|
||||||
|
|
@ -566,228 +452,6 @@ private function applyChange(array $change, string $prefix, array &$report): voi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function deletePatientFilesFromDisk(int $patientId): void
|
|
||||||
{
|
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
|
||||||
$row = DB::table('patients')->find($patientId);
|
|
||||||
if (! $row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
if (empty($row->$field)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$files = json_decode($row->$field, true) ?? [];
|
|
||||||
foreach ($files as $path) {
|
|
||||||
if (! empty($path)) {
|
|
||||||
Storage::disk('public')->delete(ltrim(str_replace('\\', '/', $path), '/'));
|
|
||||||
Log::info('sync: deleted patient file from disk', ['file' => $path, 'patient_id' => $patientId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function removePatientFilesFromPending(int $patientId): void
|
|
||||||
{
|
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
|
||||||
$row = DB::table('patients')->find($patientId);
|
|
||||||
if (! $row) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$patientPaths = [];
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
if (empty($row->$field)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$files = json_decode($row->$field, true) ?? [];
|
|
||||||
foreach ($files as $path) {
|
|
||||||
if (! empty($path)) {
|
|
||||||
$patientPaths[] = ltrim(str_replace('\\', '/', $path), '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($patientPaths)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (['s2c', 'c2s'] as $direction) {
|
|
||||||
$pending = $this->getPendingFiles($direction);
|
|
||||||
$cleaned = array_values(array_diff($pending, $patientPaths));
|
|
||||||
if (count($cleaned) !== count($pending)) {
|
|
||||||
$this->savePendingFiles($direction, $cleaned);
|
|
||||||
Log::info('sync: removed deleted patient files from pending', [
|
|
||||||
'patient_id' => $patientId,
|
|
||||||
'direction' => $direction,
|
|
||||||
'removed' => count($pending) - count($cleaned),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private function deleteRemovedPatientFiles(int $patientId, array $newData): void
|
|
||||||
{
|
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
|
||||||
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
if (! array_key_exists($field, $newData)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$row = DB::table('patients')->find($patientId);
|
|
||||||
if (! $row || empty($row->$field)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$oldFiles = json_decode($row->$field, true) ?? [];
|
|
||||||
$newRaw = $newData[$field];
|
|
||||||
$newFiles = is_array($newRaw) ? $newRaw : (json_decode($newRaw, true) ?? []);
|
|
||||||
$newFiles = array_map(fn($p) => ltrim(str_replace('\\', '/', $p), '/'), $newFiles);
|
|
||||||
|
|
||||||
$removed = array_diff(
|
|
||||||
array_map(fn($p) => ltrim(str_replace('\\', '/', $p), '/'), $oldFiles),
|
|
||||||
$newFiles,
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($removed as $path) {
|
|
||||||
if (! empty($path)) {
|
|
||||||
Storage::disk('public')->delete($path);
|
|
||||||
Log::info('sync: deleted removed patient file from disk', ['file' => $path, 'patient_id' => $patientId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function pruneOrphanedPending(): int
|
|
||||||
{
|
|
||||||
$referenced = $this->getAllReferencedFilePaths();
|
|
||||||
$pruned = 0;
|
|
||||||
|
|
||||||
foreach (['s2c', 'c2s'] as $direction) {
|
|
||||||
$pending = $this->getPendingFiles($direction);
|
|
||||||
if (empty($pending)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$valid = array_values(array_filter($pending, fn($p) => in_array($p, $referenced)));
|
|
||||||
$removed = count($pending) - count($valid);
|
|
||||||
|
|
||||||
if ($removed > 0) {
|
|
||||||
$this->savePendingFiles($direction, $valid);
|
|
||||||
$pruned += $removed;
|
|
||||||
Log::info("pruneOrphanedPending: {$removed} orphaned paths removed from {$direction} pending");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $pruned;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getAllReferencedFilePaths(): array
|
|
||||||
{
|
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
|
||||||
$paths = [];
|
|
||||||
|
|
||||||
$rows = DB::table('patients')
|
|
||||||
->where(function ($q) use ($fileFields) {
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
$q->orWhereNotNull($field);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->get($fileFields);
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
if (empty($row->$field)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$files = json_decode($row->$field, true) ?? [];
|
|
||||||
foreach ($files as $path) {
|
|
||||||
if ($path) {
|
|
||||||
$paths[] = ltrim(str_replace('\\', '/', $path), '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_unique($paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function healMissingFiles(): int
|
|
||||||
{
|
|
||||||
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
|
|
||||||
$missing = [];
|
|
||||||
|
|
||||||
$rows = DB::table('patients')
|
|
||||||
->where(function ($q) use ($fileFields) {
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
$q->orWhereNotNull($field);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->get($fileFields);
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
foreach ($fileFields as $field) {
|
|
||||||
if (empty($row->$field)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$files = json_decode($row->$field, true) ?? [];
|
|
||||||
foreach ($files as $filePath) {
|
|
||||||
if (! $filePath) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$filePath = ltrim(str_replace('\\', '/', $filePath), '/');
|
|
||||||
if (! Storage::disk('public')->exists($filePath)) {
|
|
||||||
$missing[] = $filePath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$missing = array_values(array_unique($missing));
|
|
||||||
|
|
||||||
if (! empty($missing)) {
|
|
||||||
$existing = $this->getPendingFiles('s2c');
|
|
||||||
$merged = array_values(array_unique(array_merge($existing, $missing)));
|
|
||||||
$this->savePendingFiles('s2c', $merged);
|
|
||||||
Log::info('healMissingFiles: added to pending queue', ['count' => count($missing)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return count($missing);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getPendingFiles(string $direction): array
|
|
||||||
{
|
|
||||||
$record = \App\Models\OsurgInitial::where('init_parameter', "sync_pending_{$direction}")->first();
|
|
||||||
if (! $record || ! $record->attachment) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($record->attachment, true);
|
|
||||||
|
|
||||||
return is_array($decoded) && array_is_list($decoded) ? $decoded : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function savePendingFiles(string $direction, array $files): void
|
|
||||||
{
|
|
||||||
$files = array_values(array_unique(array_filter($files)));
|
|
||||||
\App\Models\OsurgInitial::updateOrCreate(
|
|
||||||
['init_parameter' => "sync_pending_{$direction}"],
|
|
||||||
['attachment' => empty($files) ? null : json_encode($files, JSON_UNESCAPED_UNICODE)],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function preQueueOutgoingFiles(array $changes): void
|
|
||||||
{
|
|
||||||
$paths = $this->extractFilePaths($changes);
|
|
||||||
if (empty($paths)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$existing = $this->getPendingFiles('c2s');
|
|
||||||
$this->savePendingFiles('c2s', array_merge($existing, $paths));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function saveOurCursor(string $cursor): void
|
private function saveOurCursor(string $cursor): void
|
||||||
{
|
{
|
||||||
\App\Models\OsurgInitial::updateOrCreate(
|
\App\Models\OsurgInitial::updateOrCreate(
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->trustHosts(at: fn () => ['.*'], subdomains: false);
|
//
|
||||||
$middleware->encryptCookies(except: ['backup_token']);
|
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { build } from 'vite'
|
|
||||||
|
|
||||||
await build({
|
|
||||||
configFile: false,
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: './resources/js/highlight-colors.js',
|
|
||||||
formats: ['es'],
|
|
||||||
fileName: () => 'highlight-colors.js',
|
|
||||||
},
|
|
||||||
outDir: './public/js',
|
|
||||||
emptyOutDir: false,
|
|
||||||
minify: true,
|
|
||||||
},
|
|
||||||
logLevel: 'warn',
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('highlight-colors.js built successfully → public/js/highlight-colors.js')
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"ariaieboy/filament-jalali": "2.0",
|
"ariaieboy/filament-jalali": "2.0",
|
||||||
"awcodes/richer-editor": "^1.2",
|
|
||||||
"barryvdh/laravel-dompdf": "^3.1",
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"bezhansalleh/filament-shield": "^4.1",
|
"bezhansalleh/filament-shield": "^4.1",
|
||||||
"filament/filament": "^4.0",
|
"filament/filament": "^4.0",
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "2016fafdbb8bbad0a40726291171c947",
|
"content-hash": "ab5f5cc9631f7f46d10ef65adf2fcbda",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
|
|
@ -217,91 +217,6 @@
|
||||||
],
|
],
|
||||||
"time": "2025-02-21T12:23:08+00:00"
|
"time": "2025-02-21T12:23:08+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "awcodes/richer-editor",
|
|
||||||
"version": "v1.2.2",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/awcodes/richer-editor.git",
|
|
||||||
"reference": "2e1dadfc6f582963fb2d9539890c06874aba1e6e"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/awcodes/richer-editor/zipball/2e1dadfc6f582963fb2d9539890c06874aba1e6e",
|
|
||||||
"reference": "2e1dadfc6f582963fb2d9539890c06874aba1e6e",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"filament/forms": "^4.3",
|
|
||||||
"league/html-to-markdown": "*",
|
|
||||||
"phiki/phiki": "^2.0",
|
|
||||||
"php": "^8.2",
|
|
||||||
"spatie/laravel-package-tools": "^1.15.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"larastan/larastan": "^3.0",
|
|
||||||
"laravel/pint": "^1.0",
|
|
||||||
"nunomaduro/collision": "^8.0",
|
|
||||||
"orchestra/testbench": "^9.0|^10.0",
|
|
||||||
"pestphp/pest": "^3.0",
|
|
||||||
"pestphp/pest-plugin-arch": "^3.0",
|
|
||||||
"pestphp/pest-plugin-laravel": "^3.0",
|
|
||||||
"pestphp/pest-plugin-livewire": "^3.0",
|
|
||||||
"phpstan/extension-installer": "^1.1",
|
|
||||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
|
||||||
"phpstan/phpstan-phpunit": "^2.0",
|
|
||||||
"rector/rector": "^2.0",
|
|
||||||
"spatie/laravel-ray": "^1.26"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"aliases": {
|
|
||||||
"RicherEditor": "Awcodes\\RicherEditor\\Facades\\RicherEditor"
|
|
||||||
},
|
|
||||||
"providers": [
|
|
||||||
"Awcodes\\RicherEditor\\RicherEditorServiceProvider"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Awcodes\\RicherEditor\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Adam Weston",
|
|
||||||
"email": "awcodes1@gmail.com",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A collection of extensions and tools to enhance the Filament Rich Editor field.",
|
|
||||||
"homepage": "https://github.com/awcodes/richer-editor",
|
|
||||||
"keywords": [
|
|
||||||
"awcodes",
|
|
||||||
"filament",
|
|
||||||
"filamentphp",
|
|
||||||
"laravel",
|
|
||||||
"richer-editor"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/awcodes/richer-editor/issues",
|
|
||||||
"source": "https://github.com/awcodes/richer-editor"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/awcodes",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-04-13T16:57:56+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
"version": "v3.1.1",
|
"version": "v3.1.1",
|
||||||
|
|
@ -3605,95 +3520,6 @@
|
||||||
},
|
},
|
||||||
"time": "2026-01-23T15:30:45+00:00"
|
"time": "2026-01-23T15:30:45+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "league/html-to-markdown",
|
|
||||||
"version": "5.1.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/thephpleague/html-to-markdown.git",
|
|
||||||
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
|
||||||
"reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-dom": "*",
|
|
||||||
"ext-xml": "*",
|
|
||||||
"php": "^7.2.5 || ^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"mikehaertl/php-shellcommand": "^1.1.0",
|
|
||||||
"phpstan/phpstan": "^1.8.8",
|
|
||||||
"phpunit/phpunit": "^8.5 || ^9.2",
|
|
||||||
"scrutinizer/ocular": "^1.6",
|
|
||||||
"unleashedtech/php-coding-standard": "^2.7 || ^3.0",
|
|
||||||
"vimeo/psalm": "^4.22 || ^5.0"
|
|
||||||
},
|
|
||||||
"bin": [
|
|
||||||
"bin/html-to-markdown"
|
|
||||||
],
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "5.2-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"League\\HTMLToMarkdown\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Colin O'Dell",
|
|
||||||
"email": "colinodell@gmail.com",
|
|
||||||
"homepage": "https://www.colinodell.com",
|
|
||||||
"role": "Lead Developer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Nick Cernis",
|
|
||||||
"email": "nick@cern.is",
|
|
||||||
"homepage": "http://modernnerd.net",
|
|
||||||
"role": "Original Author"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "An HTML-to-markdown conversion helper for PHP",
|
|
||||||
"homepage": "https://github.com/thephpleague/html-to-markdown",
|
|
||||||
"keywords": [
|
|
||||||
"html",
|
|
||||||
"markdown"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/thephpleague/html-to-markdown/issues",
|
|
||||||
"source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://www.colinodell.com/sponsor",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://www.paypal.me/colinpodell/10.00",
|
|
||||||
"type": "custom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/colinodell",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2023-07-12T21:21:09+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "league/mime-type-detection",
|
"name": "league/mime-type-detection",
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
|
|
@ -5425,80 +5251,6 @@
|
||||||
},
|
},
|
||||||
"time": "2020-10-15T08:29:30+00:00"
|
"time": "2020-10-15T08:29:30+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "phiki/phiki",
|
|
||||||
"version": "v2.2.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/phikiphp/phiki.git",
|
|
||||||
"reference": "24375dbc474dbc484de7d53c6bab0bb9e198f647"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/phikiphp/phiki/zipball/24375dbc474dbc484de7d53c6bab0bb9e198f647",
|
|
||||||
"reference": "24375dbc474dbc484de7d53c6bab0bb9e198f647",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"ext-mbstring": "*",
|
|
||||||
"php": "^8.2",
|
|
||||||
"psr/simple-cache": "^3.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"illuminate/support": "^11.45",
|
|
||||||
"laravel/pint": "^1.18.1",
|
|
||||||
"league/commonmark": "^2.5.3",
|
|
||||||
"orchestra/testbench": "^9.15",
|
|
||||||
"pestphp/pest": "^3.5.1",
|
|
||||||
"phpstan/extension-installer": "^1.4.3",
|
|
||||||
"phpstan/phpstan": "^2.0",
|
|
||||||
"symfony/var-dumper": "^7.1.6"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"league/commonmark": "Required to use the CommonMark adapter (^2.5.3)"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"laravel": {
|
|
||||||
"providers": [
|
|
||||||
"Phiki\\Adapters\\Laravel\\PhikiServiceProvider"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Phiki\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Ryan Chandler",
|
|
||||||
"email": "support@ryangjchandler.co.uk",
|
|
||||||
"homepage": "https://ryangjchandler.co.uk",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Syntax highlighting using TextMate grammars in PHP.",
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/phikiphp/phiki/issues",
|
|
||||||
"source": "https://github.com/phikiphp/phiki/tree/v2.2.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/sponsors/ryangjchandler",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://buymeacoffee.com/ryangjchandler",
|
|
||||||
"type": "other"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-04-01T15:56:08+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phpoption/phpoption",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.5",
|
"version": "1.9.5",
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,6 @@
|
||||||
'last_sync' => 'Last Sync',
|
'last_sync' => 'Last Sync',
|
||||||
'pending_changes' => 'Pending Changes',
|
'pending_changes' => 'Pending Changes',
|
||||||
'peer_address' => 'Peer System Address',
|
'peer_address' => 'Peer System Address',
|
||||||
'peer_connected' => 'Connected',
|
|
||||||
'peer_disconnected' => 'Disconnected',
|
|
||||||
'peer_checking' => 'Checking...',
|
|
||||||
'never' => 'Never',
|
'never' => 'Never',
|
||||||
'setup_guide' => 'Setup Guide',
|
'setup_guide' => 'Setup Guide',
|
||||||
'setup_description' => 'To enable synchronization, set the following values in the .env file on both systems:',
|
'setup_description' => 'To enable synchronization, set the following values in the .env file on both systems:',
|
||||||
|
|
@ -121,10 +118,6 @@
|
||||||
'label' => 'Backup',
|
'label' => 'Backup',
|
||||||
'success' => 'Backup completed successfully',
|
'success' => 'Backup completed successfully',
|
||||||
'failed' => 'Backup failed: database file not found',
|
'failed' => 'Backup failed: database file not found',
|
||||||
'status_preparing' => 'Preparing backup...',
|
|
||||||
'status_compressing' => 'Compressing files...',
|
|
||||||
'status_ready' => 'Backup file is ready ✓',
|
|
||||||
'download_button' => 'Download Backup',
|
|
||||||
],
|
],
|
||||||
'import' => [
|
'import' => [
|
||||||
'label' => 'Import from Database',
|
'label' => 'Import from Database',
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
'illness' => '2. Medical History',
|
'illness' => '2. Medical History',
|
||||||
'surgery_notes' => '3. Surgery Notes',
|
'surgery_notes' => '3. Surgery Notes',
|
||||||
'media' => '4. Patient Media',
|
'media' => '4. Patient Media',
|
||||||
'record_info' => 'Record Info',
|
|
||||||
],
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'icno' => 'National ID',
|
'icno' => 'National ID',
|
||||||
|
|
@ -81,6 +80,5 @@
|
||||||
'last_name_required' => 'Last name is required.',
|
'last_name_required' => 'Last name is required.',
|
||||||
'father_name_required' => 'Father name is required.',
|
'father_name_required' => 'Father name is required.',
|
||||||
'hand_phone_required' => 'Mobile number is required.',
|
'hand_phone_required' => 'Mobile number is required.',
|
||||||
'icno_unique' => 'This national ID is already registered for another patient.',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
'treatment_type' => 'Treatment Type',
|
'treatment_type' => 'Treatment Type',
|
||||||
'treatment_name' => 'Treatment Name',
|
'treatment_name' => 'Treatment Name',
|
||||||
'descriptions' => 'Descriptions',
|
'descriptions' => 'Descriptions',
|
||||||
'treatment_cost' => 'Cost (Toman)',
|
'treatment_cost' => 'Cost (Rial)',
|
||||||
'created_at' => 'Created At',
|
'created_at' => 'Created At',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,6 @@
|
||||||
'last_sync' => 'آخرین همگامسازی',
|
'last_sync' => 'آخرین همگامسازی',
|
||||||
'pending_changes' => 'تغییرات ارسال نشده',
|
'pending_changes' => 'تغییرات ارسال نشده',
|
||||||
'peer_address' => 'آدرس سیستم مقابل',
|
'peer_address' => 'آدرس سیستم مقابل',
|
||||||
'peer_connected' => 'آنلاین',
|
|
||||||
'peer_disconnected' => 'آفلاین',
|
|
||||||
'peer_checking' => 'در حال بررسی...',
|
|
||||||
'never' => 'هرگز',
|
'never' => 'هرگز',
|
||||||
'setup_guide' => 'راهنمای تنظیم',
|
'setup_guide' => 'راهنمای تنظیم',
|
||||||
'setup_description' => 'برای فعالسازی همگامسازی، مقادیر زیر را در فایل .env هر دو سیستم تنظیم کنید:',
|
'setup_description' => 'برای فعالسازی همگامسازی، مقادیر زیر را در فایل .env هر دو سیستم تنظیم کنید:',
|
||||||
|
|
@ -121,10 +118,6 @@
|
||||||
'label' => 'پشتیبانگیری',
|
'label' => 'پشتیبانگیری',
|
||||||
'success' => 'پشتیبانگیری با موفقیت انجام شد',
|
'success' => 'پشتیبانگیری با موفقیت انجام شد',
|
||||||
'failed' => 'خطا در پشتیبانگیری: فایل دیتابیس یافت نشد',
|
'failed' => 'خطا در پشتیبانگیری: فایل دیتابیس یافت نشد',
|
||||||
'status_preparing' => 'در حال آمادهسازی...',
|
|
||||||
'status_compressing' => 'در حال فشردهسازی فایلها...',
|
|
||||||
'status_ready' => 'فایل پشتیبان آماده دانلود است ✓',
|
|
||||||
'download_button' => 'دانلود فایل پشتیبان',
|
|
||||||
],
|
],
|
||||||
'import' => [
|
'import' => [
|
||||||
'label' => 'ورود از دیتابیس',
|
'label' => 'ورود از دیتابیس',
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
'illness' => '۲. کدامیک از بیماریهای زیر را داشته و یا دارید؟',
|
'illness' => '۲. کدامیک از بیماریهای زیر را داشته و یا دارید؟',
|
||||||
'surgery_notes' => '۳. توضیحات عمل',
|
'surgery_notes' => '۳. توضیحات عمل',
|
||||||
'media' => '۴. تصاویر بیمار',
|
'media' => '۴. تصاویر بیمار',
|
||||||
'record_info' => 'اطلاعات ثبت',
|
|
||||||
],
|
],
|
||||||
'fields' => [
|
'fields' => [
|
||||||
'icno' => 'کد ملی',
|
'icno' => 'کد ملی',
|
||||||
|
|
@ -83,6 +82,5 @@
|
||||||
'last_name_required' => 'وارد کردن نام خانوادگی الزامی است.',
|
'last_name_required' => 'وارد کردن نام خانوادگی الزامی است.',
|
||||||
'father_name_required' => 'وارد کردن نام پدر الزامی است.',
|
'father_name_required' => 'وارد کردن نام پدر الزامی است.',
|
||||||
'hand_phone_required' => 'وارد کردن شماره موبایل الزامی است.',
|
'hand_phone_required' => 'وارد کردن شماره موبایل الزامی است.',
|
||||||
'icno_unique' => 'این کد ملی قبلاً برای بیمار دیگری ثبت شده است.',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
'treatment_type' => 'نوع درمان',
|
'treatment_type' => 'نوع درمان',
|
||||||
'treatment_name' => 'نام درمان',
|
'treatment_name' => 'نام درمان',
|
||||||
'descriptions' => 'توضیحات',
|
'descriptions' => 'توضیحات',
|
||||||
'treatment_cost' => 'هزینه (تومان)',
|
'treatment_cost' => 'هزینه (ریال)',
|
||||||
'created_at' => 'تاریخ ثبت',
|
'created_at' => 'تاریخ ثبت',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@tiptap/core": "^3.22.4",
|
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
|
@ -1358,46 +1357,6 @@
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/core": {
|
|
||||||
"version": "3.22.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.4.tgz",
|
|
||||||
"integrity": "sha512-vGIGm/HpqLg8EAAQXQ+koV+/S828OEpzocfWcPOwo1u2QUVf9dQG47Yy6JJ8zFFaJwfv4dBcOXli+7BrJwsxDQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/pm": "3.22.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/pm": {
|
|
||||||
"version": "3.22.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.4.tgz",
|
|
||||||
"integrity": "sha512-hj8Qka6WcHRllHUdeSjDnq2XaisUo4KsoGJc1WcFpoa1Yd+OeD861zUMnV7DFVGdZRy45Obht0CUYJpXQ4yA4w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-changeset": "^2.3.0",
|
|
||||||
"prosemirror-commands": "^1.6.2",
|
|
||||||
"prosemirror-dropcursor": "^1.8.1",
|
|
||||||
"prosemirror-gapcursor": "^1.3.2",
|
|
||||||
"prosemirror-history": "^1.4.1",
|
|
||||||
"prosemirror-keymap": "^1.2.2",
|
|
||||||
"prosemirror-model": "^1.24.1",
|
|
||||||
"prosemirror-schema-list": "^1.5.0",
|
|
||||||
"prosemirror-state": "^1.4.3",
|
|
||||||
"prosemirror-tables": "^1.6.4",
|
|
||||||
"prosemirror-transform": "^1.10.2",
|
|
||||||
"prosemirror-view": "^1.38.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|
@ -2289,13 +2248,6 @@
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/orderedmap": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|
@ -2346,147 +2298,6 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prosemirror-changeset": {
|
|
||||||
"version": "2.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
|
||||||
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-transform": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-commands": {
|
|
||||||
"version": "1.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
|
||||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-model": "^1.0.0",
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"prosemirror-transform": "^1.10.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-dropcursor": {
|
|
||||||
"version": "1.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
|
||||||
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"prosemirror-transform": "^1.1.0",
|
|
||||||
"prosemirror-view": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-gapcursor": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-keymap": "^1.0.0",
|
|
||||||
"prosemirror-model": "^1.0.0",
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"prosemirror-view": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-history": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-state": "^1.2.2",
|
|
||||||
"prosemirror-transform": "^1.0.0",
|
|
||||||
"prosemirror-view": "^1.31.0",
|
|
||||||
"rope-sequence": "^1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-keymap": {
|
|
||||||
"version": "1.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
|
||||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"w3c-keyname": "^2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-model": {
|
|
||||||
"version": "1.25.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
|
||||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"orderedmap": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-schema-list": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-model": "^1.0.0",
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"prosemirror-transform": "^1.7.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-state": {
|
|
||||||
"version": "1.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
|
||||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-model": "^1.0.0",
|
|
||||||
"prosemirror-transform": "^1.0.0",
|
|
||||||
"prosemirror-view": "^1.27.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-tables": {
|
|
||||||
"version": "1.8.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
|
||||||
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-keymap": "^1.2.3",
|
|
||||||
"prosemirror-model": "^1.25.4",
|
|
||||||
"prosemirror-state": "^1.4.4",
|
|
||||||
"prosemirror-transform": "^1.10.5",
|
|
||||||
"prosemirror-view": "^1.41.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-transform": {
|
|
||||||
"version": "1.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
|
||||||
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-model": "^1.21.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prosemirror-view": {
|
|
||||||
"version": "1.41.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
|
|
||||||
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prosemirror-model": "^1.20.0",
|
|
||||||
"prosemirror-state": "^1.0.0",
|
|
||||||
"prosemirror-transform": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
|
@ -2549,13 +2360,6 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rope-sequence": {
|
|
||||||
"version": "1.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
|
||||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.2",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
|
@ -2788,13 +2592,6 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/w3c-keyname": {
|
|
||||||
"version": "2.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@tiptap/core": "^3.22.4",
|
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
<IfModule mod_negotiation.c>
|
|
||||||
Options -MultiViews -Indexes
|
|
||||||
</IfModule>
|
|
||||||
|
|
||||||
RewriteEngine On
|
|
||||||
|
|
||||||
# Handle Authorization Header
|
|
||||||
RewriteCond %{HTTP:Authorization} .
|
|
||||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
|
||||||
|
|
||||||
# Handle X-XSRF-Token Header
|
|
||||||
RewriteCond %{HTTP:x-xsrf-token} .
|
|
||||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
|
||||||
|
|
||||||
# Redirect Trailing Slashes If Not A Folder...
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_URI} (.+)/$
|
|
||||||
RewriteRule ^ %1 [L,R=301]
|
|
||||||
|
|
||||||
# Send Requests To Front Controller...
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteRule ^ index.php [L]
|
|
||||||
</IfModule>
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
||||||
window.toggleRichEditorFullscreen=e=>{e.classList.toggle("fullscreen"),e.querySelector(".fullscreen-toggle").classList.toggle("fi-active");let s=function(c){c.key==="Escape"&&(e.classList.remove("fullscreen"),e.querySelector(".fullscreen-toggle").classList.remove("fi-active"),document.removeEventListener("keydown",s))};e.classList.contains("fullscreen")?(document.addEventListener("keydown",s),e.setAttribute("tabindex","-1"),e.focus()):e.removeAttribute("tabindex")};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,131 +0,0 @@
|
||||||
{
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-400-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-arabic-400-normal-C4W5XURk.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-400-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-400-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-arabic-400-normal-DMZFCm7K.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-400-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-500-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-arabic-500-normal-Dqq3-xo3.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-500-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-500-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-arabic-500-normal-C_lbnnKa.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-500-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-600-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-arabic-600-normal-CWYTfCgi.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-600-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-600-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-arabic-600-normal-CPKvAnd1.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-600-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-700-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-arabic-700-normal-B5nPuCFv.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-700-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-700-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-arabic-700-normal-Dge_DOjm.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-arabic-700-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-400-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-400-normal-GKyMcI03.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-400-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-400-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-400-normal-BT_DHTc7.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-400-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-500-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-500-normal-Bg_BALlD.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-500-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-500-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-500-normal-6zZzgpg4.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-500-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-600-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-600-normal-BxJiDPKT.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-600-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-600-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-600-normal-D-zF-Oec.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-600-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-700-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-700-normal-DrB0PBU6.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-700-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-700-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-700-normal-9BlbvDRV.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-700-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-400-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-400-normal-DPxMaNjI.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-400-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-400-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-400-normal-BdGhO0lm.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-400-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-500-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-500-normal-4XgegWYb.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-500-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-500-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-500-normal-CgxvvVrG.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-500-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-600-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-600-normal-Di8rk35l.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-600-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-600-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-600-normal-CyxCUfFz.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-600-normal.woff2"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-700-normal.woff": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-700-normal-DMYLqBto.woff",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-700-normal.woff"
|
|
||||||
},
|
|
||||||
"node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-700-normal.woff2": {
|
|
||||||
"file": "assets/vazirmatn-latin-ext-700-normal-BfoXmNMx.woff2",
|
|
||||||
"src": "node_modules/@fontsource/vazirmatn/files/vazirmatn-latin-ext-700-normal.woff2"
|
|
||||||
},
|
|
||||||
"resources/css/app.css": {
|
|
||||||
"file": "assets/app-_TFpbERp.css",
|
|
||||||
"src": "resources/css/app.css",
|
|
||||||
"isEntry": true,
|
|
||||||
"name": "app",
|
|
||||||
"names": [
|
|
||||||
"app.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resources/css/filament/admin/theme.css": {
|
|
||||||
"file": "assets/theme-CFJJ2M3j.css",
|
|
||||||
"src": "resources/css/filament/admin/theme.css",
|
|
||||||
"isEntry": true,
|
|
||||||
"name": "theme",
|
|
||||||
"names": [
|
|
||||||
"theme.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resources/css/vazirmatn.css": {
|
|
||||||
"file": "assets/vazirmatn-CE-JssqN.css",
|
|
||||||
"src": "resources/css/vazirmatn.css",
|
|
||||||
"isEntry": true,
|
|
||||||
"name": "vazirmatn",
|
|
||||||
"names": [
|
|
||||||
"vazirmatn.css"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resources/js/app.js": {
|
|
||||||
"file": "assets/app-BuG9aa18.js",
|
|
||||||
"name": "app",
|
|
||||||
"src": "resources/js/app.js",
|
|
||||||
"isEntry": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
|
||||||
<rect width="64" height="64" rx="14" fill="#2563eb"/>
|
|
||||||
<!-- Medical cross -->
|
|
||||||
<rect x="26" y="14" width="12" height="36" rx="3" fill="white"/>
|
|
||||||
<rect x="14" y="26" width="36" height="12" rx="3" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 306 B |
|
|
@ -1 +0,0 @@
|
||||||
@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-ext-wght-normal-IYF56FF6.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-wght-normal-JEOLYBOO.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-ext-wght-normal-EOVOK2B5.woff2") format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-wght-normal-IRE366VL.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-vietnamese-wght-normal-CE5GGD3W.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-ext-wght-normal-HA22NDSG.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-wght-normal-NRMW37G5.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Application;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
define('LARAVEL_START', microtime(true));
|
|
||||||
|
|
||||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
|
||||||
require $maintenance;
|
|
||||||
}
|
|
||||||
|
|
||||||
require __DIR__.'/../vendor/autoload.php';
|
|
||||||
|
|
||||||
/** @var Application $app */
|
|
||||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
|
||||||
|
|
||||||
$app->handleRequest(Request::capture());
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
|
|
@ -210,36 +210,10 @@ [dir="rtl"] .prose p {
|
||||||
}
|
}
|
||||||
|
|
||||||
.fi-photo-gallery .filepond--root {
|
.fi-photo-gallery .filepond--root {
|
||||||
contain: layout style !important;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
min-height: 60px !important;
|
min-height: 60px !important;
|
||||||
height: auto !important;
|
|
||||||
margin-top: 8px !important;
|
margin-top: 8px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.fi-photo-gallery .filepond--list-scroller {
|
|
||||||
order: 2 !important;
|
|
||||||
position: relative !important;
|
|
||||||
transform: none !important;
|
|
||||||
height: auto !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
top: unset !important;
|
|
||||||
left: unset !important;
|
|
||||||
right: unset !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.fi-photo-gallery .filepond--drop-label {
|
|
||||||
order: 1 !important;
|
|
||||||
position: relative !important;
|
|
||||||
top: unset !important;
|
|
||||||
left: unset !important;
|
|
||||||
right: unset !important;
|
|
||||||
bottom: unset !important;
|
|
||||||
transform: none !important;
|
|
||||||
margin-top: 4px !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fi-photo-gallery-view .filepond--root,
|
.fi-photo-gallery-view .filepond--root,
|
||||||
[data-hide-dropzone] .filepond--root {
|
[data-hide-dropzone] .filepond--root {
|
||||||
|
|
@ -266,12 +240,6 @@ .fi-photo-gallery .filepond--item {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
|
||||||
.fi-photo-gallery .filepond--item * {
|
|
||||||
transition: none !important;
|
|
||||||
animation: none !important;
|
|
||||||
}
|
}
|
||||||
.fi-photo-gallery .filepond--image-preview-wrapper {
|
.fi-photo-gallery .filepond--image-preview-wrapper {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
|
|
@ -281,18 +249,16 @@ .fi-photo-gallery .filepond--image-preview-wrapper {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
transform: none !important;
|
|
||||||
}
|
}
|
||||||
.fi-photo-gallery .filepond--image-preview-overlay,
|
.fi-photo-gallery .filepond--image-preview-overlay,
|
||||||
.fi-photo-gallery .filepond--image-preview {
|
.fi-photo-gallery .filepond--image-preview {
|
||||||
height: 100% !important;
|
height: 56px !important;
|
||||||
max-height: none !important;
|
max-height: 56px !important;
|
||||||
}
|
}
|
||||||
.fi-photo-gallery .filepond--image-preview canvas {
|
.fi-photo-gallery .filepond--image-preview canvas {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
object-fit: cover !important;
|
object-fit: cover !important;
|
||||||
display: block !important;
|
|
||||||
}
|
}
|
||||||
.fi-photo-gallery .filepond--panel-root,
|
.fi-photo-gallery .filepond--panel-root,
|
||||||
.fi-photo-gallery .filepond--panel-top,
|
.fi-photo-gallery .filepond--panel-top,
|
||||||
|
|
@ -313,38 +279,15 @@ .fi-photo-gallery .filepond--file-action-button {
|
||||||
.fi-photo-gallery .filepond--file-info {
|
.fi-photo-gallery .filepond--file-info {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
.fi-photo-gallery .filepond--drop-label {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fi-video-gallery .filepond--root {
|
.fi-video-gallery .filepond--root {
|
||||||
contain: layout style !important;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
min-height: 60px !important;
|
min-height: 60px !important;
|
||||||
height: auto !important;
|
|
||||||
margin-top: 8px !important;
|
margin-top: 8px !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.fi-video-gallery .filepond--list-scroller {
|
|
||||||
order: 2 !important;
|
|
||||||
position: relative !important;
|
|
||||||
transform: none !important;
|
|
||||||
height: auto !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
top: unset !important;
|
|
||||||
left: unset !important;
|
|
||||||
right: unset !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
.fi-video-gallery .filepond--drop-label {
|
|
||||||
order: 1 !important;
|
|
||||||
position: relative !important;
|
|
||||||
top: unset !important;
|
|
||||||
left: unset !important;
|
|
||||||
right: unset !important;
|
|
||||||
bottom: unset !important;
|
|
||||||
transform: none !important;
|
|
||||||
margin-bottom: 4px !important;
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
.fi-video-gallery .filepond--list {
|
.fi-video-gallery .filepond--list {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
transform: none !important;
|
transform: none !important;
|
||||||
|
|
@ -394,6 +337,9 @@ .fi-video-gallery .filepond--file-action-button {
|
||||||
width: 1.4rem !important;
|
width: 1.4rem !important;
|
||||||
height: 1.4rem !important;
|
height: 1.4rem !important;
|
||||||
}
|
}
|
||||||
|
.fi-video-gallery .filepond--drop-label {
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fi-photo-gallery-view .filepond--file-action-button.filepond--action-remove-item,
|
.fi-photo-gallery-view .filepond--file-action-button.filepond--action-remove-item,
|
||||||
.fi-photo-gallery-view .filepond--file-action-button.filepond--action-revert-item-processing,
|
.fi-photo-gallery-view .filepond--file-action-button.filepond--action-revert-item-processing,
|
||||||
|
|
@ -452,8 +398,7 @@ #fi-lb-img.lb-zoomed.lb-dragging {
|
||||||
#fi-lb-close {
|
#fi-lb-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
left: 1rem;
|
right: 1rem;
|
||||||
right: auto;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
|
|
@ -520,139 +465,3 @@ #fi-lb-hint {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
#fi-lb-caption {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
width: 280px;
|
|
||||||
height: auto;
|
|
||||||
max-height: calc(100% - 2rem);
|
|
||||||
background: rgba(255, 255, 255, 0.97);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
|
||||||
border-radius: 14px;
|
|
||||||
direction: rtl;
|
|
||||||
z-index: 4;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
#fi-lb-caption.lb-cap-hidden {
|
|
||||||
transform: translateX(calc(100% + 1rem));
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 9px;
|
|
||||||
padding: 11px 13px 9px;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
#fi-lb-cap-icon {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 7px;
|
|
||||||
background: rgba(59, 130, 246, 0.12);
|
|
||||||
border: 1px solid rgba(59, 130, 246, 0.25);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-title {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: rgba(10, 15, 35, 0.9);
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-close {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: rgba(0, 0, 0, 0.08);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.18);
|
|
||||||
color: rgba(0, 0, 0, 0.75);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: background 0.2s, color 0.2s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-close:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
color: rgba(0, 0, 0, 0.95);
|
|
||||||
}
|
|
||||||
#fi-lb-cap-body {
|
|
||||||
padding: 14px 13px 18px;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
line-height: 1.9;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
color: rgba(20, 30, 50, 0.88);
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(0,0,0,0.15) transparent;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-body::-webkit-scrollbar { width: 3px; }
|
|
||||||
#fi-lb-cap-body::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 2px; }
|
|
||||||
#fi-lb-cap-body mark { border-radius: 2px; padding: 0 2px; }
|
|
||||||
#fi-lb-cap-body ul, #fi-lb-cap-body ol { padding-right: 18px; margin: 4px 0; }
|
|
||||||
#fi-lb-cap-body li { margin: 2px 0; }
|
|
||||||
#fi-lb-cap-body strong { color: rgba(10, 20, 40, 0.95); }
|
|
||||||
#fi-lb-cap-body blockquote { border-right: 3px solid rgba(59,130,246,0.4); padding-right: 10px; margin: 6px 0; color: rgba(20,30,50,0.6); font-style: normal; }
|
|
||||||
#fi-lb-cap-toggle {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
right: 1rem;
|
|
||||||
z-index: 10;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(40, 44, 60, 0.85);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background 0.2s, border-color 0.2s, opacity 0.2s;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-toggle.lb-cap-toggle-visible {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
#fi-lb-cap-toggle:hover {
|
|
||||||
background: rgba(60, 65, 90, 0.95);
|
|
||||||
border-color: rgba(255, 255, 255, 0.45);
|
|
||||||
}
|
|
||||||
#fi-lb-img {
|
|
||||||
max-width: calc(100vw - 310px);
|
|
||||||
transition: max-width 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease, transform 0.2s ease;
|
|
||||||
}
|
|
||||||
#fi-lightbox.lb-no-caption #fi-lb-img,
|
|
||||||
#fi-lightbox.lb-cap-collapsed #fi-lb-img {
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.fi-text-expand > summary {
|
|
||||||
list-style: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
details.fi-text-expand > summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
details.fi-text-expand[open] > summary {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { Mark } from '@tiptap/core'
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
return Mark.create({
|
|
||||||
name: 'highlightColor',
|
|
||||||
|
|
||||||
priority: 1001,
|
|
||||||
|
|
||||||
parseHTML() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
tag: 'span[data-hbg]',
|
|
||||||
getAttrs: (el) => ({ 'data-hbg': el.getAttribute('data-hbg') }),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
|
||||||
const color = HTMLAttributes['data-hbg']
|
|
||||||
return [
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
'data-hbg': color,
|
|
||||||
style: `background-color: ${color};`,
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
addAttributes() {
|
|
||||||
return {
|
|
||||||
'data-hbg': {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (el) => el.getAttribute('data-hbg'),
|
|
||||||
renderHTML: (attrs) => {
|
|
||||||
if (!attrs['data-hbg']) return {}
|
|
||||||
return { 'data-hbg': attrs['data-hbg'] }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
@php
|
|
||||||
$files = $getState() ?? [];
|
|
||||||
$files = is_array($files) ? array_values(array_filter($files)) : [];
|
|
||||||
$urls = array_values(array_map(
|
|
||||||
fn ($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path),
|
|
||||||
$files
|
|
||||||
));
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@if(count($urls) > 0)
|
|
||||||
<div class="flex flex-col gap-1.5">
|
|
||||||
@foreach($urls as $idx => $url)
|
|
||||||
@php
|
|
||||||
$filename = basename(parse_url($url, PHP_URL_PATH));
|
|
||||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
|
||||||
$isVideo = in_array($ext, ['mp4', 'mov', 'avi', 'mkv', 'webm']);
|
|
||||||
@endphp
|
|
||||||
<a
|
|
||||||
href="{{ $url }}"
|
|
||||||
target="_blank"
|
|
||||||
download
|
|
||||||
class="flex items-center gap-2 text-sm text-primary-600 hover:text-primary-800 transition-colors"
|
|
||||||
>
|
|
||||||
@if($isVideo)
|
|
||||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.069A1 1 0 0121 8.879V15.12a1 1 0 01-1.447.894L15 14M3 8a2 2 0 012-2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z"/>
|
|
||||||
</svg>
|
|
||||||
@else
|
|
||||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
|
|
||||||
</svg>
|
|
||||||
@endif
|
|
||||||
<span class="truncate max-w-[180px]">{{ $idx + 1 }}. {{ $filename }}</span>
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<span class="text-sm text-gray-400">-</span>
|
|
||||||
@endif
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
@livewire('patient-media-player', ['patientId' => $patientId, 'field' => $field], key('media-' . $patientId . '-' . $field))
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
@php
|
|
||||||
$images = $getState() ?? [];
|
|
||||||
$images = is_array($images) ? array_values(array_filter($images)) : [];
|
|
||||||
$urls = array_values(array_map(
|
|
||||||
fn ($path) => \Illuminate\Support\Facades\Storage::disk('public')->url($path),
|
|
||||||
$images
|
|
||||||
));
|
|
||||||
$group = 'gallery-' . md5(implode(',', $urls));
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@if(count($urls) > 0)
|
|
||||||
<div class="grid grid-cols-4 gap-2">
|
|
||||||
@foreach($urls as $idx => $url)
|
|
||||||
<div
|
|
||||||
class="relative cursor-pointer rounded-lg overflow-hidden border border-gray-200 hover:border-primary-400 hover:opacity-80 transition-all shadow-sm group"
|
|
||||||
style="aspect-ratio:16/9;"
|
|
||||||
data-lb-group="{{ $group }}"
|
|
||||||
data-lb-idx="{{ $idx }}"
|
|
||||||
data-lb-src="{{ $url }}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="{{ $url }}"
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
alt=""
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition duration-200"></div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<span class="text-sm text-gray-400">-</span>
|
|
||||||
@endif
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
@livewire('expandable-text', ['html' => $html], key($key))
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
<div>
|
|
||||||
<div class="prose prose-sm max-w-none overflow-hidden transition-all duration-300"
|
|
||||||
style="{{ $expanded ? '' : 'max-height: 8rem;' }}">
|
|
||||||
{!! $html !!}
|
|
||||||
</div>
|
|
||||||
<button type="button" wire:click="toggle" class="text-primary-600 text-xs font-semibold hover:underline mt-1">
|
|
||||||
{{ $expanded ? 'کمتر' : 'بیشتر' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<div>
|
|
||||||
{{ $this->form }}
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,20 +1,4 @@
|
||||||
<div
|
<div class="flex items-center gap-1">
|
||||||
class="flex items-center gap-1"
|
|
||||||
@can('View:SyncPage')
|
|
||||||
x-init="if (['/admin', '/admin/'].includes(window.location.pathname)) $wire.checkPeerStatus()"
|
|
||||||
@endcan
|
|
||||||
>
|
|
||||||
|
|
||||||
@can('View:SyncPage')
|
|
||||||
@if($peerChecked)
|
|
||||||
<span class="text-xs font-medium px-1 whitespace-nowrap"
|
|
||||||
style="{{ $peerConnected ? 'color: #15803d' : 'color: #dc2626' }}"
|
|
||||||
x-bind:style="{{ json_encode($peerConnected) }} ? ($store.theme === 'dark' ? 'color: #4ade80' : 'color: #15803d') : ($store.theme === 'dark' ? 'color: #f87171' : 'color: #dc2626')"
|
|
||||||
@if($peerPath) title="{{ $peerPath }}" @endif>
|
|
||||||
{{ $peerConnected ? __('navigation.sync.peer_connected') : __('navigation.sync.peer_disconnected') }}
|
|
||||||
</span>
|
|
||||||
@endif
|
|
||||||
@endcan
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
x-data
|
x-data
|
||||||
|
|
@ -29,6 +13,7 @@ class="flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 hover:t
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@can('View:SyncPage')
|
||||||
<div
|
<div
|
||||||
class="relative"
|
class="relative"
|
||||||
x-data="{
|
x-data="{
|
||||||
|
|
@ -44,7 +29,6 @@ class="relative"
|
||||||
confirmSync() {
|
confirmSync() {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.showConfirm = true;
|
this.showConfirm = true;
|
||||||
$wire.checkPeerStatus();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
startSync() {
|
startSync() {
|
||||||
|
|
@ -76,8 +60,7 @@ class="relative"
|
||||||
if (this.done) {
|
if (this.done) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
}"
|
}"
|
||||||
x-on:sync-finished.window="
|
x-on:sync-finished.window="
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
@ -111,7 +94,6 @@ class="flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 hover:t
|
||||||
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"
|
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"
|
style="display:none"
|
||||||
>
|
>
|
||||||
@can('View:SyncPage')
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="confirmSync()"
|
@click="confirmSync()"
|
||||||
|
|
@ -129,19 +111,18 @@ class="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-
|
||||||
<x-heroicon-o-document-magnifying-glass class="w-4 h-4 text-info-500 shrink-0" />
|
<x-heroicon-o-document-magnifying-glass class="w-4 h-4 text-info-500 shrink-0" />
|
||||||
{{ __('navigation.sync.details') }}
|
{{ __('navigation.sync.details') }}
|
||||||
</a>
|
</a>
|
||||||
@endcan
|
|
||||||
|
|
||||||
<a
|
<button
|
||||||
href="{{ route('backup.download') }}"
|
type="button"
|
||||||
@click="open = false"
|
@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"
|
wire:click="runBackup"
|
||||||
|
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-arrow-down-tray class="w-4 h-4 text-success-500 shrink-0" />
|
<x-heroicon-o-arrow-down-tray class="w-4 h-4 text-success-500 shrink-0" />
|
||||||
{{ __('navigation.backup.label') }}
|
{{ __('navigation.backup.label') }}
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@can('View:SyncPage')
|
|
||||||
<template x-teleport="body">
|
<template x-teleport="body">
|
||||||
<div
|
<div
|
||||||
x-show="showConfirm"
|
x-show="showConfirm"
|
||||||
|
|
@ -240,8 +221,8 @@ class="px-4 py-2 text-sm font-medium text-white rounded-lg transition"
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@endcan
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@endcan
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue