feat: initialize Dr-Panel project with patient file number support

This commit is contained in:
SajjadMahmoody 2026-05-06 02:18:23 +03:30
parent f8f174690a
commit 8b84d1780f
460 changed files with 113960 additions and 1 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

73
.env.example Normal file
View File

@ -0,0 +1,73 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:Cwol1ZZBqRAY2Em+k6lDh8PvD2RkmfSCSR5c3t+ypjg=
APP_DEBUG=false
APP_LOCALE=fa
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=warning
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=file
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# APP_URL auto-detected from request — see AppServiceProvider
SYNC_PEER_IP=192.168.1.8
SYNC_PEER_PORT=8000
SYNC_TOKEN=matab-secret-2026
SMS_API_URL=http://94.101.184.91/api/sms
SMS_API_KEY=surgery-sms-2024

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
*.log
.DS_Store
/scripts/php/
/.claude
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db

View File

@ -1,2 +1,2 @@
# Dr-Panel
# matab-panel

View File

@ -0,0 +1,33 @@
<?php
namespace App\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use Morilog\Jalali\Jalalian;
class JalaliDatetime implements CastsAttributes
{
public function __construct(
private string $format = 'Y/m/d H:i'
) {}
public function get(Model $model, string $key, mixed $value, array $attributes): ?string
{
if (is_null($value)) {
return null;
}
try {
return Jalalian::fromCarbon(Carbon::parse($value))->format($this->format);
} catch (\Throwable $e) {
return $value;
}
}
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
return $value;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Filament\Exports;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Illuminate\Support\Number;
use Spatie\Permission\Models\Role;
class RoleExporter extends Exporter
{
protected static ?string $model = Role::class;
public static function getColumns(): array
{
return [
ExportColumn::make('id')
->label('ID'),
ExportColumn::make('name')
->label(__('filament-shield::filament-shield.column.name')),
ExportColumn::make('guard_name')
->label(__('filament-shield::filament-shield.column.guard_name')),
ExportColumn::make('permissions_count')
->label(__('filament-shield::filament-shield.column.permissions'))
->counts('permissions'),
ExportColumn::make('updated_at')
->label(__('filament-shield::filament-shield.column.updated_at')),
];
}
public static function getCompletedNotificationBody(Export $export): string
{
$body = 'Your role export has completed and ' . Number::format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
if ($failedRowsCount = $export->getFailedRowsCount()) {
$body .= ' ' . Number::format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
}
return $body;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Filament\Exports;
use App\Models\User;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
use Illuminate\Support\Number;
class UserExporter extends Exporter
{
protected static ?string $model = User::class;
public static function getColumns(): array
{
return [
ExportColumn::make('id')
->label('ID'),
ExportColumn::make('name')
->label(__('users.fields.name')),
ExportColumn::make('username')
->label(__('users.fields.username')),
ExportColumn::make('roles.name')
->label(__('users.fields.roles'))
->listAsJson(),
ExportColumn::make('created_at')
->label(__('users.fields.created_at')),
];
}
public static function getCompletedNotificationBody(Export $export): string
{
$body = 'Your user export has completed and ' . Number::format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
if ($failedRowsCount = $export->getFailedRowsCount()) {
$body .= ' ' . Number::format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
}
return $body;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
<?php
namespace App\Filament\Pages\Auth;
use Filament\Auth\Pages\Login as BaseLogin;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Illuminate\Validation\ValidationException;
use SensitiveParameter;
class Login extends BaseLogin
{
public function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('username')
->label('نام کاربری')
->required()
->maxLength(255)
->autocomplete('username')
->autofocus()
->validationMessages([
'required' => 'وارد کردن نام کاربری الزامی است.',
'max' => 'نام کاربری نباید بیشتر از ۲۵۵ کاراکتر باشد.',
]),
$this->getPasswordFormComponent()
->validationMessages([
'required' => 'وارد کردن رمز عبور الزامی است.',
]),
$this->getRememberFormComponent(),
]);
}
protected function getCredentialsFromFormData(#[SensitiveParameter] array $data): array
{
return [
'username' => $data['username'],
'password' => $data['password'],
];
}
protected function throwFailureValidationException(): never
{
throw ValidationException::withMessages([
'data.username' => __('filament-panels::auth/pages/login.messages.failed'),
]);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Filament\Pages;
use Filament\Pages\Dashboard as BaseDashboard;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\HtmlString;
class Dashboard extends BaseDashboard
{
protected string $view = 'filament.pages.dashboard';
public function getTitle(): string|Htmlable
{
return __('navigation.dashboard.title');
}
public function getHeading(): string|Htmlable
{
return new HtmlString('');
}
}

View File

@ -0,0 +1,384 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use Carbon\Carbon;
use Filament\Actions\Action as FilamentAction;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Support\Enums\FontFamily;
use Filament\Support\Enums\FontWeight;
use Filament\Tables;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Http;
use Morilog\Jalali\Jalalian;
class SmsLogsPage extends Page implements HasTable
{
use InteractsWithTable;
protected string $view = 'filament.pages.sms-logs';
protected static ?int $navigationSort = 10;
public static function canAccess(): bool
{
return auth()->user()->can('View:SmsLogsPage');
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-chat-bubble-left-ellipsis';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.settings');
}
public static function getNavigationLabel(): string
{
return __('navigation.sms_logs.label');
}
public function getTitle(): string
{
return __('navigation.sms_logs.title');
}
public array $stats = ['patients' => 0, 'total' => 0, 'sent' => 0, 'pending' => 0, 'failed' => 0];
public function table(Table $table): Table
{
return $table
->records(fn (array $filters, ?string $search, int $page, int $recordsPerPage) =>
$this->fetchRecords($filters, $search, $page, $recordsPerPage)
)
->columns([
Tables\Columns\TextColumn::make('message_type')
->label('نوع پیام')
->getStateUsing(fn (array $record): string => $record['message_type'] ?? '')
->formatStateUsing(fn (string $state): string => match ($state) {
'initial' => 'پیام اولیه',
'reminder_7days' => 'یادآوری ۷ روز',
'reminder_3days' => 'یادآوری ۳ روز',
'reminder_after_registration' => 'یادآوری پس از ثبت',
'day_of_surgery' => 'روز عمل',
default => $state,
})
->badge()
->color(fn (string $state): string => match ($state) {
'initial' => 'info',
'day_of_surgery' => 'success',
'reminder_3days' => 'pink',
default => 'warning',
}),
Tables\Columns\TextColumn::make('status')
->label('وضعیت')
->getStateUsing(fn (array $record): string => $record['status'] ?? '')
->formatStateUsing(fn (string $state): string => match ($state) {
'sent' => 'ارسال شده',
'pending' => 'در انتظار',
'failed' => 'ناموفق',
default => $state,
})
->badge()
->color(fn (string $state): string => match ($state) {
'sent' => 'success',
'pending' => 'warning',
'failed' => 'danger',
default => 'gray',
}),
Tables\Columns\TextColumn::make('scheduled_at')
->label('زمان‌بندی شده')
->getStateUsing(fn (array $record): string => $this->toJalali($record['scheduled_at'] ?? null)),
Tables\Columns\TextColumn::make('sent_at')
->label('زمان ارسال')
->getStateUsing(fn (array $record): string => $this->toJalali($record['sent_at'] ?? null)),
])
->groups([
Group::make('patient_key')
->titlePrefixedWithLabel(false)
->getTitleFromRecordUsing(fn (array $record): string => $record['patient_name'] ?? '-')
->getDescriptionFromRecordUsing(fn (array $record): string =>
($record['phone_number'] ?? '') .
($record['surgery_date']
? ' | تاریخ عمل: ' . $this->toJalali($record['surgery_date'], 'Y/m/d')
: '')
)
->collapsible(),
])
->defaultGroup('patient_key')
->collapsedGroupsByDefault()
->groupingSettingsHidden()
->filters([
Tables\Filters\SelectFilter::make('status')
->label('وضعیت')
->options([
'sent' => 'ارسال شده',
'pending' => 'در انتظار',
'failed' => 'ناموفق',
]),
Tables\Filters\Filter::make('patient_name')
->label('نام بیمار')
->form([
TextInput::make('patient_name')
->label('نام بیمار')
->placeholder('جستجو...'),
])
->indicateUsing(fn (array $data): ?string => filled($data['patient_name'] ?? null)
? 'نام: ' . $data['patient_name']
: null),
Tables\Filters\Filter::make('phone_number')
->label('شماره موبایل')
->form([
TextInput::make('phone_number')
->label('شماره موبایل')
->placeholder('09...'),
])
->indicateUsing(fn (array $data): ?string => filled($data['phone_number'] ?? null)
? 'موبایل: ' . $data['phone_number']
: null),
])
->recordActions([
FilamentAction::make('delete_message')
->label('حذف پیام')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalHeading('حذف پیام')
->modalDescription('آیا از حذف این پیام مطمئن هستید؟')
->modalSubmitActionLabel('بله، حذف شود')
->action(fn (array $record) => $this->deleteSingleMessage((int) $record['id'])),
FilamentAction::make('delete_patient')
->label('حذف همه پیام‌های بیمار')
->icon('heroicon-o-user-minus')
->color('danger')
->outlined()
->requiresConfirmation()
->modalHeading('حذف همه پیام‌های بیمار')
->modalDescription('آیا از حذف تمام پیام‌های این بیمار مطمئن هستید؟')
->modalSubmitActionLabel('بله، حذف شود')
->action(fn (array $record) => $this->deletePatientMessages(
$record['phone_number'],
$record['surgery_date']
)),
])
->paginationPageOptions([5, 10, 25, 50]);
}
protected function fetchRecords(
array $filters,
?string $search,
int $page,
int $recordsPerPage,
): LengthAwarePaginator {
$query = array_filter([
'status' => $filters['status']['value'] ?? null,
'patientName' => $filters['patient_name']['patient_name'] ?? $search,
'phoneNumber' => $filters['phone_number']['phone_number'] ?? null,
'limit' => 500,
]);
try {
$response = Http::timeout(15)
->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key')))
->get(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs-grouped', $query);
if ($response->failed() || ! ($response->json('success') ?? false)) {
\Illuminate\Support\Facades\Log::warning('SmsLogsPage: API returned failure', [
'status' => $response->status(),
'body' => $response->body(),
'api_url' => \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')),
]);
Notification::make()
->title('خطا در دریافت لاگ‌های پیامک')
->body('کد: ' . $response->status() . ' | ' . ($response->json('message') ?? $response->body()))
->danger()
->send();
return new LengthAwarePaginator([], 0, $recordsPerPage, $page);
}
$grouped = $response->json('data') ?? [];
$this->recalcStats($grouped);
$totalPatients = count($grouped);
$patientsOnPage = array_slice($grouped, ($page - 1) * $recordsPerPage, $recordsPerPage);
$rows = [];
foreach ($patientsOnPage as $patient) {
$patientKey = ($patient['phone_number'] ?? '') . '|' . ($patient['surgery_date'] ?? '');
foreach ($patient['messages'] ?? [] as $msg) {
$rows[] = [
'id' => $msg['id'],
'patient_key' => $patientKey,
'patient_name' => $patient['patient_name'] ?? '-',
'phone_number' => $patient['phone_number'] ?? '',
'surgery_date' => $patient['surgery_date'] ?? '',
'message_type' => $msg['message_type'] ?? '',
'status' => $msg['status'] ?? '',
'scheduled_at' => $msg['scheduled_at'] ?? null,
'sent_at' => $msg['sent_at'] ?? null,
];
}
}
return new LengthAwarePaginator(
items: $rows,
total: $totalPatients,
perPage: $recordsPerPage,
currentPage: $page,
);
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('SmsLogsPage: API exception', [
'error' => $e->getMessage(),
'api_url' => \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')),
]);
Notification::make()
->title('خطا در اتصال به سرویس پیامک')
->body($e->getMessage())
->danger()
->send();
return new LengthAwarePaginator([], 0, $recordsPerPage, $page);
}
}
public function deleteSingleMessage(int $id): void
{
try {
$response = Http::timeout(10)
->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key')))
->delete(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs/' . $id);
if ($response->json('success') ?? false) {
Notification::make()->title('پیام حذف شد')->success()->send();
$this->resetTable();
} else {
Notification::make()->title($response->json('message') ?? 'خطا در حذف پیام')->danger()->send();
}
} catch (\Throwable $e) {
Notification::make()->title('خطا: ' . $e->getMessage())->danger()->send();
}
}
public function deletePatientMessages(string $phoneNumber, string $surgeryDate): void
{
try {
$response = Http::timeout(10)
->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key')))
->post(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/sms-logs/delete-patient', [
'phoneNumber' => $phoneNumber,
'surgeryDate' => $surgeryDate,
]);
if ($response->json('success') ?? false) {
Notification::make()->title('همه پیام‌های بیمار حذف شد')->success()->send();
$this->resetTable();
} else {
Notification::make()->title($response->json('message') ?? 'خطا در حذف پیام‌ها')->danger()->send();
}
} catch (\Throwable $e) {
Notification::make()->title('خطا: ' . $e->getMessage())->danger()->send();
}
}
public static function cancelAppointmentMessages(string $phoneNumber, string $localSurgeryDate): void
{
try {
$apiKey = \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'));
$apiUrl = \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url'));
$localDate = \Carbon\Carbon::parse($localSurgeryDate)->format('Y-m-d');
$grouped = Http::timeout(10)
->withHeader('X-API-Key', $apiKey)
->get($apiUrl . '/sms-logs-grouped', [
'phoneNumber' => $phoneNumber,
'limit' => 200,
])
->json('data') ?? [];
$serverSurgeryDate = null;
foreach ($grouped as $group) {
if (($group['phone_number'] ?? '') !== $phoneNumber) {
continue;
}
$groupDate = $group['surgery_date'] ?? '';
if (! $groupDate) {
continue;
}
try {
if (\Carbon\Carbon::parse($groupDate)->format('Y-m-d') === $localDate) {
$serverSurgeryDate = $groupDate;
break;
}
} catch (\Throwable) {}
}
if (! $serverSurgeryDate) {
return;
}
Http::timeout(10)
->withHeader('X-API-Key', $apiKey)
->asJson()
->post($apiUrl . '/sms-logs/delete-patient', [
'phoneNumber' => $phoneNumber,
'surgeryDate' => $serverSurgeryDate,
]);
} catch (\Throwable) {
}
}
public function toJalali(?string $dateStr, string $format = 'Y/m/d H:i'): string
{
if (! $dateStr) {
return '-';
}
try {
return Jalalian::fromCarbon(
Carbon::parse($dateStr, 'UTC')->setTimezone('Asia/Tehran')
)->format($format);
} catch (\Throwable) {
return $dateStr;
}
}
private function recalcStats(array $grouped): void
{
$total = $sent = $pending = $failed = 0;
foreach ($grouped as $p) {
$total += $p['stats']['total'] ?? 0;
$sent += $p['stats']['sent'] ?? 0;
$pending += $p['stats']['pending'] ?? 0;
$failed += $p['stats']['failed'] ?? 0;
}
$this->stats = [
'patients' => count($grouped),
'total' => $total,
'sent' => $sent,
'pending' => $pending,
'failed' => $failed,
];
}
}

View File

@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Models\OsurgInitial;
use App\Models\SyncLog;
use App\Services\OsurgImportService;
use App\Services\SyncService;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Livewire\WithFileUploads;
use Morilog\Jalali\Jalalian;
class SyncPage extends Page
{
use WithFileUploads;
protected string $view = 'filament.pages.sync-page';
protected static ?int $navigationSort = 99;
public static function canAccess(): bool
{
return auth()->user()->can('View:SyncPage');
}
public static function shouldRegisterNavigation(): bool
{
return false;
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-arrows-right-left';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.system');
}
public string $report = '';
public string $importReport = '';
public bool $importing = false;
public $sqlFile = null;
public static function getNavigationLabel(): string
{
return __('navigation.sync.label');
}
public function getTitle(): string
{
return __('navigation.sync.title');
}
public function getPendingCount(): int
{
return SyncLog::changesSince(OsurgInitial::val('our_sync_cursor') ?: null)->count();
}
public function getLastSyncTime(): string
{
$last = SyncLog::where('action', 'synced')->latest('id')->first();
if (! $last) {
return __('navigation.sync.never');
}
return Jalalian::fromCarbon($last->datetime->timezone('Asia/Tehran'))->format('Y/m/d H:i:s');
}
protected function rules(): array
{
return [
'sqlFile' => ['required', 'file', 'max:307200'],
];
}
protected function getHeaderActions(): array
{
return [
Action::make('sync')
->label(__('navigation.sync.sync_with_peer'))
->icon('heroicon-o-arrows-right-left')
->color('primary')
->requiresConfirmation()
->modalHeading(__('navigation.sync.confirm_heading'))
->modalDescription(__('navigation.sync.confirm_description'))
->modalSubmitActionLabel(__('navigation.sync.confirm_button'))
->action('runSync'),
Action::make('backup')
->label(__('navigation.backup.label'))
->icon('heroicon-o-arrow-down-tray')
->color('success')
->action('downloadBackup'),
];
}
public function runSync(): void
{
$service = SyncService::fromConfig();
if (! $service->isConfigured()) {
Notification::make()
->title(__('navigation.sync.error_title'))
->body(__('navigation.sync.error_not_configured'))
->danger()
->send();
return;
}
$result = $service->sync();
$this->report = implode("\n", $result['report']);
Notification::make()
->title($result['success'] ? __('navigation.sync.success') : __('navigation.sync.failed'))
->body(__('navigation.sync.see_report'))
->color($result['success'] ? 'success' : 'warning')
->send();
}
public function startImport(): void
{
$this->validate();
$this->importing = true;
try {
@set_time_limit(600);
$tmpPath = $this->sqlFile->getRealPath();
$service = new OsurgImportService();
$result = $service->import($tmpPath);
$this->importReport = implode("\n", $result['report']);
Notification::make()
->title($result['success']
? __('navigation.import.success')
: __('navigation.import.failed'))
->body(__('navigation.sync.see_report'))
->color($result['success'] ? 'success' : 'danger')
->send();
} finally {
$this->sqlFile = null;
$this->importing = false;
}
}
public function downloadBackup(): \Symfony\Component\HttpFoundation\StreamedResponse
{
$data = [
'exported_at' => now()->toDateTimeString(),
'last_sync_id' => SyncLog::lastSyncId(),
'changes' => SyncLog::all()->toArray(),
];
$filename = 'matab-backup-' . now()->format('Y-m-d_H-i-s') . '.json';
return response()->streamDownload(function () use ($data) {
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
}, $filename, ['Content-Type' => 'application/json']);
}
}

View File

@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\DoctorResource\Pages;
use App\Models\Doctor;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class DoctorResource extends Resource
{
protected static ?string $model = Doctor::class;
protected static ?string $recordTitleAttribute = 'first_name';
public static function getGloballySearchableAttributes(): array
{
return ['first_name', 'last_name', 'speciality'];
}
public static function getGlobalSearchResultDetails(\Illuminate\Database\Eloquent\Model $record): array
{
return [
__('navigation.doctors.last_name') => $record->last_name,
];
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-user-circle';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.clinic');
}
public static function getNavigationSort(): ?int
{
return 1;
}
public static function getNavigationLabel(): string
{
return __('navigation.doctors.title');
}
public static function getModelLabel(): string
{
return __('navigation.doctors.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.doctors.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('first_name')
->label(__('doctors.fields.first_name'))
->required()
->maxLength(50),
TextInput::make('last_name')
->label(__('doctors.fields.last_name'))
->required()
->maxLength(50),
TextInput::make('speciality')
->label(__('doctors.fields.speciality'))
->maxLength(100),
TextInput::make('license_id')
->label(__('doctors.fields.license_id'))
->maxLength(50),
Toggle::make('is_default')
->label(__('doctors.fields.is_default')),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('first_name')
->label(__('doctors.fields.first_name'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('last_name')
->label(__('doctors.fields.last_name'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('speciality')
->label(__('doctors.fields.speciality'))
->searchable()
->sortable(),
TextColumn::make('license_id')
->label(__('doctors.fields.license_id'))
->badge()
->color('primary'),
IconColumn::make('is_default')
->label(__('doctors.fields.is_default'))
->boolean(),
TextColumn::make('created_at')
->label(__('doctors.fields.created_at'))
->sortable(),
])
->defaultSort('last_name')
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('doctors.fields.first_name'),
__('doctors.fields.last_name'),
__('doctors.fields.speciality'),
__('doctors.fields.license_id'),
__('doctors.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->first_name,
$record->last_name,
$record->speciality,
$record->license_id,
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'doctors-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = Doctor::all();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('doctors.fields.first_name'),
__('doctors.fields.last_name'),
__('doctors.fields.speciality'),
__('doctors.fields.license_id'),
__('doctors.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->first_name,
$record->last_name,
$record->speciality,
$record->license_id,
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'doctors-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = Doctor::all();
return PdfService::download(
'exports.doctors-pdf',
['records' => $records],
'doctors-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListDoctors::route('/'),
'create' => Pages\CreateDoctor::route('/create'),
'edit' => Pages\EditDoctor::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\DoctorResource\Pages;
use App\Filament\Resources\DoctorResource;
use Filament\Resources\Pages\CreateRecord;
class CreateDoctor extends CreateRecord
{
protected static string $resource = DoctorResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\DoctorResource\Pages;
use App\Filament\Resources\DoctorResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditDoctor extends EditRecord
{
protected static string $resource = DoctorResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\DoctorResource\Pages;
use App\Filament\Resources\DoctorResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListDoctors extends ListRecords
{
protected static string $resource = DoctorResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\LabTestResource\Pages;
use App\Models\LabTest;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class LabTestResource extends Resource
{
protected static ?string $model = LabTest::class;
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-beaker';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.diseases');
}
public static function getNavigationSort(): ?int
{
return 4;
}
public static function getNavigationLabel(): string
{
return __('navigation.lab_tests.title');
}
public static function getModelLabel(): string
{
return __('navigation.lab_tests.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.lab_tests.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('lab_tests.sections.info'))
->schema([
TextInput::make('name')
->label(__('lab_tests.fields.name'))
->required()
->maxLength(255),
Textarea::make('notes')
->label(__('lab_tests.fields.notes'))
->rows(2)
->columnSpanFull(),
Toggle::make('is_default')
->label(__('lab_tests.fields.is_default')),
Toggle::make('is_active')
->label(__('lab_tests.fields.is_active'))
->default(true),
])
->columns(3)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('lab_tests.fields.name'))
->searchable()
->sortable(),
IconColumn::make('is_default')
->label(__('lab_tests.fields.is_default'))
->boolean(),
IconColumn::make('is_active')
->label(__('lab_tests.fields.is_active'))
->boolean(),
TextColumn::make('created_at')
->label(__('lab_tests.fields.created_at'))
->sortable(),
])
->defaultSort('name')
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
\Filament\Actions\BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListLabTests::route('/'),
'create' => Pages\CreateLabTest::route('/create'),
'edit' => Pages\EditLabTest::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\LabTestResource\Pages;
use App\Filament\Resources\LabTestResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreateLabTest extends CreateRecord
{
protected static string $resource = LabTestResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\LabTestResource\Pages;
use App\Filament\Resources\LabTestResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
class EditLabTest extends EditRecord
{
protected static string $resource = LabTestResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\LabTestResource\Pages;
use App\Filament\Resources\LabTestResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\Width;
class ListLabTests extends ListRecords
{
protected static string $resource = LabTestResource::class;
protected Width|string|null $maxContentWidth = Width::Full;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\MedicationResource\Pages;
use App\Models\Medication;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class MedicationResource extends Resource
{
protected static ?string $model = Medication::class;
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-beaker';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.diseases');
}
public static function getNavigationSort(): ?int
{
return 3;
}
public static function getNavigationLabel(): string
{
return __('navigation.medications.title');
}
public static function getModelLabel(): string
{
return __('navigation.medications.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.medications.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('medications.sections.info'))
->schema([
TextInput::make('name')
->label(__('medications.fields.name'))
->required()
->maxLength(255),
TextInput::make('dosage_form')
->label(__('medications.fields.dosage_form'))
->maxLength(50)
->placeholder('Tab, Cap, Amp, Drop, Serum, Cream ...'),
TextInput::make('strength')
->label(__('medications.fields.strength'))
->maxLength(100)
->placeholder('500mg, 5mg/5ml ...'),
TextInput::make('quantity')
->label(__('medications.fields.quantity'))
->numeric()
->minValue(0),
TextInput::make('timing')
->label(__('medications.fields.timing'))
->maxLength(100)
->placeholder('q8h, q12h, q24h ...'),
Textarea::make('notes')
->label(__('medications.fields.notes'))
->rows(2)
->columnSpanFull(),
Toggle::make('is_default')
->label(__('medications.fields.is_default')),
Toggle::make('is_active')
->label(__('medications.fields.is_active'))
->default(true),
])
->columns(3)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('medications.fields.name'))
->searchable()
->sortable(),
TextColumn::make('dosage_form')
->label(__('medications.fields.dosage_form'))
->badge()
->placeholder('-'),
TextColumn::make('strength')
->label(__('medications.fields.strength'))
->placeholder('-'),
TextColumn::make('quantity')
->label(__('medications.fields.quantity'))
->placeholder('-'),
TextColumn::make('timing')
->label(__('medications.fields.timing'))
->placeholder('-'),
IconColumn::make('is_default')
->label(__('medications.fields.is_default'))
->boolean(),
IconColumn::make('is_active')
->label(__('medications.fields.is_active'))
->boolean(),
TextColumn::make('created_at')
->label(__('medications.fields.created_at'))
->sortable(),
])
->defaultSort('name')
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
\Filament\Actions\BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListMedications::route('/'),
'create' => Pages\CreateMedication::route('/create'),
'edit' => Pages\EditMedication::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\MedicationResource\Pages;
use App\Filament\Resources\MedicationResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreateMedication extends CreateRecord
{
protected static string $resource = MedicationResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\MedicationResource\Pages;
use App\Filament\Resources\MedicationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
class EditMedication extends EditRecord
{
protected static string $resource = MedicationResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\MedicationResource\Pages;
use App\Filament\Resources\MedicationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\Width;
class ListMedications extends ListRecords
{
protected static string $resource = MedicationResource::class;
protected Width|string|null $maxContentWidth = Width::Full;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\OsurgAuditResource\Pages;
use App\Models\SyncLog;
use Filament\Resources\Resource;
use Morilog\Jalali\Jalalian;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class OsurgAuditResource extends Resource
{
protected static ?string $model = SyncLog::class;
protected static bool $isGloballySearchable = false;
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-clipboard-document-list';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.settings');
}
public static function getNavigationSort(): ?int
{
return 20;
}
public static function getNavigationLabel(): string
{
return __('navigation.osurg_audit.title');
}
public static function getModelLabel(): string
{
return __('navigation.osurg_audit.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.osurg_audit.title');
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('id')
->label(__('sync_log.fields.id'))
->sortable(),
TextColumn::make('datetime')
->label(__('sync_log.fields.datetime'))
->sortable()
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
}),
TextColumn::make('ip')
->label(__('sync_log.fields.ip'))
->searchable(),
TextColumn::make('user')
->label(__('sync_log.fields.user'))
->searchable(),
TextColumn::make('table_name')
->label(__('sync_log.fields.table_name'))
->searchable(),
TextColumn::make('action')
->label(__('sync_log.fields.action'))
->badge()
->searchable()
->color(fn (string $state): string => match ($state) {
'created' => 'success',
'updated' => 'warning',
'deleted' => 'danger',
'synced' => 'gray',
default => 'primary',
}),
TextColumn::make('record_id')
->label(__('sync_log.fields.record_id')),
])
->defaultSort('id', 'desc');
}
public static function getPages(): array
{
return [
'index' => Pages\ListOsurgAudits::route('/'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OsurgAuditResource\Pages;
use App\Filament\Resources\OsurgAuditResource;
use Filament\Resources\Pages\ListRecords;
class ListOsurgAudits extends ListRecords
{
protected static string $resource = OsurgAuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\OsurgInitialResource\Pages;
use App\Models\OsurgInitial;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class OsurgInitialResource extends Resource
{
protected static ?string $model = OsurgInitial::class;
protected static ?string $recordTitleAttribute = 'init_parameter';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-adjustments-horizontal';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.settings');
}
public static function getNavigationSort(): ?int
{
return 1;
}
public static function getNavigationLabel(): string
{
return __('navigation.osurg_initials.title');
}
public static function getModelLabel(): string
{
return __('navigation.osurg_initials.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.osurg_initials.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('init_parameter')
->label(__('osurg_initials.fields.init_parameter'))
->required()
->maxLength(50)
->unique(table: OsurgInitial::class, column: 'init_parameter', ignoreRecord: true),
TextInput::make('init_value')
->label(__('osurg_initials.fields.init_value'))
->maxLength(50),
FileUpload::make('attachment')
->label(__('osurg_initials.fields.attachment'))
->disk('public')
->directory('initials')
->image()
->imagePreviewHeight('120')
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('init_parameter')
->label(__('osurg_initials.fields.init_parameter'))
->searchable()
->sortable(),
TextColumn::make('init_value')
->label(__('osurg_initials.fields.init_value'))
->searchable()
->placeholder('—'),
ImageColumn::make('attachment')
->label(__('osurg_initials.fields.attachment'))
->disk('public')
->square()
->size(48),
TextColumn::make('created_at')
->label(__('osurg_initials.fields.created_at'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('osurg_initials.fields.init_parameter'),
__('osurg_initials.fields.init_value'),
__('osurg_initials.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->init_parameter,
$record->init_value,
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'osurg-initials-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = OsurgInitial::all();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('osurg_initials.fields.init_parameter'),
__('osurg_initials.fields.init_value'),
__('osurg_initials.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->init_parameter,
$record->init_value,
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'osurg-initials-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = OsurgInitial::all();
return PdfService::download(
'exports.osurg-initials-pdf',
['records' => $records],
'osurg-initials-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListOsurgInitials::route('/'),
'create' => Pages\CreateOsurgInitial::route('/create'),
'edit' => Pages\EditOsurgInitial::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OsurgInitialResource\Pages;
use App\Filament\Resources\OsurgInitialResource;
use Filament\Resources\Pages\CreateRecord;
class CreateOsurgInitial extends CreateRecord
{
protected static string $resource = OsurgInitialResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OsurgInitialResource\Pages;
use App\Filament\Resources\OsurgInitialResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditOsurgInitial extends EditRecord
{
protected static string $resource = OsurgInitialResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\OsurgInitialResource\Pages;
use App\Filament\Resources\OsurgInitialResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListOsurgInitials extends ListRecords
{
protected static string $resource = OsurgInitialResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\PatientIllnessResource\Pages;
use App\Models\PatientIllness;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class PatientIllnessResource extends Resource
{
protected static ?string $model = PatientIllness::class;
protected static ?string $recordTitleAttribute = 'illness';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-heart';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.diseases');
}
public static function getNavigationSort(): ?int
{
return 1;
}
public static function getNavigationLabel(): string
{
return __('navigation.patient_illness.title');
}
public static function getModelLabel(): string
{
return __('navigation.patient_illness.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.patient_illness.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('illness')
->label(__('patient_illness.fields.illness'))
->required()
->maxLength(200)
->columnSpanFull(),
Select::make('row_no')
->label(__('patient_illness.fields.row_no'))
->options([
1 => __('patient_illness.row_options.first'),
2 => __('patient_illness.row_options.second'),
])
->required(),
TextInput::make('priority')
->label(__('patient_illness.fields.priority'))
->numeric()
->minValue(1),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('illness')
->label(__('patient_illness.fields.illness'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('row_no')
->label(__('patient_illness.fields.row_no'))
->badge()
->color('gray')
->formatStateUsing(fn (int $state): string => $state === 1
? __('patient_illness.row_options.first')
: __('patient_illness.row_options.second'))
->sortable(),
TextColumn::make('priority')
->label(__('patient_illness.fields.priority'))
->badge()
->color('primary')
->sortable(),
])
->reorderable('priority')
->defaultSort('priority')
->groups([
Group::make('row_no')
->label(__('patient_illness.fields.row_no'))
->getTitleFromRecordUsing(fn (PatientIllness $record): string => $record->row_no === 1
? __('patient_illness.row_options.first')
: __('patient_illness.row_options.second')),
])
->defaultGroup('row_no')
->groupingSettingsHidden()
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('patient_illness.fields.illness'),
__('patient_illness.fields.row_no'),
__('patient_illness.fields.priority'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->illness,
$record->row_no,
$record->priority,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'patient-illness-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = PatientIllness::orderBy('row_no')->orderBy('priority')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('patient_illness.fields.illness'),
__('patient_illness.fields.row_no'),
__('patient_illness.fields.priority'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->illness,
$record->row_no,
$record->priority,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'patient-illness-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = PatientIllness::orderBy('row_no')->orderBy('priority')->get();
return PdfService::download(
'exports.patient-illness-pdf',
['records' => $records],
'patient-illness-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPatientIllnesses::route('/'),
'create' => Pages\CreatePatientIllness::route('/create'),
'edit' => Pages\EditPatientIllness::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientIllnessResource\Pages;
use App\Filament\Resources\PatientIllnessResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePatientIllness extends CreateRecord
{
protected static string $resource = PatientIllnessResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientIllnessResource\Pages;
use App\Filament\Resources\PatientIllnessResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditPatientIllness extends EditRecord
{
protected static string $resource = PatientIllnessResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientIllnessResource\Pages;
use App\Filament\Resources\PatientIllnessResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Facades\Cache;
class ListPatientIllnesses extends ListRecords
{
protected static string $resource = PatientIllnessResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
public function reorderTable(array $order, string|int|null $draggedRecordKey = null): void
{
parent::reorderTable($order, $draggedRecordKey);
Cache::forget('illness_options_1');
Cache::forget('illness_options_2');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientResource\Pages;
use App\Filament\Resources\PatientResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreatePatient extends CreateRecord
{
protected static string $resource = PatientResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientResource\Pages;
use App\Filament\Resources\PatientResource;
use App\Models\SurgeryAppointment;
use App\Models\SurgeryCenter;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Forms\Components\DatePicker;
use Filament\Schemas\Components\Grid;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TimePicker;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
use Illuminate\Support\Facades\Http;
use Morilog\Jalali\Jalalian;
class EditPatient extends EditRecord
{
protected static string $resource = PatientResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
$this->getSaveFormAction()
->formId('form'),
Action::make('surgeryAppointment')
->label(__('patients.actions.surgery_appointment'))
->icon('heroicon-o-calendar-days')
->color(fn () => $this->getRecord()->surgeryAppointment ? 'success' : 'info')
->modalWidth(Width::Large)
->fillForm(function (): array {
$appointment = $this->getRecord()->surgeryAppointment;
if ($appointment) {
$surgeryDate = $appointment->surgery_date;
return [
'surgery_date' => $surgeryDate?->format('Y-m-d'),
'surgery_time' => $surgeryDate?->format('H:i'),
'surgery_center_id' => $appointment->surgery_center_id,
];
}
return [
'surgery_date' => null,
'surgery_time' => '09:00',
'surgery_center_id' => null,
];
})
->form([
Placeholder::make('patient_name')
->label(__('patients.fields.full_name'))
->content(fn () => $this->getRecord()->full_name),
Grid::make(2)
->schema([
DatePicker::make('surgery_date')
->label(__('patients.fields.surgery_date'))
->jalali()
->minDate(today())
->required(),
TimePicker::make('surgery_time')
->label(__('patients.fields.surgery_time'))
->native(false)
->seconds(false)
->minutesStep(15),
]),
Select::make('surgery_center_id')
->label(__('patients.fields.surgery_center'))
->options(fn () => SurgeryCenter::pluck('name', 'id')->toArray())
->searchable()
->required(),
])
->modalSubmitAction(false)
->extraModalFooterActions(fn (Action $action): array => [
$action->makeModalSubmitAction('saveAppointment', arguments: ['save' => true])
->label(__('patients.actions.save_appointment'))
->color('primary')
->icon('heroicon-o-check')
->visible(fn () => $this->getRecord()->surgeryAppointment === null),
Action::make('printAdmissionFromModal')
->label(__('patients.actions.print_admission'))
->color('warning')
->icon('heroicon-o-printer')
->url(fn () => route('surgery-appointment.print.admission', $this->getRecord()), shouldOpenInNewTab: true)
->visible(fn () => $this->getRecord()->surgeryAppointment !== null),
$action->makeModalSubmitAction('deleteAppointment', arguments: ['delete' => true])
->label(__('patients.actions.delete_appointment'))
->color('danger')
->icon('heroicon-o-trash')
->visible(fn () => $this->getRecord()->surgeryAppointment !== null),
])
->action(function (array $data, array $arguments): void {
$record = $this->getRecord();
if (! empty($arguments['delete'])) {
$existingAppointment = $record->surgeryAppointment;
$existingAppointment?->delete();
$record->unsetRelation('surgeryAppointment');
if ($existingAppointment) {
\App\Filament\Pages\SmsLogsPage::cancelAppointmentMessages(
$record->hand_phone ?? '',
$existingAppointment->surgery_date->toDateTimeString(),
);
}
\Filament\Notifications\Notification::make()
->title(__('patients.actions.appointment_deleted'))
->success()
->send();
return;
}
$dateStr = $data['surgery_date'];
$timeStr = $data['surgery_time'] ?? '00:00';
$surgeryDateTime = Carbon::parse($dateStr . ' ' . $timeStr);
$jalaliDate = Jalalian::fromCarbon($surgeryDateTime)->format('Y/m/d');
$apiKey = \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'));
$apiUrl = \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url'));
$smsPayload = [
'phoneNumber' => $record->hand_phone ?? '',
'patientName' => $record->full_name,
'surgeryDate' => $jalaliDate,
'patientId' => $record->id,
'surgeryDateFull' => $surgeryDateTime->toDateTimeString(),
];
// مرحله ۱: ارسال پیامک اولیه
try {
$response = Http::timeout(30)
->withHeader('X-API-Key', $apiKey)
->asJson()
->post($apiUrl . '/surgery-reminder', $smsPayload);
if (! ($response->json('success') ?? false)) {
\Filament\Notifications\Notification::make()
->title(__('patients.actions.api_failed'))
->body($response->json('message') ?? '')
->danger()
->send();
return;
}
} catch (\Throwable $e) {
\Filament\Notifications\Notification::make()
->title(__('patients.actions.api_failed'))
->body($e->getMessage())
->danger()
->send();
return;
}
$appointment = SurgeryAppointment::updateOrCreate(
['patient_id' => $record->id],
[
'surgery_date' => $surgeryDateTime,
'surgery_center_id' => $data['surgery_center_id'],
]
);
$record->unsetRelation('surgeryAppointment');
try {
Http::timeout(30)
->withHeader('X-API-Key', $apiKey)
->asJson()
->post($apiUrl . '/surgery-reminder', array_merge($smsPayload, [
'appointmentId' => $appointment->id,
]));
} catch (\Throwable) {
}
\Filament\Notifications\Notification::make()
->title(__('patients.actions.appointment_saved'))
->body(__('patients.actions.sms_sent'))
->success()
->send();
}),
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(),
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
foreach (['birth_date'] as $field) {
if (($data[$field] ?? null) === '') {
$data[$field] = null;
}
}
foreach (['surgery_before', 'sergery_after'] as $field) {
if (($data[$field] ?? null) === '') {
$data[$field] = null;
}
}
foreach (['current_illness_1', 'current_illness_2', 'has_alergyto'] as $field) {
if (is_null($data[$field] ?? null)) {
$data[$field] = [];
}
}
foreach (['photos_before', 'photos_after', 'videos', 'audio_files'] as $field) {
$data[$field] = static::normalizeFileList($data[$field] ?? null);
}
return $data;
}
public static function normalizeFileList(mixed $value): array
{
if (empty($value)) {
return [];
}
if (is_string($value)) {
$decoded = json_decode($value, true);
$value = is_array($decoded) ? $decoded : [$value];
}
return collect((array) $value)
->map(function ($item) {
if (is_array($item) && isset($item['name'])) {
return $item['name'];
}
if (is_string($item) && $item !== '') {
return $item;
}
return null;
})
->filter()
->values()
->toArray();
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientResource\Pages;
use App\Filament\Resources\PatientResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\Width;
class ListPatients extends ListRecords
{
protected static string $resource = PatientResource::class;
protected Width|string|null $maxContentWidth = Width::Full;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

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

View File

@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PatientResource\RelationManagers;
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;
use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\TimePicker;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class VisitsRelationManager extends RelationManager
{
protected static string $relationship = 'visits';
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
{
return in_array($pageClass, [CreatePatient::class, EditPatient::class, ViewPatient::class]);
}
public static function getTitle(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): string
{
return __('navigation.visits.title');
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('visits.sections.visit_info'))
->schema([
DatePicker::make('visit_date')
->label(__('visits.fields.visit_date'))
->jalali()
->default(now())
->required(),
TimePicker::make('visit_time')
->label(__('visits.fields.visit_time'))
->native(false)
->seconds(false)
->afterStateHydrated(function ($component, $state) {
if (blank($state)) {
$component->state(now()->format('Y-m-d H:i:s'));
}
}),
Select::make('doctor')
->label(__('visits.fields.doctor'))
->options(fn () => Doctor::all()->mapWithKeys(
fn ($d) => [$d->id => $d->full_name]
)->toArray())
->default(fn () => Doctor::where('is_default', true)->first()?->id)
->nullable(),
Select::make('treatment')
->label(__('visits.fields.treatment'))
->options(fn () => Treatment::all()->mapWithKeys(
fn ($t) => [$t->id => ($t->descriptions ? '★ ' : '') . ($t->treatment_name ?: $t->treatment_type)]
)->toArray())
->searchable()
->required()
->live()
->afterStateUpdated(function ($state, callable $set) {
if ($state) {
$treatment = Treatment::find($state);
if ($treatment) {
if ($treatment->treatment_cost > 0) {
$set('treatment_cost', $treatment->treatment_cost);
$set('paid_amount', $treatment->treatment_cost);
}
if ($treatment->descriptions) {
$set('treatment_description', $treatment->descriptions);
}
}
}
}),
])
->columns(2)
->columnSpanFull(),
Section::make(__('visits.sections.financial_info'))
->schema([
TextInput::make('treatment_cost')
->label(__('visits.fields.treatment_cost'))
->numeric()
->nullable()
->suffix(__('visits.fields.currency')),
TextInput::make('paid_amount')
->label(__('visits.fields.paid_amount'))
->numeric()
->nullable()
->suffix(__('visits.fields.currency')),
Select::make('payment_type')
->label(__('visits.fields.payment_type'))
->options(fn () => PaymentType::all()->mapWithKeys(
fn ($p) => [$p->id => $p->payment_type]
)->toArray())
->searchable()
->required(),
])
->columns(3)
->columnSpanFull(),
Section::make(__('visits.sections.treatment_details'))
->schema([
Textarea::make('treatment_description')
->label(__('visits.fields.treatment_description'))
->rows(3)
->columnSpanFull(),
])
->columnSpanFull(),
])
->columnSpanFull(),
]);
}
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
->recordTitleAttribute('visit_date')
->modifyQueryUsing(fn ($query) => $query->with(['doctorRecord', 'treatmentRecord', 'paymentTypeRecord']))
->deferLoading()
->columns([
TextColumn::make('visit_date')
->label(__('visits.fields.visit_date'))
->sortable()
->wrap(false)
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d');
} catch (\Exception $e) {
return $state;
}
})
->placeholder('-'),
TextColumn::make('doctorRecord.first_name')
->label(__('visits.fields.doctor'))
->formatStateUsing(fn ($record) => $record->doctorRecord?->full_name ?? '-')
->wrap(false)
->placeholder('-'),
TextColumn::make('treatmentRecord.treatment_name')
->label(__('visits.fields.treatment'))
->formatStateUsing(fn ($record) => $record->treatmentRecord
? ($record->treatmentRecord->treatment_name ?: $record->treatmentRecord->treatment_type)
: '-')
->wrap(false)
->placeholder('-'),
TextColumn::make('treatment_cost')
->label(__('visits.fields.treatment_cost'))
->formatStateUsing(fn ($state) => $state !== null ? number_format((int) $state) : '-')
->wrap(false)
->placeholder('-')
->summarize(
Sum::make()
->label(__('visits.fields.treatment_cost'))
->formatStateUsing(fn ($state) => number_format((int) ($state ?? 0)))
),
TextColumn::make('paid_amount')
->label(__('visits.fields.paid_amount'))
->formatStateUsing(fn ($state) => $state !== null ? number_format((int) $state) : '-')
->wrap(false)
->placeholder('-')
->summarize(
Sum::make()
->label(__('visits.fields.paid_amount'))
->formatStateUsing(fn ($state) => number_format((int) ($state ?? 0)))
),
TextColumn::make('remained_amount')
->label(__('visits.fields.remained_amount'))
->state(fn ($record) => $record->remained_amount)
->formatStateUsing(fn ($state) => number_format((int) $state))
->color(fn ($record) => $record->remained_amount > 0 ? 'danger' : 'success')
->wrap(false),
TextColumn::make('paymentTypeRecord.payment_type')
->label(__('visits.fields.payment_type'))
->wrap(false)
->placeholder('-'),
])
->defaultSort('visit_date', 'desc')
->headerActions([
CreateAction::make()
->label(__('visits.actions.create'))
->icon('heroicon-o-plus')
->slideOver()
->modalWidth('4xl'),
])
->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])),
])
->toolbarActions([]);
}
}

View File

@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\PaymentTypeResource\Pages;
use App\Models\PaymentType;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class PaymentTypeResource extends Resource
{
protected static ?string $model = PaymentType::class;
protected static ?string $recordTitleAttribute = 'payment_type';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-credit-card';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.clinic');
}
public static function getNavigationSort(): ?int
{
return 2;
}
public static function getNavigationLabel(): string
{
return __('navigation.payment_types.title');
}
public static function getModelLabel(): string
{
return __('navigation.payment_types.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.payment_types.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('payment_type')
->label(__('payment_types.fields.payment_type'))
->required()
->maxLength(50),
TextInput::make('account_no')
->label(__('payment_types.fields.account_no'))
->maxLength(50),
TextInput::make('pos_no')
->label(__('payment_types.fields.pos_no'))
->maxLength(50),
Textarea::make('description')
->label(__('payment_types.fields.description'))
->rows(3)
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('payment_type')
->label(__('payment_types.fields.payment_type'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('account_no')
->label(__('payment_types.fields.account_no'))
->placeholder('-'),
TextColumn::make('pos_no')
->label(__('payment_types.fields.pos_no'))
->placeholder('-'),
TextColumn::make('description')
->label(__('payment_types.fields.description'))
->limit(50)
->placeholder('-'),
TextColumn::make('created_at')
->label(__('payment_types.fields.created_at'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('payment_types.fields.payment_type'),
__('payment_types.fields.account_no'),
__('payment_types.fields.pos_no'),
__('payment_types.fields.description'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->payment_type,
$record->account_no,
$record->pos_no,
$record->description,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'payment-types-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = PaymentType::all();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('payment_types.fields.payment_type'),
__('payment_types.fields.account_no'),
__('payment_types.fields.pos_no'),
__('payment_types.fields.description'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->payment_type,
$record->account_no,
$record->pos_no,
$record->description,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'payment-types-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = PaymentType::all();
return PdfService::download(
'exports.payment-types-pdf',
['records' => $records],
'payment-types-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPaymentTypes::route('/'),
'create' => Pages\CreatePaymentType::route('/create'),
'edit' => Pages\EditPaymentType::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PaymentTypeResource\Pages;
use App\Filament\Resources\PaymentTypeResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePaymentType extends CreateRecord
{
protected static string $resource = PaymentTypeResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PaymentTypeResource\Pages;
use App\Filament\Resources\PaymentTypeResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditPaymentType extends EditRecord
{
protected static string $resource = PaymentTypeResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PaymentTypeResource\Pages;
use App\Filament\Resources\PaymentTypeResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListPaymentTypes extends ListRecords
{
protected static string $resource = PaymentTypeResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,505 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\PrescriptionResource\Pages;
use App\Models\LabTest;
use App\Models\Medication;
use App\Models\Patient;
use App\Models\Prescription;
use App\Services\PdfService;
use Morilog\Jalali\Jalalian;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
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;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class PrescriptionResource extends Resource
{
protected static ?string $model = Prescription::class;
protected static ?string $recordTitleAttribute = 'doctor';
public static function getGloballySearchableAttributes(): array
{
return ['doctor', 'patientRecord.first_name', 'patientRecord.last_name'];
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-document-text';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.patients');
}
public static function getNavigationSort(): ?int
{
return 2;
}
public static function getNavigationLabel(): string
{
return __('navigation.prescriptions.title');
}
public static function getModelLabel(): string
{
return __('navigation.prescriptions.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.prescriptions.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('prescriptions.sections.info'))
->schema([
Select::make('patient')
->label(__('prescriptions.fields.patient'))
->options(fn () => Patient::all()->mapWithKeys(
fn ($p) => [$p->id => $p->full_name]
)->toArray())
->searchable()
->nullable(),
DatePicker::make('issue_date')
->label(__('prescriptions.fields.issue_date'))
->jalali()
->default(now()),
])
->columns(2)
->columnSpanFull(),
Section::make()
->schema([
Tabs::make()
->tabs([
Tab::make(__('prescriptions.sections.medications'))
->icon('heroicon-o-clipboard-document-list')
->schema([
Hidden::make('medication_order'),
CheckboxList::make('medication_ids')
->label(__('prescriptions.fields.medication_ids'))
->options(fn () => Medication::where('is_active', true)
->orderBy('name')
->get()
->mapWithKeys(fn ($m) => [
$m->id => implode(' ', array_filter([
$m->dosage_form,
$m->name,
$m->strength,
$m->quantity ? '#' . $m->quantity : null,
$m->timing,
])),
])
->toArray()
)
->columns(3)
->live()
->afterStateUpdated(function (array $state, callable $set, callable $get) {
$currentOrder = array_map('intval', (array) ($get('medication_order') ?: []));
$state = array_map('intval', $state);
$newOrder = array_values(
array_filter($currentOrder, fn ($id) => in_array($id, $state))
);
foreach ($state as $id) {
if (! in_array($id, $newOrder)) {
$newOrder[] = $id;
}
}
$set('medication_order', $newOrder);
if (empty($newOrder)) {
$set('content', null);
return;
}
$medications = Medication::whereIn('id', $newOrder)->get()->keyBy('id');
$html = '<ul>' . collect($newOrder)
->map(fn ($id) => isset($medications[$id])
? '<li>' . implode(' ', array_filter([
$medications[$id]->dosage_form,
$medications[$id]->name,
$medications[$id]->strength,
$medications[$id]->quantity ? '#' . $medications[$id]->quantity : null,
$medications[$id]->timing,
])) . '</li>'
: ''
)
->filter()
->implode('') . '</ul>';
$set('content', $html);
})
->columnSpanFull(),
RichEditor::make('content')
->label(__('prescriptions.fields.content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
Tab::make(__('prescriptions.sections.lab_tests'))
->icon('heroicon-o-beaker')
->schema([
Hidden::make('lab_test_order'),
CheckboxList::make('lab_test_ids')
->label(__('prescriptions.fields.lab_test_ids'))
->options(fn () => LabTest::where('is_active', true)
->orderBy('name')
->get()
->mapWithKeys(fn ($t) => [$t->id => $t->name])
->toArray()
)
->columns(3)
->live()
->afterStateHydrated(function (array $state, callable $set, callable $get) {
if (empty($state) || ! empty($get('lab_content'))) {
return;
}
$order = array_map('intval', $state);
$set('lab_test_order', $order);
$tests = LabTest::whereIn('id', $order)->get()->keyBy('id');
$html = '<ul>' . collect($order)->map(fn ($id) => isset($tests[$id]) ? '<li>' . $tests[$id]->name . '</li>' : '')->filter()->implode('') . '</ul>';
$set('lab_content', $html);
})
->afterStateUpdated(function (array $state, callable $set, callable $get) {
$currentOrder = array_map('intval', (array) ($get('lab_test_order') ?: []));
$state = array_map('intval', $state);
$newOrder = array_values(
array_filter($currentOrder, fn ($id) => in_array($id, $state))
);
foreach ($state as $id) {
if (! in_array($id, $newOrder)) {
$newOrder[] = $id;
}
}
$set('lab_test_order', $newOrder);
if (empty($newOrder)) {
$set('lab_content', null);
return;
}
$tests = LabTest::whereIn('id', $newOrder)->get()->keyBy('id');
$html = '<ul>' . collect($newOrder)
->map(fn ($id) => isset($tests[$id])
? '<li>' . $tests[$id]->name . '</li>'
: ''
)
->filter()
->implode('') . '</ul>';
$set('lab_content', $html);
})
->columnSpanFull(),
RichEditor::make('lab_content')
->label(__('prescriptions.fields.lab_content'))
->plugins([HighlightColorPlugin::make()])
->toolbarButtons([
['bold', 'italic', 'underline', 'strike'],
['highlightYellow', 'highlightRed'],
['bulletList', 'orderedList'],
['undo', 'redo'],
])
->columnSpanFull(),
]),
])
->columnSpanFull(),
])
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('createAndPrint')
->label(__('prescriptions.actions.save_and_print'))
->color('primary')
->icon('heroicon-o-printer')
->action(function ($livewire) {
$livewire->printType = 'prescription';
$livewire->create();
})
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAndPrintLab')
->label(__('prescriptions.actions.print_lab'))
->color('gray')
->icon('heroicon-o-beaker')
->action(function ($livewire) {
$livewire->printType = 'lab';
$livewire->create();
})
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAndPrintBoth')
->label(__('prescriptions.actions.save_and_print_both'))
->color('warning')
->icon('heroicon-o-document-duplicate')
->action(function ($livewire) {
$livewire->printType = 'both';
$livewire->create();
})
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn ($query) => $query->with(['patientRecord', 'creator']))
->deferLoading()
->columns([
TextColumn::make('patientRecord.first_name')
->label(__('prescriptions.fields.patient'))
->formatStateUsing(fn ($record) => $record->patientRecord?->full_name ?? '-')
->searchable()
->sortable(),
TextColumn::make('doctor')
->label(__('prescriptions.fields.doctor'))
->searchable()
->sortable(),
TextColumn::make('issue_date')
->label(__('prescriptions.fields.issue_date'))
->sortable()
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
})
->placeholder('-'),
TextColumn::make('content')
->label(__('prescriptions.fields.content'))
->formatStateUsing(fn ($state) => strip_tags($state ?? ''))
->limit(80)
->placeholder('-'),
TextColumn::make('creator.name')
->label(__('prescriptions.fields.created_by'))
->sortable(),
TextColumn::make('created_at')
->label(__('prescriptions.fields.created_at'))
->sortable()
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
}),
])
->defaultSort('created_at', 'desc')
->recordActions([
Action::make('printBoth')
->label(__('prescriptions.actions.print_both'))
->icon('heroicon-o-document-duplicate')
->color('purple')
->modalContent(fn ($record) => view('filament.prescription-print-modal', [
'url' => route('prescription.print', ['prescription' => $record, 'type' => 'both']),
]))
->modalHeading(fn ($record) => ($record->patientRecord?->full_name ?? '') . ' — نسخه و آزمایش')
->modalWidth('7xl')
->modalSubmitAction(false)
->modalCancelActionLabel('بستن'),
Action::make('printPrescription')
->label(__('prescriptions.actions.print_prescription'))
->icon('heroicon-o-printer')
->color('info')
->url(fn ($record) => route('prescription.print', $record))
->openUrlInNewTab(),
Action::make('printAdmission')
->label(__('prescriptions.actions.print_admission'))
->icon('heroicon-o-printer')
->color('warning')
->url(fn ($record) => route('prescription.print.admission', $record))
->openUrlInNewTab(),
Action::make('printLab')
->label(__('prescriptions.actions.print_lab'))
->icon('heroicon-o-beaker')
->color('gray')
->url(fn ($record) => route('prescription.print', ['prescription' => $record, 'type' => 'lab']))
->openUrlInNewTab(),
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('prescriptions.fields.patient'),
__('prescriptions.fields.doctor'),
__('prescriptions.fields.issue_date'),
__('prescriptions.fields.content'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->patientRecord?->full_name ?? '',
$record->doctor,
$record->issue_date,
strip_tags($record->content ?? ''),
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'prescriptions-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = Prescription::with('patientRecord')->orderBy('created_at', 'desc')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('prescriptions.fields.patient'),
__('prescriptions.fields.doctor'),
__('prescriptions.fields.issue_date'),
__('prescriptions.fields.content'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->patientRecord?->full_name ?? '',
$record->doctor,
$record->issue_date,
strip_tags($record->content ?? ''),
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'prescriptions-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = Prescription::with('patientRecord')->orderBy('created_at', 'desc')->get();
return PdfService::download(
'exports.prescriptions-pdf',
['records' => $records],
'prescriptions-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListPrescriptions::route('/'),
'create' => Pages\CreatePrescription::route('/create'),
'edit' => Pages\EditPrescription::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PrescriptionResource\Pages;
use App\Filament\Resources\PrescriptionResource;
use App\Models\LabTest;
use App\Models\Medication;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreatePrescription extends CreateRecord
{
protected static string $resource = PrescriptionResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
public string $printType = '';
protected function mutateFormDataBeforeCreate(array $data): array
{
$data['doctor'] = auth()->user()?->name;
$data['issue_datetime'] = now();
$data['medication_ids'] = $data['medication_order'] ?? $data['medication_ids'] ?? [];
$content = $data['content'] ?? '';
if (empty($content) && ! empty($data['medication_ids'])) {
$medications = Medication::whereIn('id', $data['medication_ids'])->get()->keyBy('id');
$data['content'] = '<ul>' . collect($data['medication_ids'])->map(fn ($id) => isset($medications[$id])
? '<li>' . implode(' ', array_filter([
$medications[$id]->dosage_form,
$medications[$id]->name,
$medications[$id]->strength,
$medications[$id]->quantity ? '#' . $medications[$id]->quantity : null,
$medications[$id]->timing,
])) . '</li>'
: ''
)->filter()->implode('') . '</ul>';
}
$labTestIds = $data['lab_test_ids'] ?? [];
$labContent = $data['lab_content'] ?? '';
if (empty($labContent) && ! empty($labTestIds)) {
$tests = LabTest::whereIn('id', $labTestIds)->orderBy('name')->get();
$data['lab_content'] = '<ul>' . $tests->map(fn ($t) => '<li>' . $t->name . '</li>')->implode('') . '</ul>';
}
return $data;
}
protected function getFormActions(): array
{
return [];
}
protected function getRedirectUrl(): string
{
$record = $this->getRecord();
if ($this->printType === 'prescription') {
return route('prescription.print', $record);
}
if ($this->printType === 'lab') {
return route('prescription.print', ['prescription' => $record, 'type' => 'lab']);
}
if ($this->printType === 'both') {
return route('prescription.print', ['prescription' => $record, 'type' => 'both']);
}
if ($this->printType === 'admission') {
return route('prescription.print.admission', $record);
}
return static::getResource()::getUrl('index');
}
}

View File

@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PrescriptionResource\Pages;
use App\Filament\Resources\PrescriptionResource;
use App\Models\LabTest;
use App\Models\Medication;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
use Illuminate\Support\Facades\URL;
class EditPrescription extends EditRecord
{
protected static string $resource = PrescriptionResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function mutateFormDataBeforeFill(array $data): array
{
$data['medication_order'] = $data['medication_ids'] ?? [];
$data['lab_test_order'] = $data['lab_test_ids'] ?? [];
$medicationIds = $data['medication_ids'] ?? [];
if (empty($data['content']) && ! empty($medicationIds)) {
$meds = Medication::whereIn('id', $medicationIds)->get()->keyBy('id');
$data['content'] = '<ul>' . collect($medicationIds)->map(fn ($id) => isset($meds[$id])
? '<li>' . implode(' ', array_filter([
$meds[$id]->dosage_form,
$meds[$id]->name,
$meds[$id]->strength,
$meds[$id]->quantity ? '#' . $meds[$id]->quantity : null,
$meds[$id]->timing,
])) . '</li>'
: ''
)->filter()->implode('') . '</ul>';
}
$labTestIds = $data['lab_test_ids'] ?? [];
if (empty($data['lab_content']) && ! empty($labTestIds)) {
$tests = LabTest::whereIn('id', $labTestIds)->orderBy('name')->get();
$data['lab_content'] = $tests->isNotEmpty()
? '<ul>' . $tests->map(fn ($t) => '<li>' . $t->name . '</li>')->implode('') . '</ul>'
: '';
}
return $data;
}
protected function mutateFormDataBeforeSave(array $data): array
{
$data['doctor'] = auth()->user()?->name;
$data['issue_datetime'] = now();
$data['medication_ids'] = $data['medication_order'] ?? $data['medication_ids'] ?? [];
$data['lab_test_ids'] = $data['lab_test_order'] ?? $data['lab_test_ids'] ?? [];
$content = $data['content'] ?? '';
if (empty($content) && ! empty($data['medication_ids'])) {
$medications = Medication::whereIn('id', $data['medication_ids'])->get()->keyBy('id');
$data['content'] = '<ul>' . collect($data['medication_ids'])->map(fn ($id) => isset($medications[$id])
? '<li>' . implode(' ', array_filter([
$medications[$id]->dosage_form,
$medications[$id]->name,
$medications[$id]->strength,
$medications[$id]->quantity ? '#' . $medications[$id]->quantity : null,
$medications[$id]->timing,
])) . '</li>'
: ''
)->filter()->implode('') . '</ul>';
}
$labContent = $data['lab_content'] ?? '';
if (empty($labContent) && ! empty($data['lab_test_ids'])) {
$tests = LabTest::whereIn('id', $data['lab_test_ids'])->orderBy('name')->get();
$data['lab_content'] = '<ul>' . $tests->map(fn ($t) => '<li>' . $t->name . '</li>')->implode('') . '</ul>';
}
return $data;
}
protected function getRedirectUrl(): string
{
$return = request()->query('return');
if ($return && str_starts_with($return, url('/'))) {
return $return;
}
return static::getResource()::getUrl('index');
}
protected function getHeaderActions(): array
{
return [
Action::make('printPrescription')
->label(__('prescriptions.actions.print_prescription'))
->icon('heroicon-o-printer')
->color('info')
->url(fn () => route('prescription.print', $this->getRecord()))
->openUrlInNewTab(),
Action::make('printAdmission')
->label(__('prescriptions.actions.print_admission'))
->icon('heroicon-o-printer')
->color('warning')
->visible(fn () => $this->getRecord()->patientRecord?->surgeryAppointment !== null)
->url(fn () => route('prescription.print.admission', $this->getRecord()))
->openUrlInNewTab(),
Action::make('printLab')
->label(__('prescriptions.actions.print_lab'))
->icon('heroicon-o-beaker')
->color('gray')
->url(fn () => route('prescription.print', ['prescription' => $this->getRecord(), 'type' => 'lab']))
->openUrlInNewTab(),
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\PrescriptionResource\Pages;
use App\Filament\Resources\PrescriptionResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListPrescriptions extends ListRecords
{
protected static string $resource = PrescriptionResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\RoleResource\Pages;
use App\Services\PdfService;
use Morilog\Jalali\Jalalian;
use BezhanSalleh\FilamentShield\Resources\Roles\RoleResource as ShieldRoleResource;
use BezhanSalleh\FilamentShield\Support\Utils;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
use Filament\Facades\Filament;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Unique;
use Livewire\Component as Livewire;
use Spatie\Permission\Models\Role;
class RoleResource extends ShieldRoleResource
{
public static function getNavigationLabel(): string
{
return __('navigation.roles.title');
}
public static function getModelLabel(): string
{
return __('navigation.roles.title');
}
public static function getPluralModelLabel(): string
{
return __('navigation.roles.title');
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.settings');
}
public static function getNavigationSort(): ?int
{
return 2;
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('name')
->label(__('filament-shield::filament-shield.field.name'))
->unique(
ignoreRecord: true,
modifyRuleUsing: fn (Unique $rule): Unique => Utils::isTenancyEnabled()
? $rule->where(Utils::getTenantModelForeignKey(), Filament::getTenant()?->id)
: $rule
)
->required()
->maxLength(255),
TextInput::make('guard_name')
->label(__('filament-shield::filament-shield.field.guard_name'))
->default(Utils::getFilamentAuthGuard())
->nullable()
->maxLength(255),
Select::make(config('permission.column_names.team_foreign_key'))
->label(__('filament-shield::filament-shield.field.team'))
->placeholder(__('filament-shield::filament-shield.field.team.placeholder'))
->default(Filament::getTenant()?->id)
->options(fn (): array => in_array(Utils::getTenantModel(), [null, '', '0'], true) ? [] : Utils::getTenantModel()::pluck('name', 'id')->toArray())
->visible(fn (): bool => static::shield()->isCentralApp() && Utils::isTenancyEnabled())
->dehydrated(fn (): bool => static::shield()->isCentralApp() && Utils::isTenancyEnabled()),
static::getSelectAllFormComponent(),
])
->columns(['sm' => 2, 'lg' => 3])
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
static::getShieldFormComponents(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->weight(FontWeight::Medium)
->label(__('filament-shield::filament-shield.column.name'))
->formatStateUsing(fn (string $state): string => Str::headline($state))
->searchable(),
TextColumn::make('guard_name')
->badge()
->color('warning')
->label(__('filament-shield::filament-shield.column.guard_name')),
TextColumn::make('team.name')
->default('Global')
->badge()
->color(fn (mixed $state): string => str($state)->contains('Global') ? 'gray' : 'primary')
->label(__('filament-shield::filament-shield.column.team'))
->searchable()
->visible(fn (): bool => static::shield()->isCentralApp() && Utils::isTenancyEnabled()),
TextColumn::make('permissions_count')
->badge()
->label(__('filament-shield::filament-shield.column.permissions'))
->counts('permissions')
->color('primary'),
TextColumn::make('updated_at')
->label(__('filament-shield::filament-shield.column.updated_at'))
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
}),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->paginationPageOptions([5, 10, 25, 50, 100])
->defaultPaginationPageOption(25)
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('filament-shield::filament-shield.column.name'),
__('filament-shield::filament-shield.column.guard_name'),
__('filament-shield::filament-shield.column.permissions'),
__('filament-shield::filament-shield.column.updated_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->guard_name,
$record->permissions_count ?? $record->permissions()->count(),
$record->updated_at?->format('Y-m-d H:i:s'),
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'roles-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = Role::with('permissions')->withCount('permissions')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('filament-shield::filament-shield.column.name'),
__('filament-shield::filament-shield.column.guard_name'),
__('filament-shield::filament-shield.column.permissions'),
__('filament-shield::filament-shield.column.updated_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->guard_name,
$record->permissions_count,
$record->updated_at?->format('Y-m-d H:i:s'),
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'roles-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = Role::with('permissions')->get();
return PdfService::download(
'exports.roles-pdf',
['records' => $records],
'roles-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListRoles::route('/'),
'create' => Pages\CreateRole::route('/create'),
'view' => Pages\ViewRole::route('/{record}'),
'edit' => Pages\EditRole::route('/{record}/edit'),
];
}
public static function toggleSelectAllViaEntities(Livewire $livewire, Set $set): void
{
/** @phpstan-ignore-next-line */
$entitiesStates = collect($livewire->form->getFlatComponents())
->filter(fn (mixed $component): bool => $component instanceof Component)
->reduce(function (mixed $counts, Component $component) {
if ($component instanceof CheckboxList) {
$counts[$component->getName()] = count(array_keys($component->getOptions())) === count(collect($component->getState())->values()->unique()->toArray());
}
return $counts;
}, collect())
->values();
if ($entitiesStates->containsStrict(false)) {
$set('select_all', false);
} else {
$set('select_all', true);
}
}
public static function toggleEntitiesViaSelectAll(Livewire $livewire, Set $set, bool $state): void
{
/** @phpstan-ignore-next-line */
$entitiesComponents = collect($livewire->form->getFlatComponents())
->filter(fn (mixed $component): bool => $component instanceof CheckboxList);
if ($state) {
$entitiesComponents->each(function (CheckboxList $component) use ($set): void {
$set($component->getName(), array_keys($component->getOptions()));
});
} else {
$entitiesComponents->each(fn (CheckboxList $component) => $component->state([]));
}
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use BezhanSalleh\FilamentShield\Resources\Roles\Pages\CreateRole as ShieldCreateRole;
class CreateRole extends ShieldCreateRole
{
protected static string $resource = RoleResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use BezhanSalleh\FilamentShield\Resources\Roles\Pages\EditRole as ShieldEditRole;
use Filament\Actions\DeleteAction;
class EditRole extends ShieldEditRole
{
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use BezhanSalleh\FilamentShield\Resources\Roles\Pages\ListRoles as ShieldListRoles;
class ListRoles extends ShieldListRoles
{
protected static string $resource = RoleResource::class;
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use BezhanSalleh\FilamentShield\Resources\Roles\Pages\ViewRole as ShieldViewRole;
class ViewRole extends ShieldViewRole
{
protected static string $resource = RoleResource::class;
}

View File

@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\SurgeryAppointmentResource\Pages;
use App\Models\Patient;
use App\Models\SurgeryAppointment;
use App\Models\SurgeryCenter;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TimePicker;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Morilog\Jalali\Jalalian;
class SurgeryAppointmentResource extends Resource
{
protected static ?string $model = SurgeryAppointment::class;
protected static bool $isGloballySearchable = false;
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-calendar-days';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.patients');
}
public static function getNavigationSort(): ?int
{
return 3;
}
public static function getNavigationLabel(): string
{
return __('navigation.surgery_appointments.title');
}
public static function getModelLabel(): string
{
return __('navigation.surgery_appointments.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.surgery_appointments.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('surgery_appointments.sections.info'))
->schema([
Select::make('patient_id')
->label(__('surgery_appointments.fields.patient'))
->options(fn () => Patient::all()->mapWithKeys(
fn ($p) => [$p->id => $p->full_name]
)->toArray())
->searchable()
->required(),
Select::make('surgery_center_id')
->label(__('surgery_appointments.fields.surgery_center'))
->options(fn () => SurgeryCenter::pluck('name', 'id')->toArray())
->searchable()
->required(),
DatePicker::make('surgery_date_date')
->label(__('surgery_appointments.fields.surgery_date'))
->jalali()
->minDate(today())
->required(),
TimePicker::make('surgery_date_time')
->label(__('surgery_appointments.fields.surgery_time'))
->native(false)
->seconds(false)
->minutesStep(15)
->default('09:00'),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('patient.first_name')
->label(__('surgery_appointments.fields.patient'))
->formatStateUsing(fn ($record) => $record->patient?->full_name ?? '-')
->searchable()
->sortable(),
TextColumn::make('surgeryCenter.name')
->label(__('surgery_appointments.fields.surgery_center'))
->searchable()
->sortable()
->placeholder('-'),
TextColumn::make('surgery_date')
->label(__('surgery_appointments.fields.surgery_date'))
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
})
->sortable(),
TextColumn::make('creator.name')
->label(__('surgery_appointments.fields.created_by'))
->placeholder('-'),
TextColumn::make('created_at')
->label(__('surgery_appointments.fields.created_at'))
->sortable(),
])
->defaultSort('surgery_date', 'desc')
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
DeleteBulkAction::make(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListSurgeryAppointments::route('/'),
'create' => Pages\CreateSurgeryAppointment::route('/create'),
'edit' => Pages\EditSurgeryAppointment::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryAppointmentResource\Pages;
use App\Filament\Resources\SurgeryAppointmentResource;
use App\Models\Patient;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Morilog\Jalali\Jalalian;
class CreateSurgeryAppointment extends CreateRecord
{
protected static string $resource = SurgeryAppointmentResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function mutateFormDataBeforeCreate(array $data): array
{
$dateStr = $data['surgery_date_date'] ?? '';
$timeStr = $data['surgery_date_time'] ?? '09:00';
$data['surgery_date'] = \Carbon\Carbon::parse($dateStr . ' ' . $timeStr);
unset($data['surgery_date_date'], $data['surgery_date_time']);
return $data;
}
protected function afterCreate(): void
{
$data = $this->form->getState();
$dateStr = $data['surgery_date_date'] ?? $this->record->surgery_date?->format('Y-m-d') ?? '';
$timeStr = $data['surgery_date_time'] ?? $this->record->surgery_date?->format('H:i') ?? '09:00';
$surgeryDateTime = \Carbon\Carbon::parse($dateStr . ' ' . $timeStr);
$jalaliDate = Jalalian::fromCarbon($surgeryDateTime)->format('Y/m/d');
$patient = Patient::find($data['patient_id'] ?? $this->record->patient_id);
$apiKey = \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key'));
$apiUrl = \App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url'));
try {
$response = Http::timeout(30)
->withHeader('X-API-Key', $apiKey)
->asJson()
->post($apiUrl . '/surgery-reminder', [
'phoneNumber' => $patient?->hand_phone ?? '',
'patientName' => $patient?->full_name ?? '',
'surgeryDate' => $jalaliDate,
'appointmentId' => $this->record->id,
'patientId' => $patient?->id,
'surgeryDateFull' => $surgeryDateTime->toDateTimeString(),
]);
if (! ($response->json('success') ?? false)) {
Notification::make()
->title(__('patients.actions.api_failed'))
->body($response->json('message') ?? '')
->danger()
->send();
return;
}
} catch (\Throwable $e) {
Log::warning('Surgery appointment SMS failed', [
'patient_id' => $patient?->id,
'appointment_id' => $this->record->id,
'error' => $e->getMessage(),
]);
Notification::make()
->title(__('patients.actions.api_failed'))
->body($e->getMessage())
->danger()
->send();
return;
}
Notification::make()
->title(__('patients.actions.appointment_saved'))
->body(__('patients.actions.sms_sent'))
->success()
->send();
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryAppointmentResource\Pages;
use App\Filament\Resources\SurgeryAppointmentResource;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
use Illuminate\Support\Facades\Log;
use Morilog\Jalali\Jalalian;
class EditSurgeryAppointment extends EditRecord
{
protected static string $resource = SurgeryAppointmentResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->before(function () {
$record = $this->getRecord();
\App\Filament\Pages\SmsLogsPage::cancelAppointmentMessages(
$record->patient->hand_phone ?? '',
$record->surgery_date->toDateTimeString(),
);
}),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
if (! empty($data['surgery_date'])) {
$date = \Carbon\Carbon::parse($data['surgery_date']);
$data['surgery_date_date'] = $date->format('Y-m-d');
$data['surgery_date_time'] = $date->format('H:i');
}
return $data;
}
protected function mutateFormDataBeforeSave(array $data): array
{
$dateStr = $data['surgery_date_date'] ?? '';
$timeStr = $data['surgery_date_time'] ?? '09:00';
$data['surgery_date'] = \Carbon\Carbon::parse($dateStr . ' ' . $timeStr);
unset($data['surgery_date_date'], $data['surgery_date_time']);
return $data;
}
protected function beforeSave(): void
{
$data = $this->form->getState();
$dateStr = $data['surgery_date_date'] ?? '';
$timeStr = $data['surgery_date_time'] ?? '09:00';
$surgeryDateTime = \Carbon\Carbon::parse($dateStr . ' ' . $timeStr);
$jalaliDate = Jalalian::fromCarbon($surgeryDateTime)->format('Y/m/d');
$patient = $this->getRecord()->patient;
try {
$response = Http::timeout(30)
->withHeader('X-API-Key', \App\Models\OsurgInitial::val('sms_api_key', config('sms.api_key')))
->asJson()
->post(\App\Models\OsurgInitial::val('sms_api_url', config('sms.api_url')) . '/surgery-reminder', [
'phoneNumber' => $patient->hand_phone ?? '',
'patientName' => $patient->full_name,
'surgeryDate' => $jalaliDate,
'appointmentId' => $this->getRecord()->id,
'patientId' => $patient->id,
'surgeryDateFull' => $surgeryDateTime->toDateTimeString(),
]);
if (! ($response->json('success') ?? false)) {
Notification::make()
->title(__('patients.actions.api_failed'))
->body($response->json('message') ?? '')
->danger()
->send();
$this->halt();
}
} catch (\Throwable $e) {
Log::warning('Surgery appointment API failed', [
'patient_id' => $patient->id,
'error' => $e->getMessage(),
]);
Notification::make()
->title(__('patients.actions.api_failed'))
->body($e->getMessage())
->danger()
->send();
$this->halt();
}
}
protected function afterSave(): void
{
Notification::make()
->title(__('patients.actions.appointment_saved'))
->body(__('patients.actions.sms_sent'))
->success()
->send();
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryAppointmentResource\Pages;
use App\Filament\Resources\SurgeryAppointmentResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListSurgeryAppointments extends ListRecords
{
protected static string $resource = SurgeryAppointmentResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\SurgeryCenterResource\Pages;
use App\Models\SurgeryCenter;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class SurgeryCenterResource extends Resource
{
protected static ?string $model = SurgeryCenter::class;
protected static ?string $recordTitleAttribute = 'name';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-building-office-2';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.clinic');
}
public static function getNavigationSort(): ?int
{
return 4;
}
public static function getNavigationLabel(): string
{
return __('navigation.surgery_centers.title');
}
public static function getModelLabel(): string
{
return __('navigation.surgery_centers.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.surgery_centers.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('name')
->label(__('surgery_centers.fields.name'))
->required()
->maxLength(255)
->columnSpanFull(),
TextInput::make('phone')
->label(__('surgery_centers.fields.phone'))
->maxLength(50),
TextInput::make('email')
->label(__('surgery_centers.fields.email'))
->email()
->maxLength(100),
TextInput::make('contact_person')
->label(__('surgery_centers.fields.contact_person'))
->maxLength(100)
->columnSpanFull(),
Textarea::make('address')
->label(__('surgery_centers.fields.address'))
->rows(2)
->columnSpanFull(),
Textarea::make('description')
->label(__('surgery_centers.fields.description'))
->rows(3)
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('surgery_centers.fields.name'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('phone')
->label(__('surgery_centers.fields.phone'))
->placeholder('-'),
TextColumn::make('email')
->label(__('surgery_centers.fields.email'))
->placeholder('-'),
TextColumn::make('contact_person')
->label(__('surgery_centers.fields.contact_person'))
->placeholder('-'),
TextColumn::make('address')
->label(__('surgery_centers.fields.address'))
->limit(40)
->placeholder('-'),
TextColumn::make('created_at')
->label(__('surgery_centers.fields.created_at'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('surgery_centers.fields.name'),
__('surgery_centers.fields.phone'),
__('surgery_centers.fields.email'),
__('surgery_centers.fields.contact_person'),
__('surgery_centers.fields.address'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->phone,
$record->email,
$record->contact_person,
$record->address,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'surgery-centers-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = SurgeryCenter::orderBy('name')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('surgery_centers.fields.name'),
__('surgery_centers.fields.phone'),
__('surgery_centers.fields.email'),
__('surgery_centers.fields.contact_person'),
__('surgery_centers.fields.address'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->phone,
$record->email,
$record->contact_person,
$record->address,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'surgery-centers-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = SurgeryCenter::orderBy('name')->get();
return PdfService::download(
'exports.surgery-centers-pdf',
['records' => $records],
'surgery-centers-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListSurgeryCenters::route('/'),
'create' => Pages\CreateSurgeryCenter::route('/create'),
'edit' => Pages\EditSurgeryCenter::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryCenterResource\Pages;
use App\Filament\Resources\SurgeryCenterResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSurgeryCenter extends CreateRecord
{
protected static string $resource = SurgeryCenterResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryCenterResource\Pages;
use App\Filament\Resources\SurgeryCenterResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditSurgeryCenter extends EditRecord
{
protected static string $resource = SurgeryCenterResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\SurgeryCenterResource\Pages;
use App\Filament\Resources\SurgeryCenterResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListSurgeryCenters extends ListRecords
{
protected static string $resource = SurgeryCenterResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,277 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\TreatmentResource\Pages;
use App\Models\Treatment;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Enums\FontWeight;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class TreatmentResource extends Resource
{
protected static ?string $model = Treatment::class;
protected static ?string $recordTitleAttribute = 'treatment_name';
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-clipboard-document-list';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.diseases');
}
public static function getNavigationSort(): ?int
{
return 2;
}
public static function getNavigationLabel(): string
{
return __('navigation.treatments.title');
}
public static function getModelLabel(): string
{
return __('navigation.treatments.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.treatments.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('treatment_type')
->label(__('treatments.fields.treatment_type'))
->maxLength(50),
TextInput::make('treatment_name')
->label(__('treatments.fields.treatment_name'))
->required()
->maxLength(50),
TextInput::make('treatment_cost')
->label(__('treatments.fields.treatment_cost'))
->numeric()
->minValue(0),
Textarea::make('descriptions')
->label(__('treatments.fields.descriptions'))
->rows(3)
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('treatment_name')
->label(__('treatments.fields.treatment_name'))
->weight(FontWeight::Medium)
->searchable()
->sortable(),
TextColumn::make('treatment_type')
->label(__('treatments.fields.treatment_type'))
->badge()
->color('primary')
->placeholder('-')
->sortable(),
TextColumn::make('treatment_cost')
->label(__('treatments.fields.treatment_cost'))
->numeric(thousandsSeparator: ',')
->placeholder('-')
->sortable(),
TextColumn::make('descriptions')
->label(__('treatments.fields.descriptions'))
->limit(40)
->placeholder('-'),
TextColumn::make('created_at')
->label(__('treatments.fields.created_at'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('treatments.fields.treatment_name'),
__('treatments.fields.treatment_type'),
__('treatments.fields.treatment_cost'),
__('treatments.fields.descriptions'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->treatment_name,
$record->treatment_type,
$record->treatment_cost,
$record->descriptions,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'treatments-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = Treatment::orderBy('treatment_type')->orderBy('treatment_name')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('treatments.fields.treatment_name'),
__('treatments.fields.treatment_type'),
__('treatments.fields.treatment_cost'),
__('treatments.fields.descriptions'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->treatment_name,
$record->treatment_type,
$record->treatment_cost,
$record->descriptions,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'treatments-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = Treatment::orderBy('treatment_type')->orderBy('treatment_name')->get();
return PdfService::download(
'exports.treatments-pdf',
['records' => $records],
'treatments-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListTreatments::route('/'),
'create' => Pages\CreateTreatment::route('/create'),
'edit' => Pages\EditTreatment::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TreatmentResource\Pages;
use App\Filament\Resources\TreatmentResource;
use Filament\Resources\Pages\CreateRecord;
class CreateTreatment extends CreateRecord
{
protected static string $resource = TreatmentResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TreatmentResource\Pages;
use App\Filament\Resources\TreatmentResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditTreatment extends EditRecord
{
protected static string $resource = TreatmentResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\TreatmentResource\Pages;
use App\Filament\Resources\TreatmentResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListTreatments extends ListRecords
{
protected static string $resource = TreatmentResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use App\Services\PdfService;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Hash;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $recordTitleAttribute = 'name';
public static function getGloballySearchableAttributes(): array
{
return ['name', 'username'];
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-users';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.clinic');
}
public static function getNavigationSort(): ?int
{
return 1;
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make()
->schema([
TextInput::make('name')
->label(__('users.fields.name'))
->required()
->maxLength(255),
TextInput::make('username')
->label(__('users.fields.username'))
->required()
->maxLength(255)
->unique(table: User::class, column: 'username', ignoreRecord: true),
TextInput::make('password')
->label(__('users.fields.password'))
->password()
->required(fn (string $operation): bool => $operation === 'create')
->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
->dehydrated(fn (?string $state): bool => filled($state))
->maxLength(255),
Select::make('roles')
->label(__('users.fields.roles'))
->relationship('roles', 'name')
->multiple()
->preload(),
])
->columns(2)
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('createAnother')
->label(__('filament-panels::resources/pages/create-record.form.actions.create_another.label'))
->action('createAnother')
->keyBindings(['mod+shift+s'])
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('users.fields.name'))
->searchable()
->sortable(),
TextColumn::make('username')
->label(__('users.fields.username'))
->searchable()
->sortable(),
TextColumn::make('roles.name')
->label(__('users.fields.roles'))
->badge()
->separator(','),
TextColumn::make('created_at')
->label(__('users.fields.created_at'))
->sortable(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('users.fields.name'),
__('users.fields.username'),
__('users.fields.roles'),
__('users.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->username,
$record->roles->pluck('name')->join(', '),
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'users-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = User::with('roles')->get();
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('users.fields.name'),
__('users.fields.username'),
__('users.fields.roles'),
__('users.fields.created_at'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->name,
$record->username,
$record->roles->pluck('name')->join(', '),
$record->created_at,
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'users-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = User::with('roles')->get();
return PdfService::download(
'exports.users-pdf',
['records' => $records],
'users-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
public static function getNavigationLabel(): string
{
return __('navigation.users.title');
}
public static function getModelLabel(): string
{
return __('navigation.users.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.users.title');
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
}

View File

@ -0,0 +1,478 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources;
use App\Filament\Resources\VisitResource\Pages;
use App\Filament\Widgets\VisitStatsWidget;
use App\Models\Doctor;
use App\Models\Patient;
use App\Models\PaymentType;
use App\Models\Treatment;
use App\Models\Visit;
use App\Services\PdfService;
use Morilog\Jalali\Jalalian;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\TimePicker;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
class VisitResource extends Resource
{
protected static ?string $model = Visit::class;
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 [
VisitStatsWidget::class,
];
}
public static function getGloballySearchableAttributes(): array
{
return ['visit_date', 'patientRecord.first_name', 'patientRecord.last_name', 'treatment_description'];
}
public static function getNavigationIcon(): \BackedEnum|string|null
{
return 'heroicon-o-calendar';
}
public static function getNavigationGroup(): ?string
{
return __('navigation.groups.patients');
}
public static function getNavigationSort(): ?int
{
return 3;
}
public static function getNavigationLabel(): string
{
return __('navigation.visits.title');
}
public static function getModelLabel(): string
{
return __('navigation.visits.singular');
}
public static function getPluralModelLabel(): string
{
return __('navigation.visits.title');
}
public static function form(Schema $schema): Schema
{
return $schema
->components([
Grid::make()
->schema([
Section::make(__('visits.sections.visit_info'))
->schema([
Select::make('patient')
->label(__('visits.fields.patient'))
->options(fn () => Patient::all()->mapWithKeys(
fn ($p) => [$p->id => $p->full_name . ($p->icno ? ' - ' . $p->icno : '')]
)->toArray())
->searchable()
->nullable()
->columnSpan(2),
DatePicker::make('visit_date')
->label(__('visits.fields.visit_date'))
->jalali()
->default(now())
->required(),
TimePicker::make('visit_time')
->label(__('visits.fields.visit_time'))
->native(false)
->seconds(false)
->minutesStep(15)
->afterStateHydrated(function ($component, $state) {
if (blank($state)) {
$component->state(now()->format('Y-m-d H:i:s'));
}
}),
Select::make('doctor')
->label(__('visits.fields.doctor'))
->options(fn () => Doctor::all()->mapWithKeys(
fn ($d) => [$d->id => $d->full_name]
)->toArray())
->default(fn () => Doctor::where('is_default', true)->first()?->id)
->nullable(),
Select::make('treatment')
->label(__('visits.fields.treatment'))
->options(fn () => Treatment::all()->mapWithKeys(
fn ($t) => [$t->id => ($t->descriptions ? '★ ' : '') . ($t->treatment_name ?: $t->treatment_type)]
)->toArray())
->searchable()
->required()
->live()
->afterStateUpdated(function ($state, callable $set) {
if ($state) {
$treatment = Treatment::find($state);
if ($treatment) {
if ($treatment->treatment_cost > 0) {
$set('treatment_cost', $treatment->treatment_cost);
$set('paid_amount', $treatment->treatment_cost);
}
if ($treatment->descriptions) {
$set('treatment_description', $treatment->descriptions);
}
}
}
}),
])
->columns(2)
->columnSpanFull(),
Section::make(__('visits.sections.financial_info'))
->schema([
TextInput::make('treatment_cost')
->label(__('visits.fields.treatment_cost'))
->numeric()
->nullable()
->suffix(__('visits.fields.currency')),
TextInput::make('paid_amount')
->label(__('visits.fields.paid_amount'))
->numeric()
->nullable()
->suffix(__('visits.fields.currency')),
Select::make('payment_type')
->label(__('visits.fields.payment_type'))
->options(fn () => PaymentType::all()->mapWithKeys(
fn ($p) => [$p->id => $p->payment_type]
)->toArray())
->searchable()
->required(),
])
->columns(3)
->columnSpanFull(),
Section::make(__('visits.sections.treatment_details'))
->schema([
Textarea::make('treatment_description')
->label(__('visits.fields.treatment_description'))
->rows(4)
->columnSpanFull(),
])
->columnSpanFull()
->footerActions([
fn (string $operation) => Action::make('create')
->label(__('filament-panels::resources/pages/create-record.form.actions.create.label'))
->submit('create')
->keyBindings(['mod+s'])
->visible($operation === 'create'),
fn (string $operation) => Action::make('cancelCreate')
->label(__('filament-panels::resources/pages/create-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'create'),
fn (string $operation) => Action::make('save')
->label(__('filament-panels::resources/pages/edit-record.form.actions.save.label'))
->submit('save')
->keyBindings(['mod+s'])
->visible($operation === 'edit'),
fn (string $operation) => Action::make('cancelEdit')
->label(__('filament-panels::resources/pages/edit-record.form.actions.cancel.label'))
->url(static::getUrl('index'))
->color('gray')
->visible($operation === 'edit'),
]),
])
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(fn ($query) => $query->with(['patientRecord', 'doctorRecord', 'treatmentRecord', 'paymentTypeRecord']))
->deferLoading()
->columns([
TextColumn::make('visit_date')
->label(__('visits.fields.visit_date'))
->sortable()
->searchable()
->wrap(false)
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d');
} catch (\Exception $e) {
return $state;
}
})
->placeholder('-'),
TextColumn::make('patientRecord.first_name')
->label(__('visits.fields.patient'))
->formatStateUsing(fn ($record) => $record->patientRecord?->full_name ?? '-')
->searchable()
->sortable()
->wrap(false),
TextColumn::make('doctorRecord.first_name')
->label(__('visits.fields.doctor'))
->formatStateUsing(fn ($record) => $record->doctorRecord?->full_name ?? '-')
->sortable()
->wrap(false)
->placeholder('-'),
TextColumn::make('treatmentRecord.treatment_name')
->label(__('visits.fields.treatment'))
->formatStateUsing(fn ($record) => $record->treatmentRecord
? ($record->treatmentRecord->treatment_name ?: $record->treatmentRecord->treatment_type)
: '-')
->sortable()
->wrap(false)
->placeholder('-'),
TextColumn::make('treatment_cost')
->label(__('visits.fields.treatment_cost'))
->formatStateUsing(fn ($state) => $state !== null ? number_format((int) $state) : '-')
->sortable()
->wrap(false)
->placeholder('-'),
TextColumn::make('paid_amount')
->label(__('visits.fields.paid_amount'))
->formatStateUsing(fn ($state) => $state !== null ? number_format((int) $state) : '-')
->sortable()
->wrap(false)
->placeholder('-'),
TextColumn::make('remained_amount')
->label(__('visits.fields.remained_amount'))
->state(fn ($record) => $record->remained_amount)
->formatStateUsing(fn ($state) => number_format((int) $state))
->color(fn ($record) => $record->remained_amount > 0 ? 'danger' : 'success')
->wrap(false)
->placeholder('-'),
TextColumn::make('paymentTypeRecord.payment_type')
->label(__('visits.fields.payment_type'))
->wrap(false)
->placeholder('-'),
TextColumn::make('created_at')
->label(__('visits.fields.created_at'))
->sortable()
->wrap(false)
->formatStateUsing(function ($state) {
if (! $state) {
return '-';
}
try {
$date = \Carbon\Carbon::parse($state);
return Jalalian::fromCarbon($date)->format('Y/m/d - H:i');
} catch (\Exception $e) {
return $state;
}
})
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
Filter::make('visit_date_range')
->label(__('visits.filters.date_range'))
->form([
DatePicker::make('from_date')
->label(__('visits.filters.from_date'))
->jalali(),
DatePicker::make('to_date')
->label(__('visits.filters.to_date'))
->jalali(),
])
->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]);
})
)
->indicateUsing(function (array $data): array {
$indicators = [];
if (! empty($data['from_date'])) {
$indicators[] = __('visits.filters.from_date') . ': ' . $data['from_date'];
}
if (! empty($data['to_date'])) {
$indicators[] = __('visits.filters.to_date') . ': ' . $data['to_date'];
}
return $indicators;
}),
SelectFilter::make('doctor')
->label(__('visits.fields.doctor'))
->options(fn () => Doctor::all()->mapWithKeys(
fn ($d) => [$d->id => $d->full_name]
)->toArray()),
SelectFilter::make('treatment')
->label(__('visits.fields.treatment'))
->options(fn () => Treatment::all()->mapWithKeys(
fn ($t) => [$t->id => ($t->treatment_name ?: $t->treatment_type)]
)->toArray()),
SelectFilter::make('payment_type')
->label(__('visits.fields.payment_type'))
->options(fn () => PaymentType::all()->mapWithKeys(
fn ($p) => [$p->id => $p->payment_type]
)->toArray()),
])
->filtersLayout(FiltersLayout::AboveContent)
->filtersFormColumns(5)
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
BulkAction::make('export_excel_selected')
->label(__('table.export_excel_selected'))
->icon('heroicon-o-table-cells')
->color('success')
->action(function (Collection $records) {
return self::exportToExcel($records);
}),
]),
ActionGroup::make([
Action::make('export_excel')
->label(__('table.export_excel'))
->color('success')
->icon('heroicon-o-table-cells')
->action(function () {
$records = Visit::with(['patientRecord', 'doctorRecord', 'treatmentRecord', 'paymentTypeRecord'])
->orderBy('created_at', 'desc')
->get();
return self::exportToExcel($records);
}),
Action::make('export_pdf')
->label(__('table.export_pdf'))
->color('danger')
->icon('heroicon-o-document-arrow-down')
->action(function () {
$records = Visit::with(['patientRecord', 'doctorRecord', 'treatmentRecord', 'paymentTypeRecord'])
->orderBy('created_at', 'desc')
->get();
return PdfService::download(
'exports.visits-pdf',
['records' => $records],
'visits-' . now()->format('Y-m-d') . '.pdf',
);
}),
])
->label(__('table.export'))
->color('orange')
->button(),
]);
}
private static function exportToExcel(Collection $records): mixed
{
$tempPath = tempnam(sys_get_temp_dir(), 'export_') . '.xlsx';
$writer = new Writer();
$writer->openToFile($tempPath);
$writer->addRow(Row::fromValues([
'ID',
__('visits.fields.visit_date'),
__('visits.fields.patient'),
__('visits.fields.doctor'),
__('visits.fields.treatment'),
__('visits.fields.treatment_cost'),
__('visits.fields.paid_amount'),
__('visits.fields.remained_amount'),
__('visits.fields.payment_type'),
__('visits.fields.treatment_description'),
]));
foreach ($records as $record) {
$writer->addRow(Row::fromValues([
$record->id,
$record->visit_date ?? '',
$record->patientRecord?->full_name ?? '',
$record->doctorRecord?->full_name ?? '',
$record->treatmentRecord
? ($record->treatmentRecord->treatment_name ?: $record->treatmentRecord->treatment_type)
: '',
$record->treatment_cost ?? 0,
$record->paid_amount ?? 0,
$record->remained_amount,
$record->paymentTypeRecord?->payment_type ?? '',
$record->treatment_description ?? '',
]));
}
$writer->close();
return response()->streamDownload(function () use ($tempPath) {
readfile($tempPath);
@unlink($tempPath);
}, 'visits-' . now()->format('Y-m-d') . '.xlsx', [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
]);
}
public static function getPages(): array
{
return [
'index' => Pages\ListVisits::route('/'),
'create' => Pages\CreateVisit::route('/create'),
'edit' => Pages\EditVisit::route('/{record}/edit'),
];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\VisitResource\Pages;
use App\Filament\Resources\VisitResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Enums\Width;
class CreateVisit extends CreateRecord
{
protected static string $resource = VisitResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getFormActions(): array
{
return [];
}
protected function getRedirectUrl(): string
{
return static::getResource()::getUrl('index');
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\VisitResource\Pages;
use App\Filament\Resources\VisitResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Width;
class EditVisit extends EditRecord
{
protected static string $resource = VisitResource::class;
protected Width|string|null $maxContentWidth = Width::SevenExtraLarge;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function getFormActions(): array
{
return [];
}
protected function getRedirectUrl(): string
{
return static::getResource()::getUrl('index');
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Filament\Resources\VisitResource\Pages;
use App\Filament\Resources\VisitResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListVisits extends ListRecords
{
protected static string $resource = VisitResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->icon('heroicon-o-plus'),
];
}
protected function getHeaderWidgets(): array
{
return VisitResource::getWidgets();
}
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 ?? [];
$this->dispatch('visit-stats-updated',
fromDate: ($filters['visit_date_range']['from_date'] ?? null) ?: null,
toDate: ($filters['visit_date_range']['to_date'] ?? null) ?: null,
doctor: isset($filters['doctor']['value']) && $filters['doctor']['value'] !== '' ? (int) $filters['doctor']['value'] : null,
treatment: isset($filters['treatment']['value']) && $filters['treatment']['value'] !== '' ? (int) $filters['treatment']['value'] : null,
paymentType: isset($filters['payment_type']['value']) && $filters['payment_type']['value'] !== '' ? (int) $filters['payment_type']['value'] : null,
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Visit;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Livewire\Attributes\On;
class VisitStatsWidget extends BaseWidget
{
public ?string $fromDate = null;
public ?string $toDate = null;
public ?int $doctor = null;
public ?int $treatment = null;
public ?int $paymentType = null;
public function mount(): void
{
$this->fromDate = now()->toDateString();
$this->toDate = now()->toDateString();
}
#[On('visit-stats-updated')]
public function updateStats(
?string $fromDate,
?string $toDate,
?int $doctor,
?int $treatment,
?int $paymentType,
): void {
if (! $fromDate && ! $toDate) {
$this->fromDate = now()->toDateString();
$this->toDate = null;
} else {
$this->fromDate = $fromDate;
$this->toDate = $toDate;
}
$this->doctor = $doctor;
$this->treatment = $treatment;
$this->paymentType = $paymentType;
}
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]);
}
if ($this->toDate) {
$toDate = \Carbon\Carbon::parse($this->toDate)->format('Y-m-d');
$query->whereRaw("SUBSTRING(visit_date, 1, 10) <= ?", [$toDate]);
}
if ($this->doctor) {
$query->where('doctor', $this->doctor);
}
if ($this->treatment) {
$query->where('treatment', $this->treatment);
}
if ($this->paymentType) {
$query->where('payment_type', $this->paymentType);
}
return $query;
}
protected function getStats(): array
{
$query = $this->buildQuery();
$totalPaid = (int) (clone $query)->sum('paid_amount');
$totalCost = (int) (clone $query)->sum('treatment_cost');
$totalRemained = $totalCost - $totalPaid;
$currency = __('visits.fields.currency');
return [
Stat::make(__('visits.fields.paid_amount'), number_format($totalPaid) . ' ' . $currency)
->color('success')
->icon('heroicon-o-banknotes'),
Stat::make(__('visits.fields.treatment_cost'), number_format($totalCost) . ' ' . $currency)
->color('info')
->icon('heroicon-o-clipboard-document-list'),
Stat::make(__('visits.fields.remained_amount'), number_format($totalRemained) . ' ' . $currency)
->color($totalRemained > 0 ? 'danger' : 'success')
->icon('heroicon-o-arrow-trending-down'),
];
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\LabTest;
use App\Models\Patient;
use App\Models\Prescription;
use Illuminate\Http\Request;
use Morilog\Jalali\Jalalian;
class PrescriptionPrintController extends Controller
{
public function print(Request $request, Prescription $prescription)
{
$prescription->load('patientRecord');
$type = $request->query('type', 'prescription');
$labContent = $prescription->lab_content ?? '';
$labLines = [];
if ($labContent) {
preg_match_all('/<li[^>]*>(.*?)<\/li>/s', $labContent, $m);
$labLines = $m[1] ?? [];
if (empty($labLines)) {
$cleaned = preg_replace('/<br\s*\/?>/i', "\n", $labContent);
$labLines = explode("\n", strip_tags($cleaned));
}
$labLines = array_values(array_filter(
array_map(fn ($l) => html_entity_decode(trim(strip_tags($l)), ENT_QUOTES, 'UTF-8'), $labLines)
));
}
return view('prints.prescription', [
'prescription' => $prescription,
'type' => $type,
'labLines' => $labLines,
]);
}
public function printAdmission(Prescription $prescription)
{
$prescription->load('patientRecord');
$patient = $prescription->patientRecord;
$appointment = $patient?->surgeryAppointment?->load('surgeryCenter');
$centerName = '';
$surgeryDate = '';
if ($appointment) {
try {
$surgeryDate = Jalalian::fromCarbon($appointment->surgery_date)->format('Y/m/d');
} catch (\Throwable) {
$surgeryDate = $appointment->surgery_date?->format('Y-m-d') ?? '';
}
$centerName = $appointment->surgeryCenter?->name ?? '';
}
return view('prints.prescription-admission', [
'prescription' => $prescription,
'centerName' => $centerName,
'surgeryDate' => $surgeryDate,
]);
}
public function printPatientLab(Patient $patient)
{
return view('prints.patient-lab', [
'patient' => $patient,
'issueDate' => Jalalian::now()->format('Y/m/d'),
]);
}
public function printSurgeryAdmission(Patient $patient)
{
$appointment = $patient->surgeryAppointment?->load('surgeryCenter');
$surgeryDate = '';
$centerName = '';
if ($appointment) {
try {
$surgeryDate = Jalalian::fromCarbon($appointment->surgery_date)->format('Y/m/d');
} catch (\Throwable) {
$surgeryDate = $appointment->surgery_date?->format('Y-m-d') ?? '';
}
$centerName = $appointment->surgeryCenter?->name ?? '';
}
return view('prints.surgery-admission', [
'patient' => $patient,
'surgeryDate' => $surgeryDate,
'centerName' => $centerName,
]);
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\OsurgInitial;
use App\Models\SyncLog;
use App\Services\SyncService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class SyncController extends Controller
{
protected function verifyToken(Request $request): bool
{
$token = OsurgInitial::val('sync_token', config('sync.token'));
return $token !== '' && $request->header('X-Sync-Token') === $token;
}
public function export(Request $request): JsonResponse
{
if (! $this->verifyToken($request)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$since = $request->query('since') ?: null;
return response()->json([
'changes' => SyncLog::changesSince($since),
]);
}
public function apply(Request $request): JsonResponse
{
if (! $this->verifyToken($request)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
set_time_limit(0);
$service = SyncService::fromConfig();
$report = $service->applyChanges($request->input('changes', []));
return response()->json(['report' => $report]);
}
public function downloadFile(Request $request): mixed
{
if (! $this->verifyToken($request)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$path = $request->query('path');
if (! $path || ! Storage::disk('public')->exists($path)) {
return response()->json(['error' => 'File not found'], 404);
}
return Storage::disk('public')->download($path);
}
public function receiveFile(Request $request): JsonResponse
{
if (! $this->verifyToken($request)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
set_time_limit(0);
$path = $request->input('path');
$file = $request->file('file');
if (! $path || ! $file) {
return response()->json(['error' => 'Missing path or file'], 400);
}
$path = ltrim(str_replace(['..', '\\'], ['', '/'], $path), '/');
Storage::disk('public')->put($path, file_get_contents($file->getRealPath()));
return response()->json(['success' => true, 'path' => $path]);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale', config('app.locale'));
if (in_array($locale, config('app.available_locales', ['fa', 'en']))) {
app()->setLocale($locale);
}
return $next($request);
}
}

View File

@ -0,0 +1,22 @@
<?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

@ -0,0 +1,90 @@
<?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

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Livewire;
use App\Services\SyncService;
use Filament\Notifications\Notification;
use Livewire\Component;
class SettingsDropdown extends Component
{
public bool $syncSuccess = false;
public bool $syncFailed = false;
public array $reportLines = [];
public bool $peerConnected = false;
public ?string $peerPath = null;
public bool $peerChecked = false;
public function checkPeerStatus(): void
{
$result = SyncService::fromConfig()->ping();
$this->peerConnected = $result['connected'];
$this->peerPath = $result['path'];
$this->peerChecked = true;
}
public function runSync(): void
{
$service = SyncService::fromConfig();
if (! $service->isConfigured()) {
$this->syncFailed = true;
$this->reportLines = [__('navigation.sync.error_not_configured')];
$this->dispatch('sync-finished', success: false);
return;
}
$result = $service->sync();
$this->reportLines = $result['report'] ?? [];
if ($result['success']) {
$this->syncSuccess = true;
$this->dispatch('sync-finished', success: true);
} else {
$this->syncFailed = true;
$this->dispatch('sync-finished', success: false);
}
}
public function render()
{
return view('livewire.settings-dropdown');
}
}

33
app/Models/Doctor.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class Doctor extends Model
{
protected $fillable = [
'first_name',
'last_name',
'speciality',
'license_id',
'is_default',
];
protected function casts(): array
{
return [
'is_default' => 'boolean',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function getFullNameAttribute(): string
{
return $this->first_name . ' ' . $this->last_name;
}
}

28
app/Models/LabTest.php Normal file
View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class LabTest extends Model
{
protected $fillable = [
'name',
'notes',
'is_default',
'is_active',
];
protected function casts(): array
{
return [
'is_default' => 'boolean',
'is_active' => 'boolean',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

33
app/Models/Medication.php Normal file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class Medication extends Model
{
protected $fillable = [
'name',
'dosage_form',
'strength',
'quantity',
'timing',
'notes',
'is_default',
'is_active',
];
protected function casts(): array
{
return [
'is_default' => 'boolean',
'is_active' => 'boolean',
'quantity' => 'integer',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class OsurgInitial extends Model
{
protected $fillable = [
'init_parameter',
'init_value',
'attachment',
];
protected function casts(): array
{
return [
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public static function val(string $key, mixed $default = null): mixed
{
return cache()->remember(
"osurg_initial.{$key}",
60,
fn () => static::where('init_parameter', $key)->value('init_value') ?? $default
);
}
}

105
app/Models/Patient.php Normal file
View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use App\Traits\HasUserStamps;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Patient extends Model
{
use HasUserStamps;
protected $fillable = [
'icno',
'first_name',
'last_name',
'father_name',
'gender',
'birth_date',
'age',
'marital_status',
'education',
'job',
'hand_phone',
'home_phone',
'work_phone',
'other_phone',
'home_address',
'work_address',
'refered_by',
'referal_reason',
'doc_id',
'insurance',
'insurance_no',
'blood_pressure',
'blood_sugar',
'current_illness_1',
'current_illness_2',
'is_undercare',
'undercare_reason',
'is_usingdrug',
'underdrug_reason',
'has_alergyto',
'alergy_reason',
'description',
'surgery_before',
'sergery_after',
'photos_before',
'photos_after',
'videos',
'audio_files',
];
protected function casts(): array
{
return [
'blood_pressure' => 'integer',
'blood_sugar' => 'integer',
'age' => 'integer',
'current_illness_1' => 'array',
'current_illness_2' => 'array',
'has_alergyto' => 'array',
'photos_before' => 'array',
'photos_after' => 'array',
'videos' => 'array',
'audio_files' => 'array',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function getFullNameAttribute(): string
{
return trim($this->first_name . ' ' . $this->last_name);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updator(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
public function surgeryAppointment(): HasOne
{
return $this->hasOne(SurgeryAppointment::class);
}
public function prescriptions(): HasMany
{
return $this->hasMany(Prescription::class, 'patient');
}
public function visits(): HasMany
{
return $this->hasMany(Visit::class, 'patient');
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class PatientIllness extends Model
{
protected $table = 'patient_illness';
protected $fillable = [
'illness',
'row_no',
'priority',
];
protected function casts(): array
{
return [
'row_no' => 'integer',
'priority' => 'integer',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class PaymentType extends Model
{
protected $fillable = [
'payment_type',
'account_no',
'pos_no',
'description',
];
protected function casts(): array
{
return [
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use App\Traits\HasUserStamps;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Prescription extends Model
{
use HasUserStamps;
protected $fillable = [
'patient',
'issue_date',
'issue_datetime',
'doctor',
'medication_ids',
'lab_test_ids',
'content',
'lab_content',
];
protected function casts(): array
{
return [
'issue_datetime' => 'datetime',
'medication_ids' => 'array',
'lab_test_ids' => 'array',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function patientRecord(): BelongsTo
{
return $this->belongsTo(Patient::class, 'patient');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updator(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use App\Traits\HasUserStamps;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SurgeryAppointment extends Model
{
use HasUserStamps;
protected $fillable = [
'patient_id',
'surgery_date',
'surgery_center_id',
];
protected function casts(): array
{
return [
'surgery_date' => 'datetime',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function patient(): BelongsTo
{
return $this->belongsTo(Patient::class);
}
public function surgeryCenter(): BelongsTo
{
return $this->belongsTo(SurgeryCenter::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updator(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class SurgeryCenter extends Model
{
protected $fillable = [
'name',
'address',
'phone',
'email',
'contact_person',
'description',
];
protected function casts(): array
{
return [
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

63
app/Models/SyncLog.php Normal file
View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SyncLog extends Model
{
public $timestamps = false;
protected $table = 'sync_log';
protected $fillable = [
'datetime',
'ip',
'user',
'table_name',
'action',
'record_id',
'changed_data',
];
protected $casts = [
'datetime' => 'datetime',
'changed_data' => 'array',
];
public static function lastSyncId(): int
{
return (int) static::where('action', 'synced')->max('id') ?: 0;
}
public static function changesSince(?string $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');
}
public static function markSynced(string $ip): static
{
return static::create([
'datetime' => now(),
'ip' => $ip,
'user' => auth()->user()?->name ?? 'system',
'table_name' => '',
'action' => 'synced',
]);
}
}

27
app/Models/Treatment.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use Illuminate\Database\Eloquent\Model;
class Treatment extends Model
{
protected $fillable = [
'treatment_type',
'treatment_name',
'descriptions',
'treatment_cost',
];
protected function casts(): array
{
return [
'treatment_cost' => 'integer',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
}

71
app/Models/User.php Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace App\Models;
use App\Casts\JalaliDatetime;
use BezhanSalleh\FilamentShield\Traits\HasPanelShield;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\HasAvatar;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements HasAvatar
{
use HasFactory, Notifiable, HasRoles, HasPanelShield;
protected $fillable = [
'name',
'username',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function getFilamentAvatarUrl(): ?string
{
$name = $this->name ?? $this->username ?? '?';
$initials = $this->getAvatarInitials($name);
$color = $this->getAvatarColor($name);
$svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">'
. '<circle cx="50" cy="50" r="50" fill="' . $color . '"/>'
. '<text x="50" y="50" font-size="38" text-anchor="middle" dominant-baseline="central" fill="white" font-family="Vazirmatn,Tahoma,sans-serif">' . $initials . '</text>'
. '</svg>';
return 'data:image/svg+xml;base64,' . base64_encode($svg);
}
private function getAvatarInitials(string $name): string
{
$words = preg_split('/\s+/', trim($name));
if (count($words) >= 2) {
return mb_substr($words[0], 0, 1) . mb_substr($words[1], 0, 1);
}
return mb_substr($name, 0, 2);
}
private function getAvatarColor(string $name): string
{
$colors = ['#4F46E5', '#7C3AED', '#2563EB', '#0891B2', '#059669', '#D97706', '#DC2626', '#0F766E'];
$index = abs(crc32($name) % count($colors));
return $colors[$index];
}
}

80
app/Models/Visit.php Normal file
View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Casts\JalaliDatetime;
use App\Traits\HasUserStamps;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Visit extends Model
{
use HasUserStamps;
protected $fillable = [
'patient',
'visit_date',
'visit_time',
'treatment',
'treatment_cost',
'payment_type',
'paid_amount',
'pos_ul',
'pos_ll',
'pos_ur',
'pos_lr',
'treatment_description',
'doctor',
];
protected function casts(): array
{
return [
'treatment_cost' => 'integer',
'paid_amount' => 'integer',
'pos_ul' => 'integer',
'pos_ll' => 'integer',
'pos_ur' => 'integer',
'pos_lr' => 'integer',
'created_at' => JalaliDatetime::class,
'updated_at' => JalaliDatetime::class,
];
}
public function getRemainedAmountAttribute(): int
{
return (int) ($this->treatment_cost ?? 0) - (int) ($this->paid_amount ?? 0);
}
public function patientRecord(): BelongsTo
{
return $this->belongsTo(Patient::class, 'patient');
}
public function doctorRecord(): BelongsTo
{
return $this->belongsTo(Doctor::class, 'doctor');
}
public function treatmentRecord(): BelongsTo
{
return $this->belongsTo(Treatment::class, 'treatment');
}
public function paymentTypeRecord(): BelongsTo
{
return $this->belongsTo(PaymentType::class, 'payment_type');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updator(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
}

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