Compare commits

..

No commits in common. "main" and "test" have entirely different histories.
main ... test

112 changed files with 314 additions and 10283 deletions

View File

@ -1,7 +1,7 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:Cwol1ZZBqRAY2Em+k6lDh8PvD2RkmfSCSR5c3t+ypjg=
APP_DEBUG=false
APP_DEBUG=true
APP_LOCALE=fa
APP_FALLBACK_LOCALE=en
@ -10,14 +10,14 @@ APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=warning
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
@ -26,7 +26,7 @@ DB_CONNECTION=sqlite
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=file
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
@ -36,7 +36,7 @@ BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=file
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
@ -63,7 +63,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
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_PORT=8000

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -4,7 +4,6 @@
namespace App\Filament\Pages;
use App\Models\OsurgInitial;
use App\Models\SyncLog;
use App\Services\OsurgImportService;
use App\Services\SyncService;
@ -59,7 +58,7 @@ public function getTitle(): string
public function getPendingCount(): int
{
return SyncLog::changesSince(OsurgInitial::val('our_sync_cursor') ?: null)->count();
return SyncLog::changesSince(SyncLog::lastSyncId())->count();
}
public function getLastSyncTime(): string

View File

@ -20,26 +20,18 @@
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Illuminate\Support\Facades\Storage;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Radio;
use App\Filament\RichEditor\HighlightColorPlugin;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TimePicker;
use Filament\Forms\Components\Placeholder;
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\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
@ -62,63 +54,9 @@ class PatientResource extends Resource
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
{
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
@ -195,15 +133,7 @@ public static function form(Schema $schema): Schema
TextInput::make('icno')
->label(__('patients.fields.icno'))
->required()
->maxLength(11)
->unique(
table: Patient::class,
column: 'icno',
ignorable: fn ($record) => $record,
)
->validationMessages([
'unique' => __('patients.validation.icno_unique'),
]),
->maxLength(11),
TextInput::make('hand_phone')
->label(__('patients.fields.hand_phone'))
@ -213,10 +143,6 @@ public static function form(Schema $schema): Schema
])
->maxLength(50),
TextInput::make('home_phone')
->label(__('patients.fields.home_phone'))
->maxLength(50),
TextInput::make('age')
->label(__('patients.fields.age'))
->numeric()
@ -252,6 +178,10 @@ public static function form(Schema $schema): Schema
->label(__('patients.fields.job'))
->maxLength(50),
TextInput::make('home_phone')
->label(__('patients.fields.home_phone'))
->maxLength(50),
TextInput::make('work_phone')
->label(__('patients.fields.work_phone'))
->maxLength(50),
@ -357,10 +287,9 @@ public static function form(Schema $schema): Schema
RichEditor::make('surgery_before')
->label(__('patients.fields.surgery_before'))
->extraInputAttributes(['style' => 'min-height: 150px'])
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike', 'link'],
['highlightYellow', 'highlightRed'],
['highlight'],
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
['blockquote', 'bulletList', 'orderedList'],
['undo', 'redo'],
@ -369,10 +298,9 @@ public static function form(Schema $schema): Schema
RichEditor::make('sergery_after')
->label(__('patients.fields.sergery_after'))
->extraInputAttributes(['style' => 'min-height: 150px'])
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike', 'link'],
['highlightYellow', 'highlightRed'],
['highlight'],
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
['blockquote', 'bulletList', 'orderedList'],
['undo', 'redo'],
@ -388,10 +316,7 @@ public static function form(Schema $schema): Schema
->multiple()
->image()
->panelLayout('grid')
->extraAttributes(fn (string $operation) => $operation === 'view'
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
: ['class' => 'fi-photo-gallery']
)
->extraAttributes(['class' => 'fi-photo-gallery'])
->disk('public')
->directory(function ($record) {
$year = \Morilog\Jalali\Jalalian::now()->getYear();
@ -400,8 +325,6 @@ public static function form(Schema $schema): Schema
})
->downloadable()
->openable()
->disabled(fn (string $operation) => $operation === 'view')
->deletable(fn (string $operation) => $operation !== 'view')
->fetchFileInformation(false),
FileUpload::make('photos_after')
@ -409,10 +332,7 @@ public static function form(Schema $schema): Schema
->multiple()
->image()
->panelLayout('grid')
->extraAttributes(fn (string $operation) => $operation === 'view'
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
: ['class' => 'fi-photo-gallery']
)
->extraAttributes(['class' => 'fi-photo-gallery'])
->disk('public')
->directory(function ($record) {
$year = \Morilog\Jalali\Jalalian::now()->getYear();
@ -421,8 +341,6 @@ public static function form(Schema $schema): Schema
})
->downloadable()
->openable()
->disabled(fn (string $operation) => $operation === 'view')
->deletable(fn (string $operation) => $operation !== 'view')
->fetchFileInformation(false),
FileUpload::make('videos')
@ -445,14 +363,8 @@ public static function form(Schema $schema): Schema
])
->maxSize(102400)
->downloadable()
->openable()
->panelLayout('grid')
->extraAttributes(fn (string $operation) => $operation === 'view'
? ['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true']
: ['class' => 'fi-video-gallery']
)
->disabled(fn (string $operation) => $operation === 'view')
->deletable(fn (string $operation) => $operation !== 'view')
->extraAttributes(['class' => 'fi-video-gallery'])
->fetchFileInformation(false),
FileUpload::make('audio_files')
@ -472,13 +384,6 @@ public static function form(Schema $schema): Schema
'audio/aiff', 'audio/x-aiff',
])
->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),
])
@ -516,186 +421,6 @@ public static function form(Schema $schema): Schema
->color('gray')
->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(),
]);
@ -717,19 +442,13 @@ public static function table(Table $table): Table
->searchable()
->sortable(),
TextColumn::make('icno')
->label(__('patients.fields.icno'))
->searchable()
->placeholder('-'),
TextColumn::make('hand_phone')
->label(__('patients.fields.hand_phone'))
->searchable()
->placeholder('-'),
TextColumn::make('home_phone')
->label(__('patients.fields.home_phone'))
->placeholder('-')
->hidden(),
->placeholder('-'),
TextColumn::make('creator.name')
->label(__('patients.fields.created_by'))
@ -756,7 +475,17 @@ public static function table(Table $table): Table
? ['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()
->action(
Action::make('viewSurgeryBefore')
@ -781,7 +510,17 @@ public static function table(Table $table): Table
? ['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()
->action(
Action::make('viewSergeryAfter')
@ -1110,13 +849,6 @@ public static function table(Table $table): Table
\Filament\Forms\Components\RichEditor::make('content')
->label(__('prescriptions.fields.content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
@ -1157,13 +889,6 @@ public static function table(Table $table): Table
\Filament\Forms\Components\RichEditor::make('lab_content')
->label(__('prescriptions.fields.lab_content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
])
@ -1249,18 +974,14 @@ public static function table(Table $table): Table
$record->audio_files,
]))
)
->modalWidth(Width::ThreeExtraLarge)
->modalWidth(Width::ExtraLarge)
->fillForm(fn (Patient $record): array => [
'photos_before' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_before),
'photos_after' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->photos_after),
'videos' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->videos),
'audio_files' => \App\Filament\Resources\PatientResource\Pages\EditPatient::normalizeFileList($record->audio_files),
'surgery_before' => $record->surgery_before,
'sergery_after' => $record->sergery_after,
])
->schema([
\Filament\Forms\Components\Hidden::make('surgery_before'),
\Filament\Forms\Components\Hidden::make('sergery_after'),
Grid::make(2)
->schema([
FileUpload::make('photos_before')
@ -1277,12 +998,7 @@ public static function table(Table $table): Table
->openable()
->disabled()
->fetchFileInformation(false)
->extraAttributes(fn ($get): array => [
'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'),
]),
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
FileUpload::make('photos_after')
->label(__('patients.fields.photos_after'))
@ -1298,12 +1014,7 @@ public static function table(Table $table): Table
->openable()
->disabled()
->fetchFileInformation(false)
->extraAttributes(fn ($get): array => [
'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'),
]),
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
FileUpload::make('videos')
->label(__('patients.fields.videos'))
@ -1326,8 +1037,7 @@ public static function table(Table $table): Table
->downloadable()
->disabled()
->fetchFileInformation(false)
->panelLayout('grid')
->extraAttributes(['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true']),
->extraAttributes(['data-hide-dropzone' => 'true']),
FileUpload::make('audio_files')
->label(__('patients.fields.audio_files'))
@ -1366,7 +1076,6 @@ public static function table(Table $table): Table
->modalSubmitAction(false)
->modalCancelActionLabel('بستن'),
ViewAction::make()->iconButton()->color('gray'),
EditAction::make()->iconButton(),
DeleteAction::make()->iconButton(),
], position: \Filament\Tables\Enums\RecordActionsPosition::BeforeCells)
@ -1491,7 +1200,6 @@ public static function getPages(): array
return [
'index' => Pages\ListPatients::route('/'),
'create' => Pages\CreatePatient::route('/create'),
'view' => Pages\ViewPatient::route('/{record}'),
'edit' => Pages\EditPatient::route('/{record}/edit'),
];
}

