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, fn ($q) => $q->whereDate('visit_date', '>=', $data['from_date'])) ->when($data['to_date'] ?? null, fn ($q) => $q->whereDate('visit_date', '<=', $data['to_date'])) ) ->indicateUsing(function (array $data): array { $indicators = []; 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'), ]; } }