feat: patient view, media player, and rich editor enhancements

This commit is contained in:
SajjadMahmoody
2026-04-23 21:24:36 +03:30
parent 95592aa510
commit 063b20d26f
101 changed files with 9648 additions and 117 deletions
@@ -0,0 +1,39 @@
<?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);
}
}
@@ -0,0 +1,35 @@
<?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);
}
}
@@ -0,0 +1,65 @@
<?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);
}
}
+322 -38
View File
@@ -20,18 +20,26 @@
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;
@@ -54,9 +62,63 @@ 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'];
return ['first_name', 'last_name', 'hand_phone', 'icno'];
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
@@ -287,9 +349,10 @@ 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'],
['highlight'],
['highlightYellow', 'highlightRed'],
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
['blockquote', 'bulletList', 'orderedList'],
['undo', 'redo'],
@@ -298,9 +361,10 @@ 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'],
['highlight'],
['highlightYellow', 'highlightRed'],
['h2', 'h3', 'alignStart', 'alignCenter', 'alignEnd'],
['blockquote', 'bulletList', 'orderedList'],
['undo', 'redo'],
@@ -316,7 +380,10 @@ public static function form(Schema $schema): Schema
->multiple()
->image()
->panelLayout('grid')
->extraAttributes(['class' => 'fi-photo-gallery'])
->extraAttributes(fn (string $operation) => $operation === 'view'
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
: ['class' => 'fi-photo-gallery']
)
->disk('public')
->directory(function ($record) {
$year = \Morilog\Jalali\Jalalian::now()->getYear();
@@ -325,6 +392,8 @@ 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')
@@ -332,7 +401,10 @@ public static function form(Schema $schema): Schema
->multiple()
->image()
->panelLayout('grid')
->extraAttributes(['class' => 'fi-photo-gallery'])
->extraAttributes(fn (string $operation) => $operation === 'view'
? ['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']
: ['class' => 'fi-photo-gallery']
)
->disk('public')
->directory(function ($record) {
$year = \Morilog\Jalali\Jalalian::now()->getYear();
@@ -341,6 +413,8 @@ 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')
@@ -363,8 +437,14 @@ public static function form(Schema $schema): Schema
])
->maxSize(102400)
->downloadable()
->openable()
->panelLayout('grid')
->extraAttributes(['class' => 'fi-video-gallery'])
->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')
->fetchFileInformation(false),
FileUpload::make('audio_files')
@@ -384,6 +464,13 @@ 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),
])
@@ -421,6 +508,186 @@ 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('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('home_phone')
->label(__('patients.fields.home_phone'))
->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(),
]);
@@ -442,13 +709,19 @@ 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('-'),
->placeholder('-')
->hidden(),
TextColumn::make('creator.name')
->label(__('patients.fields.created_by'))
@@ -475,17 +748,7 @@ public static function table(Table $table): Table
? ['class' => 'pointer-events-none']
: []
)
->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;
})
->formatStateUsing(fn ($state) => static::previewHighlightedHtml($state ?? ''))
->html()
->action(
Action::make('viewSurgeryBefore')
@@ -510,17 +773,7 @@ public static function table(Table $table): Table
? ['class' => 'pointer-events-none']
: []
)
->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;
})
->formatStateUsing(fn ($state) => static::previewHighlightedHtml($state ?? ''))
->html()
->action(
Action::make('viewSergeryAfter')
@@ -554,7 +807,7 @@ public static function table(Table $table): Table
->iconColor(fn ($state) => $state ? 'success' : 'gray')
->placeholder('-'),
])
->defaultSort('updated_at', 'desc')
->defaultSort('created_at', 'desc')
->recordActions([
Action::make('surgeryAppointment')
->label(__('patients.actions.surgery_appointment'))
@@ -849,6 +1102,13 @@ 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(),
]),
@@ -889,6 +1149,13 @@ 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(),
]),
])
@@ -974,14 +1241,18 @@ public static function table(Table $table): Table
$record->audio_files,
]))
)
->modalWidth(Width::ExtraLarge)
->modalWidth(Width::ThreeExtraLarge)
->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),
'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')
@@ -998,7 +1269,12 @@ public static function table(Table $table): Table
->openable()
->disabled()
->fetchFileInformation(false)
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
->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'),
]),
FileUpload::make('photos_after')
->label(__('patients.fields.photos_after'))
@@ -1014,7 +1290,12 @@ public static function table(Table $table): Table
->openable()
->disabled()
->fetchFileInformation(false)
->extraAttributes(['class' => 'fi-photo-gallery fi-photo-gallery-view', 'data-hide-dropzone' => 'true']),
->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'),
]),
FileUpload::make('videos')
->label(__('patients.fields.videos'))
@@ -1037,7 +1318,8 @@ public static function table(Table $table): Table
->downloadable()
->disabled()
->fetchFileInformation(false)
->extraAttributes(['data-hide-dropzone' => 'true']),
->panelLayout('grid')
->extraAttributes(['class' => 'fi-video-gallery', 'data-hide-dropzone' => 'true']),
FileUpload::make('audio_files')
->label(__('patients.fields.audio_files'))
@@ -1076,6 +1358,7 @@ 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)
@@ -1200,6 +1483,7 @@ 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'),
];
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientResource\Pages;
use App\Filament\Resources\PatientResource;
use Filament\Actions\Action;
use Filament\Resources\Pages\ViewRecord;
use Filament\Support\Enums\Width;
class ViewPatient extends ViewRecord
{
protected static string $resource = PatientResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
Action::make('printPrescription')
->label(__('prescriptions.actions.print_prescription'))
->icon('heroicon-o-printer')
->color('info')
->visible(fn () => $this->getRecord()->prescriptions()->exists())
->url(fn () => route('prescription.print', $this->getRecord()->prescriptions()->latest()->first()))
->openUrlInNewTab(),
Action::make('printAdmission')
->label(__('prescriptions.actions.print_admission'))
->icon('heroicon-o-printer')
->color('warning')
->visible(fn () => $this->getRecord()->surgeryAppointment !== null)
->url(fn () => route('surgery-appointment.print.admission', $this->getRecord()))
->openUrlInNewTab(),
Action::make('printLab')
->label(__('prescriptions.actions.print_lab'))
->icon('heroicon-o-beaker')
->color('gray')
->url(fn () => route('patient.print.lab', $this->getRecord()))
->openUrlInNewTab(),
Action::make('edit')
->label(__('filament-actions::edit.single.label'))
->icon('heroicon-o-pencil-square')
->color('primary')
->url(fn () => PatientResource::getUrl('edit', ['record' => $this->getRecord()])),
];
}
}
@@ -6,6 +6,7 @@
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;
@@ -32,7 +33,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]);
return in_array($pageClass, [CreatePatient::class, EditPatient::class, ViewPatient::class]);
}
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
@@ -136,6 +137,18 @@ 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
@@ -219,8 +232,10 @@ public function table(Table $table): Table
->recordActions([
EditAction::make()
->slideOver()
->modalWidth('4xl'),
DeleteAction::make(),
->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])),
])
->toolbarActions([]);
}
@@ -20,6 +20,7 @@
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;
@@ -164,6 +165,13 @@ 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(),
]),
@@ -224,6 +232,13 @@ 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(),
]),
])
+16 -2
View File
@@ -44,6 +44,14 @@ 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 [
@@ -323,8 +331,14 @@ public static function table(Table $table): Table
->columns(2)
->columnSpan(2)
->query(fn ($query, array $data) => $query
->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']))
->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]);
})
)
->indicateUsing(function (array $data): array {
$indicators = [];
@@ -28,21 +28,31 @@ 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 ?? [];
@@ -0,0 +1,58 @@
<?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,
];
}
}
@@ -0,0 +1,50 @@
<?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 [];
}
}
+4 -2
View File
@@ -48,11 +48,13 @@ protected function buildQuery()
$query = Visit::query();
if ($this->fromDate) {
$query->whereDate('visit_date', '>=', $this->fromDate);
$fromDate = \Carbon\Carbon::parse($this->fromDate)->format('Y-m-d');
$query->whereRaw("SUBSTRING(visit_date, 1, 10) >= ?", [$fromDate]);
}
if ($this->toDate) {
$query->whereDate('visit_date', '<=', $this->toDate);
$toDate = \Carbon\Carbon::parse($this->toDate)->format('Y-m-d');
$query->whereRaw("SUBSTRING(visit_date, 1, 10) <= ?", [$toDate]);
}
if ($this->doctor) {