View File

@ -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()])),
];
}
}

View File

@ -6,7 +6,6 @@
use App\Filament\Resources\PatientResource\Pages\CreatePatient;
use App\Filament\Resources\PatientResource\Pages\EditPatient;
use App\Filament\Resources\PatientResource\Pages\ViewPatient;
use App\Models\Doctor;
use App\Models\PaymentType;
use App\Models\Treatment;
@ -33,7 +32,7 @@ class VisitsRelationManager extends RelationManager
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
@ -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
{
return $table
@ -232,10 +219,8 @@ public function table(Table $table): Table
->recordActions([
EditAction::make()
->slideOver()
->modalWidth('4xl')
->modalHeading(fn () => __('filament-actions::edit.single.modal.heading', ['label' => $this->getOwnerRecord()->full_name])),
DeleteAction::make()
->modalHeading(fn () => __('filament-actions::delete.single.modal.heading', ['label' => $this->getOwnerRecord()->full_name])),
->modalWidth('4xl'),
DeleteAction::make(),
])
->toolbarActions([]);
}

View File

@ -20,7 +20,6 @@
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use App\Filament\RichEditor\HighlightColorPlugin;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Resources\Resource;
@ -165,13 +164,6 @@ public static function form(Schema $schema): Schema
RichEditor::make('content')
->label(__('prescriptions.fields.content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
@ -232,13 +224,6 @@ public static function form(Schema $schema): Schema
RichEditor::make('lab_content')
->label(__('prescriptions.fields.lab_content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
])

View File

@ -44,14 +44,6 @@ class VisitResource extends Resource
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
{
return [
@ -331,14 +323,8 @@ public static function table(Table $table): Table
->columns(2)
->columnSpan(2)
->query(fn ($query, array $data) => $query
->when($data['from_date'] ?? null, function ($q) use ($data) {
$fromDate = \Carbon\Carbon::parse($data['from_date'])->format('Y-m-d');
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]);
})
->when($data['from_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date']))
->when($data['to_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date']))
)
->indicateUsing(function (array $data): array {
$indicators = [];

View File

@ -28,31 +28,21 @@ protected function getHeaderWidgets(): array
public function applyTableFilters(): void
{
parent::applyTableFilters();
$this->applySortFromFilters();
$this->dispatchStatsUpdate();
}
public function updatedTableFilters(): void
{
parent::updatedTableFilters();
$this->applySortFromFilters();
$this->dispatchStatsUpdate();
}
public function resetTableFiltersForm(): void
{
parent::resetTableFiltersForm();
$this->tableSort = null;
$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
{
$filters = $this->tableFilters ?? [];

View File

@ -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,
];
}
}

View File

@ -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 [];
}
}

View File

@ -48,13 +48,11 @@ protected function buildQuery()
$query = Visit::query();
if ($this->fromDate) {
$fromDate = \Carbon\Carbon::parse($this->fromDate)->format('Y-m-d');
$query->whereRaw("SUBSTRING(visit_date, 1, 10) >= ?", [$fromDate]);
$query->whereDate('visit_date', '>=', $this->fromDate);
}
if ($this->toDate) {
$toDate = \Carbon\Carbon::parse($this->toDate)->format('Y-m-d');
$query->whereRaw("SUBSTRING(visit_date, 1, 10) <= ?", [$toDate]);
$query->whereDate('visit_date', '<=', $this->toDate);
}
if ($this->doctor) {

View File

@ -25,9 +25,10 @@ public function export(Request $request): JsonResponse
return response()->json(['error' => 'Unauthorized'], 401);
}
$since = $request->query('since') ?: null;
$since = (int) $request->query('since', 0);
return response()->json([
'last_sync_id' => SyncLog::lastSyncId(),
'changes' => SyncLog::changesSince($since),
]);
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -14,18 +14,37 @@ class SettingsDropdown extends Component
public bool $syncFailed = false;
public array $reportLines = [];
public bool $peerConnected = false;
public ?string $peerPath = null;
public bool $peerChecked = false;
public function checkPeerStatus(): void
public function runBackup(): void
{
$result = SyncService::fromConfig()->ping();
$dbPath = database_path('database.sqlite');
$this->peerConnected = $result['connected'];
$this->peerPath = $result['path'];
$this->peerChecked = true;
if (! file_exists($dbPath)) {
Notification::make()
->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
{
$service = SyncService::fromConfig();

View File

@ -32,22 +32,12 @@ public static function lastSyncId(): int
return (int) static::where('action', 'synced')->max('id') ?: 0;
}
public static function changesSince(?string $since): \Illuminate\Database\Eloquent\Collection
public static function changesSince(int $since): \Illuminate\Database\Eloquent\Collection
{
$query = static::whereIn('action', ['created', 'updated', 'deleted'])
->orderBy('datetime')
->orderBy('id');
if ($since !== null && $since !== '') {
$query->where('datetime', '>', $since);
}
return $query->get();
}
protected function serializeDate(\DateTimeInterface $date): string
{
return $date->format('Y-m-d H:i:s');
return static::where('id', '>', $since)
->whereIn('action', ['created', 'updated', 'deleted'])
->orderBy('id')
->get();
}
public static function markSynced(string $ip): static

View File

@ -38,10 +38,8 @@ public function updated(Model $model): void
}
$changedData = [];
foreach ($dirty as $field => $rawValue) {
$changedData[$field] = $rawValue instanceof \BackedEnum
? $rawValue->value
: (is_object($rawValue) ? (string) $rawValue : $rawValue);
foreach ($dirty as $field => $newValue) {
$changedData[$field] = $model->getAttribute($field);
}
SyncLog::create([

View File

@ -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();
}
}

View File

@ -12,19 +12,15 @@
use App\Models\SurgeryCenter;
use App\Models\Treatment;
use App\Models\LabTest;
use App\Models\User;
use App\Models\Visit;
use Spatie\Permission\Models\Role;
use App\Observers\PatientIllnessObserver;
use App\Observers\PatientObserver;
use App\Observers\SyncObserver;
use App\Observers\VisitObserver;
use App\Filament\Widgets\VisitStatsWidget;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Support\Enums\Alignment;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\URL;
use Livewire\Livewire;
class AppServiceProvider extends ServiceProvider
@ -33,18 +29,7 @@ public function register(): 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('patient-media-player', \App\Livewire\PatientMediaPlayer::class);
Livewire::component('expandable-text', \App\Livewire\ExpandableText::class);
$observer = new SyncObserver();
Patient::observe($observer);
@ -60,9 +45,6 @@ public function boot(): void
Prescription::observe($observer);
SurgeryAppointment::observe($observer);
Visit::observe($observer);
Visit::observe(VisitObserver::class);
User::observe($observer);
Role::observe($observer);
DeleteAction::configureUsing(function (DeleteAction $action): void {
$action->modalAlignment(Alignment::Start);

View File

@ -82,166 +82,6 @@ public function panel(Panel $panel): Panel
->renderHook(
PanelsRenderHook::BODY_END,
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>
(function () {
var speeds = [0.5, 1, 1.5, 2];
@ -260,74 +100,15 @@ function registerWire(w) { if (!registeredWires.includes(w)) registeredWires.pus
});
})();
</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,
)
->renderHook(
PanelsRenderHook::BODY_END,
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-lb-wrap">
<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-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="قبلی">&#8249;</button>
<button id="fi-lb-next" title="بعدی">&#8250;</button>
<div id="fi-lb-counter">1 / 1</div>
@ -345,76 +126,22 @@ function fixGalleryItems() {
var lbPrev = document.getElementById('fi-lb-prev');
var lbNext = document.getElementById('fi-lb-next');
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;
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 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() {
lbImg.style.transform = 'scale(' + zoom + ') translate(' + panX + 'px, ' + panY + 'px)';
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() {
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) {
function showLightbox(imgs, idx) {
images = imgs;
captions = caps || [];
sections = secs || [];
currentIdx = idx;
zoom = 1; panX = 0; panY = 0;
manuallyHidden = false; autoHidden = false;
applyTransform();
updateCounter();
updateCaption();
updateTitle();
loadImage();
lb.classList.add('lb-open');
document.addEventListener('keydown', onKey);
@ -433,8 +160,6 @@ function navigate(dir) {
applyTransform();
loadImage();
updateCounter();
updateCaption();
updateTitle();
}
function loadImage() {
@ -493,44 +218,8 @@ function onKey(e) {
lbWrap.addEventListener('click', function (e) { if (e.target === lbWrap) 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); });
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) {
var thumb = e.target.closest('[data-lb-src]');
@ -541,10 +230,8 @@ function onKey(e) {
? Array.from(document.querySelectorAll('[data-lb-group="' + group + '"]'))
: [thumb];
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);
showLightbox(urls, Math.max(0, idx), caps, secs);
showLightbox(urls, Math.max(0, idx));
});
document.addEventListener('click', function (e) {
@ -561,13 +248,6 @@ function onKey(e) {
var allItems = Array.from(root.querySelectorAll('.filepond--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 alpineEl = root.closest('[x-data]');
if (alpineEl && window.Alpine) {
@ -589,7 +269,7 @@ function mkArrays(urls) {
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) {}
}
@ -604,8 +284,7 @@ function mkArrays(urls) {
if (files) {
var arr = Array.isArray(files) ? files : Object.values(files);
if (arr.length) {
var arr2=arr.map(function(f){return '/storage/'+f;}); var a2=mkArrays(arr2);
showLightbox(arr2, Math.min(clickedIdx, arr2.length-1), a2.caps, a2.secs);
showLightbox(arr.map(function (f) { return '/storage/' + f; }), Math.min(clickedIdx, arr.length - 1));
return;
}
}
@ -623,7 +302,7 @@ function mkArrays(urls) {
} catch (ex) {}
}
});
if (imgs.length) { var a3=mkArrays(imgs); showLightbox(imgs, fbIdx, a3.caps, a3.secs); }
if (imgs.length) showLightbox(imgs, fbIdx);
}, true);
})();
</script>
@ -633,11 +312,7 @@ function mkArrays(urls) {
Action::make('switch_language')
->label(fn () => app()->getLocale() === 'fa' ? 'English' : 'فارسی')
->icon('heroicon-o-language')
->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') . '\'',
]),
->url(fn () => route('locale.switch', app()->getLocale() === 'fa' ? 'en' : 'fa')),
])
->navigationGroups([
NavigationGroup::make('بیماران و ویزیت'),
@ -646,8 +321,6 @@ function mkArrays(urls) {
NavigationGroup::make('تنظیمات'),
])
->sidebarFullyCollapsibleOnDesktop()
->spa()
->unsavedChangesAlerts()
->maxContentWidth(Width::Full)
->viteTheme('resources/css/filament/admin/theme.css')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')

View File

@ -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
{
return $this->peerIp !== '' && $this->token !== '';
@ -80,22 +48,11 @@ public function isFtpConfigured(): bool
public function sync(): array
{
set_time_limit(0);
$report = [];
$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;
$ourLastSync = \App\Models\OsurgInitial::val('our_sync_cursor') ?: null;
$peerCursor = (int) \App\Models\OsurgInitial::val('sync_peer_cursor', 0);
$ourLastSync = (int) \App\Models\OsurgInitial::val('our_sync_cursor', 0);
try {
$pullResponse = Http::timeout(30)
@ -108,36 +65,23 @@ public function sync(): array
if (! empty($peerChanges)) {
app()->instance('sync.applying', true);
$pullReport = [];
DB::transaction(function () use ($peerChanges, &$pullReport) {
DB::transaction(function () use ($peerChanges, &$report) {
foreach ($peerChanges as $change) {
$this->applyChange($change, 'دریافت', $pullReport);
$this->applyChange($change, 'دریافت', $report);
}
});
foreach ($pullReport as $line) {
$report[] = $line;
}
$pullHasErrors = ! empty(array_filter($pullReport, fn ($l) => str_contains((string) $l, '✗')));
if (! $pullHasErrors) {
$datetimes = array_filter(array_column($peerChanges, 'datetime'));
$maxPeerDatetime = \Carbon\Carbon::parse(max($datetimes))->format('Y-m-d H:i:s');
if ($peerCursor === null || $maxPeerDatetime > $peerCursor) {
$this->savePeerCursor($maxPeerDatetime);
}
} else {
$report[] = 'هشدار: peer_cursor پیش نرفت — برخی تغییرات دریافتی اعمال نشد و دفعه بعد retry می‌شه.';
}
}
$maxPeerChangeId = max(array_column($peerChanges, 'id'));
$this->savePeerCursor(max($peerCursor, $maxPeerChangeId));
if ($this->isFtpConfigured()) {
$this->transferFilesViaFtp($peerChanges ?? [], 's2c', $report);
$this->transferFilesViaFtp($peerChanges, 's2c', $report);
} else {
$this->syncFilesViaHttp($peerChanges ?? [], $baseUrl, $report);
$this->syncFilesViaHttp($peerChanges, $baseUrl, $report);
}
}
$report[] = count($peerChanges ?? []) . ' تغییر از سیستم مقابل دریافت شد.';
$report[] = count($peerChanges) . ' تغییر از سیستم مقابل دریافت شد.';
} else {
$report[] = 'خطا در دریافت تغییرات از سیستم مقابل: ' . $pullResponse->status();
}
@ -154,23 +98,14 @@ public function sync(): array
->post("{$baseUrl}/api/sync/apply", ['changes' => $ourChanges]);
if ($pushResponse->successful()) {
$pushReport = $pushResponse->json('report', []);
foreach ($pushReport as $line) {
foreach ($pushResponse->json('report', []) as $line) {
$report[] = 'ارسال: ' . $line;
}
$report[] = count($ourChanges) . ' تغییر به سیستم مقابل ارسال شد.';
$hasErrors = ! empty(array_filter($pushReport, fn ($l) => str_contains((string) $l, '✗')));
if (! empty($ourChanges) && ! $hasErrors) {
$ourDatetimes = array_filter(array_column($ourChanges, 'datetime'));
$maxOurDatetime = \Carbon\Carbon::parse(max($ourDatetimes))->format('Y-m-d H:i:s');
if ($ourLastSync === null || $maxOurDatetime > $ourLastSync) {
$this->preQueueOutgoingFiles($ourChanges);
$this->saveOurCursor($maxOurDatetime);
}
} elseif ($hasErrors) {
$report[] = 'هشدار: cursor پیش نرفت — برخی تغییرات روی سیستم مقابل اعمال نشد و دفعه بعد retry می‌شه.';
if (! empty($ourChanges)) {
$maxOurId = max(array_column($ourChanges, 'id'));
$this->saveOurCursor($maxOurId);
}
if ($this->isFtpConfigured()) {
@ -206,16 +141,11 @@ public function applyChanges(array $changes): array
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);
$pendingPaths = $this->getPendingFiles($side);
$allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths)));
if (empty($allPaths)) {
if (empty($changes)) {
return;
}
$ftp = $this->connectFtp();
if (! $ftp) {
$report[] = 'خطا در اتصال FTP به سیستم مقابل';
@ -223,18 +153,31 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
return;
}
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
$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;
$remoteFile = $remoteBase . '/' . $filePath;
$remoteFolder = dirname($remoteFile);
if ($side === 'c2s') {
if (! file_exists($localFile)) {
$report[] = "فایل [{$filePath}] در سیستم محلی یافت نشد ✗";
$errorMsg = "فایل [{$filePath}] در سیستم محلی یافت نشد";
$report[] = $errorMsg . ' ✗';
Log::warning('FTP upload skipped - local file not found', [
'file' => $filePath,
'local_path' => $localFile,
@ -243,31 +186,40 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
}
if (! $this->createFtpDirectory($ftp, $remoteFolder)) {
$report[] = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد ✗";
Log::error('FTP mkdir failed', ['file' => $filePath, 'remote_folder' => $remoteFolder]);
$failedPaths[] = $filePath;
$errorMsg = "ایجاد پوشه [{$remoteFolder}] برای فایل [{$filePath}] شکست خورد";
$report[] = $errorMsg . ' ✗';
Log::error('FTP mkdir failed', [
'file' => $filePath,
'remote_folder' => $remoteFolder,
]);
continue;
}
if ($this->ftpPut($ftp, $remoteFile, $localFile)) {
if (ftp_put($ftp, $remoteFile, $localFile, FTP_BINARY)) {
$report[] = "فایل [{$filePath}] ارسال شد (FTP) ✓";
Log::info('FTP file uploaded', ['file' => $filePath, 'size' => filesize($localFile)]);
Log::info('FTP file uploaded', [
'file' => $filePath,
'size' => filesize($localFile),
]);
} else {
$errorDetail = error_get_last();
$errorMsg = "فایل [{$filePath}] ارسال نشد (FTP)";
if ($errorDetail) {
$errorMsg .= ' - ' . $errorDetail['message'];
$errorMsg .= " - " . $errorDetail['message'];
}
$report[] = $errorMsg . ' ✗';
$failedPaths[] = $filePath;
Log::error('FTP upload failed', [
'file' => $filePath, 'local_file' => $localFile,
'remote_file' => $remoteFile, 'error' => $errorDetail,
'file' => $filePath,
'local_file' => $localFile,
'remote_file' => $remoteFile,
'error' => $errorDetail,
]);
}
} else {
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;
}
@ -275,72 +227,44 @@ protected function transferFilesViaFtp(array $changes, string $side, array &$rep
if (! is_dir($localFolder)) {
if (! mkdir($localFolder, 0755, true)) {
$report[] = "ایجاد پوشه [{$localFolder}] شکست خورد ✗";
$failedPaths[] = $filePath;
Log::error('Local mkdir failed', ['folder' => $localFolder, 'error' => error_get_last()]);
Log::error('Local mkdir failed', [
'folder' => $localFolder,
'error' => error_get_last(),
]);
continue;
}
}
if ($this->ftpGet($ftp, $localFile, $remoteFile)) {
if (ftp_get($ftp, $localFile, $remoteFile, FTP_BINARY)) {
$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 {
$errorDetail = error_get_last();
$errorMsg = "فایل [{$filePath}] دریافت نشد (FTP)";
if ($errorDetail) {
$errorMsg .= ' - ' . $errorDetail['message'];
$errorMsg .= " - " . $errorDetail['message'];
}
$report[] = $errorMsg . ' ✗';
$failedPaths[] = $filePath;
Log::error('FTP download failed', [
'file' => $filePath, 'local_file' => $localFile,
'remote_file' => $remoteFile, 'error' => $errorDetail,
'file' => $filePath,
'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');
}
protected function ftpGet(mixed $ftp, string $localFile, string $remoteFile): bool
{
$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
private function connectFtp(): mixed
{
if (! function_exists('ftp_connect')) {
Log::error('FTP extension is not available');
@ -377,7 +301,7 @@ protected function connectFtp(): mixed
return $ftp;
}
protected function createFtpDirectory($ftp, string $path): bool
private function createFtpDirectory($ftp, string $path): bool
{
$parts = explode('/', trim($path, '/'));
$currentPath = '';
@ -410,10 +334,10 @@ protected function createFtpDirectory($ftp, string $path): bool
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'];
$paths = [];
foreach ($changes as $change) {
$data = $change['changed_data'] ?? [];
foreach ($fileFields as $field) {
@ -422,109 +346,57 @@ private function extractFilePaths(array $changes): array
}
$files = is_array($data[$field]) ? $data[$field] : json_decode($data[$field], true) ?? [];
foreach ($files as $filePath) {
if ($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}] در سیستم محلی یافت نشد ✗";
if (! $filePath || ! Storage::disk('public')->exists($filePath)) {
continue;
}
$stream = fopen($localPath, 'rb');
try {
$response = Http::timeout(600)
->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]);
if ($response->successful()) {
$report[] = "فایل [{$filePath}] ارسال شد (HTTP) ✓";
} else {
$report[] = "فایل [{$filePath}] ارسال نشد (HTTP) ✗ " . $response->status();
$failedPaths[] = $filePath;
}
} catch (\Throwable $e) {
$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
{
$localBase = rtrim(Storage::disk('public')->path(''), '/\\');
$currentPaths = $this->extractFilePaths($changes);
$pendingPaths = $this->getPendingFiles('s2c');
$allPaths = array_values(array_unique(array_merge($pendingPaths, $currentPaths)));
$failedPaths = [];
$fileFields = ['photos_before', 'photos_after', 'videos', 'audio_files'];
foreach ($allPaths as $filePath) {
if (Storage::disk('public')->exists($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 || Storage::disk('public')->exists($filePath)) {
continue;
}
$localFile = $localBase . '/' . $filePath;
$partFile = $localFile . '.part';
$localDir = dirname($localFile);
try {
if (! is_dir($localDir)) {
mkdir($localDir, 0755, true);
}
if (file_exists($partFile)) {
@unlink($partFile);
}
$response = Http::timeout(60)
->withHeader('X-Sync-Token', $this->token)
->get("{$baseUrl}/api/sync/file", ['path' => $filePath]);
if ($response->successful()) {
file_put_contents($partFile, $response->body());
rename($partFile, $localFile);
Storage::disk('public')->put($filePath, $response->body());
$report[] = "فایل [{$filePath}] دریافت شد (HTTP) ✓";
} else {
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $response->status();
$failedPaths[] = $filePath;
}
} catch (\Throwable $e) {
if (file_exists($partFile)) {
@unlink($partFile);
}
$report[] = "فایل [{$filePath}] دریافت نشد (HTTP) ✗ " . $e->getMessage();
$failedPaths[] = $filePath;
}
}
$this->savePendingFiles('s2c', $failedPaths);
if (! empty($failedPaths)) {
$report[] = count($failedPaths) . ' فایل در صف retry دفعه بعد قرار گرفت.';
}
}
}
@ -539,10 +411,6 @@ private function applyChange(array $change, string $prefix, array &$report): voi
try {
if ($action === 'deleted') {
if ($table === 'patients') {
$this->deletePatientFilesFromDisk((int) $recordId);
$this->removePatientFilesFromPending((int) $recordId);
}
DB::table($table)->where('id', $recordId)->delete();
$report[] = "{$label} حذف ✓";
} elseif ($action === 'created') {
@ -555,9 +423,6 @@ private function applyChange(array $change, string $prefix, array &$report): voi
$report[] = "{$label} ایجاد ✓";
}
} elseif ($action === 'updated') {
if ($table === 'patients') {
$this->deleteRemovedPatientFiles((int) $recordId, $data);
}
DB::table($table)->where('id', $recordId)->update($this->sanitize($data));
$report[] = "{$label} بروزرسانی ✓";
}
@ -566,229 +431,7 @@ 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(int $cursor): void
{
\App\Models\OsurgInitial::updateOrCreate(
['init_parameter' => 'our_sync_cursor'],
@ -797,7 +440,7 @@ private function saveOurCursor(string $cursor): void
cache()->forget('osurg_initial.our_sync_cursor');
}
private function savePeerCursor(string $cursor): void
private function savePeerCursor(int $cursor): void
{
\App\Models\OsurgInitial::updateOrCreate(
['init_parameter' => 'sync_peer_cursor'],

View File

@ -12,8 +12,7 @@
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustHosts(at: fn () => ['.*'], subdomains: false);
$middleware->encryptCookies(except: ['backup_token']);
//
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@ -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')

View File

@ -11,7 +11,6 @@
"require": {
"php": "^8.2",
"ariaieboy/filament-jalali": "2.0",
"awcodes/richer-editor": "^1.2",
"barryvdh/laravel-dompdf": "^3.1",
"bezhansalleh/filament-shield": "^4.1",
"filament/filament": "^4.0",

250
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2016fafdbb8bbad0a40726291171c947",
"content-hash": "ab5f5cc9631f7f46d10ef65adf2fcbda",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@ -217,91 +217,6 @@
],
"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",
"version": "v3.1.1",
@ -3605,95 +3520,6 @@
},
"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",
"version": "1.16.0",
@ -5425,80 +5251,6 @@
},
"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",
"version": "1.9.5",

View File

@ -19,7 +19,6 @@ public function run(): void
TreatmentSeeder::class,
SurgeryCenterSeeder::class,
MedicationSeeder::class,
LabTestSeeder::class,
OsurgInitialSeeder::class,
RolePermissionSeeder::class,
]);

View File

@ -82,17 +82,7 @@ public function run(): void
],
[
'init_parameter' => 'ftp_remote_path',
'init_value' => '/',
'attachment' => null,
],
[
'init_parameter' => 'sync_peer_cursor',
'init_value' => null,
'attachment' => null,
],
[
'init_parameter' => 'our_sync_cursor',
'init_value' => null,
'init_value' => 'C:/wamp64/www/matab-panel/storage/app/public',
'attachment' => null,
],
];

View File

@ -99,9 +99,6 @@
'last_sync' => 'Last Sync',
'pending_changes' => 'Pending Changes',
'peer_address' => 'Peer System Address',
'peer_connected' => 'Connected',
'peer_disconnected' => 'Disconnected',
'peer_checking' => 'Checking...',
'never' => 'Never',
'setup_guide' => 'Setup Guide',
'setup_description' => 'To enable synchronization, set the following values in the .env file on both systems:',
@ -121,10 +118,6 @@
'label' => 'Backup',
'success' => 'Backup completed successfully',
'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' => [
'label' => 'Import from Database',

View File

@ -10,7 +10,6 @@
'illness' => '2. Medical History',
'surgery_notes' => '3. Surgery Notes',
'media' => '4. Patient Media',
'record_info' => 'Record Info',
],
'fields' => [
'icno' => 'National ID',
@ -81,6 +80,5 @@
'last_name_required' => 'Last name is required.',
'father_name_required' => 'Father name is required.',
'hand_phone_required' => 'Mobile number is required.',
'icno_unique' => 'This national ID is already registered for another patient.',
],
];

View File

@ -5,7 +5,7 @@
'treatment_type' => 'Treatment Type',
'treatment_name' => 'Treatment Name',
'descriptions' => 'Descriptions',
'treatment_cost' => 'Cost (Toman)',
'treatment_cost' => 'Cost (Rial)',
'created_at' => 'Created At',
],
];

View File

@ -89,9 +89,6 @@
'last_sync' => 'آخرین همگام‌سازی',
'pending_changes' => 'تغییرات ارسال نشده',
'peer_address' => 'آدرس سیستم مقابل',
'peer_connected' => 'آنلاین',
'peer_disconnected' => 'آفلاین',
'peer_checking' => 'در حال بررسی...',
'never' => 'هرگز',
'setup_guide' => 'راهنمای تنظیم',
'setup_description' => 'برای فعال‌سازی همگام‌سازی، مقادیر زیر را در فایل .env هر دو سیستم تنظیم کنید:',
@ -121,10 +118,6 @@
'label' => 'پشتیبان‌گیری',
'success' => 'پشتیبان‌گیری با موفقیت انجام شد',
'failed' => 'خطا در پشتیبان‌گیری: فایل دیتابیس یافت نشد',
'status_preparing' => 'در حال آماده‌سازی...',
'status_compressing' => 'در حال فشرده‌سازی فایل‌ها...',
'status_ready' => 'فایل پشتیبان آماده دانلود است ✓',
'download_button' => 'دانلود فایل پشتیبان',
],
'import' => [
'label' => 'ورود از دیتابیس',

View File

@ -25,7 +25,6 @@
'illness' => '۲. کدامیک از بیماری‌های زیر را داشته و یا دارید؟',
'surgery_notes' => '۳. توضیحات عمل',
'media' => '۴. تصاویر بیمار',
'record_info' => 'اطلاعات ثبت',
],
'fields' => [
'icno' => 'کد ملی',
@ -83,6 +82,5 @@
'last_name_required' => 'وارد کردن نام خانوادگی الزامی است.',
'father_name_required' => 'وارد کردن نام پدر الزامی است.',
'hand_phone_required' => 'وارد کردن شماره موبایل الزامی است.',
'icno_unique' => 'این کد ملی قبلاً برای بیمار دیگری ثبت شده است.',
],
];

View File

@ -5,7 +5,7 @@
'treatment_type' => 'نوع درمان',
'treatment_name' => 'نام درمان',
'descriptions' => 'توضیحات',
'treatment_cost' => 'هزینه (تومان)',
'treatment_cost' => 'هزینه (ریال)',
'created_at' => 'تاریخ ثبت',
],
];

203
package-lock.json generated
View File

@ -10,7 +10,6 @@
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@tauri-apps/cli": "^2",
"@tiptap/core": "^3.22.4",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
@ -1358,46 +1357,6 @@
"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": {
"version": "1.0.8",
"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_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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2346,147 +2298,6 @@
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -2549,13 +2360,6 @@
"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": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@ -2788,13 +2592,6 @@
"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": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@ -13,7 +13,6 @@
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
"@tauri-apps/cli": "^2",
"@tiptap/core": "^3.22.4",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",

View File

@ -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

View File

@ -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

View File

@ -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

View File

View File

@ -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

View File

@ -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}

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

View File

@ -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());

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -210,36 +210,10 @@ [dir="rtl"] .prose p {
}
.fi-photo-gallery .filepond--root {
contain: layout style !important;
display: flex !important;
flex-direction: column !important;
min-height: 60px !important;
height: auto !important;
margin-top: 8px !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,
[data-hide-dropzone] .filepond--root {
@ -266,12 +240,6 @@ .fi-photo-gallery .filepond--item {
overflow: hidden !important;
border-radius: 6px !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 {
position: absolute !important;
@ -281,18 +249,16 @@ .fi-photo-gallery .filepond--image-preview-wrapper {
height: 100% !important;
max-height: none !important;
margin-top: 0 !important;
transform: none !important;
}
.fi-photo-gallery .filepond--image-preview-overlay,
.fi-photo-gallery .filepond--image-preview {
height: 100% !important;
max-height: none !important;
height: 56px !important;
max-height: 56px !important;
}
.fi-photo-gallery .filepond--image-preview canvas {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block !important;
}
.fi-photo-gallery .filepond--panel-root,
.fi-photo-gallery .filepond--panel-top,
@ -313,38 +279,15 @@ .fi-photo-gallery .filepond--file-action-button {
.fi-photo-gallery .filepond--file-info {
display: none !important;
}
.fi-photo-gallery .filepond--drop-label {
margin-top: 10px !important;
}
.fi-video-gallery .filepond--root {
contain: layout style !important;
display: flex !important;
flex-direction: column !important;
min-height: 60px !important;
height: auto !important;
margin-top: 8px !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 {
position: relative !important;
transform: none !important;
@ -394,6 +337,9 @@ .fi-video-gallery .filepond--file-action-button {
width: 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-revert-item-processing,
@ -452,8 +398,7 @@ #fi-lb-img.lb-zoomed.lb-dragging {
#fi-lb-close {
position: absolute;
top: 1rem;
left: 1rem;
right: auto;
right: 1rem;
z-index: 10;
width: 2.5rem;
height: 2.5rem;
@ -520,139 +465,3 @@ #fi-lb-hint {
pointer-events: none;
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;
}

View File

@ -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'] }
},
},
}
},
})
}

View File

@ -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

View File

@ -1 +0,0 @@
@livewire('patient-media-player', ['patientId' => $patientId, 'field' => $field], key('media-' . $patientId . '-' . $field))

Some files were not shown because too many files have changed in this diff Show More