From 4afc3053b956bbf3febdfa61ad50df2e2fcccf9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Fri, 28 Mar 2025 16:28:38 +0000 Subject: [PATCH 1/5] feat: groups --- .../Shelter/Resources/BeneficiaryResource.php | 2 +- .../Widgets/StaysWidget.php | 14 +++ .../Shelter/Resources/GroupResource.php | 111 ++++++++++++++++++ .../GroupResource/Pages/ManageGroups.php | 21 ++++ app/Models/Group.php | 39 ++++++ app/Models/Stay.php | 5 + app/Policies/GroupPolicy.php | 67 +++++++++++ app/Providers/AppServiceProvider.php | 1 + database/factories/GroupFactory.php | 25 ++++ database/factories/ShelterFactory.php | 17 ++- .../0001_01_04_000004_create_stays_table.php | 1 - .../0001_01_04_000007_create_groups_table.php | 34 ++++++ lang/en/app.php | 8 ++ 13 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 app/Filament/Shelter/Resources/GroupResource.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/Pages/ManageGroups.php create mode 100644 app/Models/Group.php create mode 100644 app/Policies/GroupPolicy.php create mode 100644 database/factories/GroupFactory.php create mode 100644 database/migrations/0001_01_04_000007_create_groups_table.php diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource.php b/app/Filament/Shelter/Resources/BeneficiaryResource.php index ececddf..ce026fc 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource.php @@ -30,7 +30,7 @@ class BeneficiaryResource extends Resource { protected static ?string $model = Beneficiary::class; - protected static ?string $navigationIcon = 'heroicon-o-user-group'; + protected static ?string $navigationIcon = 'heroicon-o-user'; protected static ?string $recordTitleAttribute = 'name'; diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php index ff0e8ac..99fd942 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php @@ -49,6 +49,20 @@ public function table(Table $table): Table ->sortable() ->shrink(), + TextColumn::make('group.title') + ->label(__('app.field.group')) + ->url(function (Stay $record) { + if (blank($record->group_id)) { + return null; + } + + // TODO: use action to open infolist modal + return '#'; + }) + ->color('primary') + ->wrap() + ->shrink(), + TextColumn::make('request.title') ->label(__('app.field.request')) ->url(function (Stay $record) { diff --git a/app/Filament/Shelter/Resources/GroupResource.php b/app/Filament/Shelter/Resources/GroupResource.php new file mode 100644 index 0000000..73dc1a4 --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource.php @@ -0,0 +1,111 @@ +columns(1) + ->schema([ + TextInput::make('name') + ->label(__('app.field.group_name')), + + Select::make('stays') + ->relationship('stays', 'id') + ->multiple() + ->required(), + ]); + } + + public static function infolist(Infolist $infolist): Infolist + { + return $infolist + ->columns(1) + ->schema([ + TextEntry::make('name') + ->label(__('app.field.group_name')), + + TextEntry::make('stays.id') + ->label(__('app.field.group_members')) + ->listWithLineBreaks(), + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('id') + ->label(__('app.field.id')) + ->prefix('#') + ->sortable() + ->shrink(), + + TextColumn::make('name') + ->label(__('app.field.group_name')) + ->searchable() + ->sortable(), + + TextColumn::make('stays_count') + ->label(__('app.field.group_members')) + ->counts('stays') + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\ActionGroup::make([ + Tables\Actions\ViewAction::make(), + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ManageGroups::route('/'), + ]; + } +} diff --git a/app/Filament/Shelter/Resources/GroupResource/Pages/ManageGroups.php b/app/Filament/Shelter/Resources/GroupResource/Pages/ManageGroups.php new file mode 100644 index 0000000..eda2836 --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource/Pages/ManageGroups.php @@ -0,0 +1,21 @@ + */ + use HasFactory; + use LogsActivity; + + protected static string $factory = GroupFactory::class; + + protected $fillable = [ + 'name', + ]; + + public function stays(): HasMany + { + return $this->hasMany(Stay::class); + } + + public function title(): Attribute + { + return Attribute::make( + fn () => \sprintf('#%d: %s', $this->id, $this->name) + ); + } +} diff --git a/app/Models/Stay.php b/app/Models/Stay.php index 2549332..de3c674 100644 --- a/app/Models/Stay.php +++ b/app/Models/Stay.php @@ -45,6 +45,11 @@ public function beneficiary(): BelongsTo return $this->belongsTo(Beneficiary::class); } + public function group(): BelongsTo + { + return $this->belongsTo(Group::class); + } + public function request(): BelongsTo { return $this->belongsTo(Request::class); diff --git a/app/Policies/GroupPolicy.php b/app/Policies/GroupPolicy.php new file mode 100644 index 0000000..d80faca --- /dev/null +++ b/app/Policies/GroupPolicy.php @@ -0,0 +1,67 @@ +delete($user, $group); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Group $group): bool + { + return $this->delete($user, $group); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a6094df..7216a83 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -87,6 +87,7 @@ protected function enforceMorphMap(): void 'country' => \App\Models\Country::class, 'document' => \App\Models\Document::class, 'form' => \App\Models\Form::class, + 'group' => \App\Models\Group::class, 'location' => \App\Models\Location::class, 'media' => \App\Models\Media::class, 'membership' => \App\Models\Membership::class, diff --git a/database/factories/GroupFactory.php b/database/factories/GroupFactory.php new file mode 100644 index 0000000..a8f69f3 --- /dev/null +++ b/database/factories/GroupFactory.php @@ -0,0 +1,25 @@ + + */ +class GroupFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->words(asText: true), + ]; + } +} diff --git a/database/factories/ShelterFactory.php b/database/factories/ShelterFactory.php index 594b536..49653d6 100644 --- a/database/factories/ShelterFactory.php +++ b/database/factories/ShelterFactory.php @@ -7,6 +7,7 @@ use App\Data\PersonData; use App\Models\Beneficiary; use App\Models\Country; +use App\Models\Group; use App\Models\Location; use App\Models\Organization; use App\Models\Request; @@ -45,12 +46,18 @@ public function definition(): array public function configure(): static { - $beneficiaries = Beneficiary::pluck('id') - ->map(fn (int $id) => [ - 'beneficiary_id' => $id, - ]); + return $this->afterCreating(function (Shelter $shelter) { + $groups = Group::factory() + ->count(10) + ->for($shelter) + ->create(); + + $beneficiaries = Beneficiary::pluck('id') + ->map(fn (int $id) => [ + 'beneficiary_id' => $id, + 'group_id' => fake()->boolean() ? $groups->random()->id : null, + ]); - return $this->afterCreating(function (Shelter $shelter) use ($beneficiaries) { Stay::factory() ->count($beneficiaries->count()) ->sequence(...$beneficiaries) diff --git a/database/migrations/0001_01_04_000004_create_stays_table.php b/database/migrations/0001_01_04_000004_create_stays_table.php index 3471fa7..74de16f 100644 --- a/database/migrations/0001_01_04_000004_create_stays_table.php +++ b/database/migrations/0001_01_04_000004_create_stays_table.php @@ -30,7 +30,6 @@ public function up(): void $table->tinyInteger('children_count')->nullable(); $table->text('children_notes')->nullable(); - // TODO: Group $table->foreignIdFor(Request::class) ->nullable() ->constrained() diff --git a/database/migrations/0001_01_04_000007_create_groups_table.php b/database/migrations/0001_01_04_000007_create_groups_table.php new file mode 100644 index 0000000..caac439 --- /dev/null +++ b/database/migrations/0001_01_04_000007_create_groups_table.php @@ -0,0 +1,34 @@ +id(); + + $table->string('name'); + + $table->foreignIdFor(Shelter::class) + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + }); + + Schema::table('stays', function (Blueprint $table) { + $table->foreignIdFor(Group::class) + ->nullable() + ->constrained() + ->nullOnDelete(); + }); + } +}; diff --git a/lang/en/app.php b/lang/en/app.php index a75cb93..a7a80b5 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -43,6 +43,8 @@ 'form_sections' => 'Form sections', 'form_type' => 'Form type', 'gender' => 'Gender', + 'group_name' => 'Group name', + 'group_members' => 'Members', 'group_size' => 'No.', 'group' => 'Group', 'has_children' => 'Accompanied by children', @@ -142,6 +144,12 @@ ], ], ], + 'group' => [ + 'label' => [ + 'singular' => 'group', + 'plural' => 'groups', + ], + ], 'country' => [ 'label' => [ 'singular' => 'country', From 8ee9609e249b08a4befa45caf616de056b66c759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Wed, 2 Apr 2025 16:32:13 +0100 Subject: [PATCH 2/5] wip --- app/Concerns/Searchable.php | 14 + .../Commands/ScoutDeleteAllIndexesCommand.php | 37 ++ app/Console/Commands/ScoutRebuildCommand.php | 38 ++ .../Schemas/StayInfolist.php | 10 +- .../Widgets/StaysWidget.php | 22 +- .../Shelter/Resources/GroupResource.php | 37 +- .../GroupResource/Pages/CreateGroup.php | 13 + .../Pages/{ManageGroups.php => EditGroup.php} | 6 +- .../GroupResource/Pages/ListGroups.php | 24 + .../GroupResource/Pages/ViewGroup.php | 29 + .../RelationManagers/StaysRelationManager.php | 98 ++++ .../GroupResource/Schemas/GroupForm.php | 23 + .../GroupResource/Schemas/GroupInfolist.php | 31 ++ app/Models/Group.php | 40 ++ app/Models/Stay.php | 87 ++- app/Providers/ScoutServiceProvider.php | 50 ++ bootstrap/providers.php | 1 + composer.json | 4 +- composer.lock | 507 +++++++++++++++++- 19 files changed, 1036 insertions(+), 35 deletions(-) create mode 100644 app/Concerns/Searchable.php create mode 100644 app/Console/Commands/ScoutDeleteAllIndexesCommand.php create mode 100644 app/Console/Commands/ScoutRebuildCommand.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/Pages/CreateGroup.php rename app/Filament/Shelter/Resources/GroupResource/Pages/{ManageGroups.php => EditGroup.php} (71%) create mode 100644 app/Filament/Shelter/Resources/GroupResource/Pages/ListGroups.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/Pages/ViewGroup.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/Schemas/GroupForm.php create mode 100644 app/Filament/Shelter/Resources/GroupResource/Schemas/GroupInfolist.php create mode 100644 app/Providers/ScoutServiceProvider.php diff --git a/app/Concerns/Searchable.php b/app/Concerns/Searchable.php new file mode 100644 index 0000000..816495d --- /dev/null +++ b/app/Concerns/Searchable.php @@ -0,0 +1,14 @@ +each(function (string $model): void { + $this->call('scout:delete-index', ['name' => (new $model)->searchableAs()]); + }); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ScoutRebuildCommand.php b/app/Console/Commands/ScoutRebuildCommand.php new file mode 100644 index 0000000..0b25c32 --- /dev/null +++ b/app/Console/Commands/ScoutRebuildCommand.php @@ -0,0 +1,38 @@ +each(function (string $model): void { + $this->call('scout:flush', ['model' => $model]); + $this->call('scout:import', ['model' => $model]); + }); + + return self::SUCCESS; + } +} diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php index bc58ff0..b1662f9 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php @@ -21,10 +21,10 @@ public static function getSchema(): array TextEntry::make('id') ->label(__('app.field.id')) ->prefix('#'), + TextEntry::make('created_at') ->label(__('app.field.created_at')) ->dateTime(), - ]), Section::make(__('app.stay.details')) @@ -62,6 +62,14 @@ public static function getSchema(): array 'record' => $record->request_id, ])) ->color('primary'), + + TextEntry::make('group.title') + ->visible(fn (Stay $record) => $record->has_group) + ->label(__('app.field.group')) + + // TODO: implement + // ->action() + ->color('primary'), ]), ]; } diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php index 99fd942..408a1ad 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php @@ -6,9 +6,11 @@ use App\Filament\Shelter\Resources\BeneficiaryResource; use App\Filament\Shelter\Resources\BeneficiaryResource\Schemas\StayForm; +use App\Filament\Shelter\Resources\GroupResource\Schemas\GroupInfolist; use App\Filament\Shelter\Resources\RequestResource; use App\Models\Beneficiary; use App\Models\Stay; +use Filament\Tables\Actions\Action; use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; @@ -51,14 +53,20 @@ public function table(Table $table): Table TextColumn::make('group.title') ->label(__('app.field.group')) - ->url(function (Stay $record) { - if (blank($record->group_id)) { - return null; - } + ->action( + fn (Stay $record) => Action::make('view') + ->record($record->group) + ->infolist(GroupInfolist::getSchema()) + ->modal() + ) + // ->url(function (Stay $record) { + // if (blank($record->group_id)) { + // return null; + // } - // TODO: use action to open infolist modal - return '#'; - }) + // // TODO: use action to open infolist modal + // return '#'; + // }) ->color('primary') ->wrap() ->shrink(), diff --git a/app/Filament/Shelter/Resources/GroupResource.php b/app/Filament/Shelter/Resources/GroupResource.php index 73dc1a4..dee4566 100644 --- a/app/Filament/Shelter/Resources/GroupResource.php +++ b/app/Filament/Shelter/Resources/GroupResource.php @@ -5,11 +5,11 @@ namespace App\Filament\Shelter\Resources; use App\Filament\Shelter\Resources\GroupResource\Pages; +use App\Filament\Shelter\Resources\GroupResource\RelationManagers\StaysRelationManager; +use App\Filament\Shelter\Resources\GroupResource\Schemas\GroupForm; +use App\Filament\Shelter\Resources\GroupResource\Schemas\GroupInfolist; use App\Models\Group; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Forms\Form; -use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Infolist; use Filament\Resources\Resource; use Filament\Tables; @@ -45,29 +45,14 @@ public static function form(Form $form): Form { return $form ->columns(1) - ->schema([ - TextInput::make('name') - ->label(__('app.field.group_name')), - - Select::make('stays') - ->relationship('stays', 'id') - ->multiple() - ->required(), - ]); + ->schema(GroupForm::getSchema()); } public static function infolist(Infolist $infolist): Infolist { return $infolist ->columns(1) - ->schema([ - TextEntry::make('name') - ->label(__('app.field.group_name')), - - TextEntry::make('stays.id') - ->label(__('app.field.group_members')) - ->listWithLineBreaks(), - ]); + ->schema(GroupInfolist::getSchema()); } public static function table(Table $table): Table @@ -102,10 +87,20 @@ public static function table(Table $table): Table ]); } + public static function getRelations(): array + { + return [ + StaysRelationManager::class, + ]; + } + public static function getPages(): array { return [ - 'index' => Pages\ManageGroups::route('/'), + 'index' => Pages\ListGroups::route('/'), + 'create' => Pages\CreateGroup::route('/create'), + 'view' => Pages\ViewGroup::route('/{record}'), + 'edit' => Pages\EditGroup::route('/{record}/edit'), ]; } } diff --git a/app/Filament/Shelter/Resources/GroupResource/Pages/CreateGroup.php b/app/Filament/Shelter/Resources/GroupResource/Pages/CreateGroup.php new file mode 100644 index 0000000..6cc206e --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource/Pages/CreateGroup.php @@ -0,0 +1,13 @@ +columns([ + TextColumn::make('id') + ->label(__('app.field.id')) + ->prefix('#') + ->sortable() + ->shrink(), + + TextColumn::make('beneficiary.name') + ->label(__('app.field.name')) + ->sortable() + ->searchable(), + + TextColumn::make('start_date') + ->label(__('app.field.start_date')) + ->date() + ->sortable(), + + TextColumn::make('end_date') + ->label(__('app.field.end_date')) + ->date() + ->sortable(), + ]) + ->headerActions([ + Tables\Actions\AssociateAction::make() + ->modalHeading(__('app.field.group_members')) + ->recordSelect(function (Select $select) { + $staysWithGroup = Stay::query() + ->whereBelongsTo($this->ownerRecord->shelter) + ->whereNotNull('group_id') + ->get(); + + return $select + ->getSearchResultsUsing(fn (string $search) => $this->getOptions($this->ownerRecord->shelter, $search)) + ->options(fn () => $this->getOptions($this->ownerRecord->shelter)) + ->disableOptionWhen(function ($value) use ($staysWithGroup) { + return $staysWithGroup->contains('id', $value); + }); + }), + ]) + ->actions([ + Tables\Actions\DissociateAction::make(), + ]) + ->paginated(false); + } + + protected function getOptions(Shelter $shelter, ?string $search = null): array + { + if (filled($search)) { + $options = Stay::search($search) + ->query( + fn (Builder $query) => $query + ->whereBelongsTo($shelter) + ->with('beneficiary:id,name') + ) + ->where('shelter_id', $shelter->id) + ->limit(50) + ->get(); + } else { + $options = Stay::query() + ->whereBelongsTo($shelter) + ->with('beneficiary:id,name') + ->limit(50) + ->get(); + } + + return $options + ->pluck('title_with_beneficiary_name', 'id') + ->all(); + } +} diff --git a/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupForm.php b/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupForm.php new file mode 100644 index 0000000..4ccfa95 --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupForm.php @@ -0,0 +1,23 @@ +schema([ + TextInput::make('name') + ->label(__('app.field.group_name')) + ->required(), + ]), + ]; + } +} diff --git a/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupInfolist.php b/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupInfolist.php new file mode 100644 index 0000000..46cf223 --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource/Schemas/GroupInfolist.php @@ -0,0 +1,31 @@ +columns(3) + ->schema([ + TextEntry::make('id') + ->label(__('app.field.id')) + ->prefix('#'), + + TextEntry::make('name') + ->label(__('app.field.group_name')), + + TextEntry::make('created_at') + ->label(__('app.field.created_at')) + ->dateTime(), + ]), + ]; + } +} diff --git a/app/Models/Group.php b/app/Models/Group.php index 2b74b85..a3772b1 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -6,7 +6,9 @@ use App\Concerns\BelongsToShelter; use App\Concerns\LogsActivity; +use App\Concerns\Searchable; use Database\Factories\GroupFactory; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -18,6 +20,7 @@ class Group extends Model /** @use HasFactory */ use HasFactory; use LogsActivity; + // use Searchable; protected static string $factory = GroupFactory::class; @@ -36,4 +39,41 @@ public function title(): Attribute fn () => \sprintf('#%d: %s', $this->id, $this->name) ); } + + public static function typesenseModelSettings(): array + { + return [ + 'collection-schema' => [ + 'fields' => [ + [ + 'name' => 'id', + 'type' => 'string', + ], + ], + ], + 'search-parameters' => [ + 'query_by' => 'id', + ], + ]; + } + + protected function makeAllSearchableUsing(Builder $query): Builder + { + return $query->with([ + // 'stays:id,start_date,end_date,beneficiary_id,group_id', + 'stays.beneficiary:id,name', + ]); + } + + public function toSearchableArray(): array + { + return [ + 'id' => (string) $this->id, + // 'stay' => $this->stay, + // 'beneficiary_id' => (string) $this->stay->beneficiary_id, + // 'beneficiary_name' => $this->stay->beneficiary->name, + // 'start_date' => $this->stay->start_date->toFormattedDate(), + // 'end_date' => $this->stay->end_date->toFormattedDate(), + ]; + } } diff --git a/app/Models/Stay.php b/app/Models/Stay.php index de3c674..1849a48 100644 --- a/app/Models/Stay.php +++ b/app/Models/Stay.php @@ -6,6 +6,7 @@ use App\Concerns\BelongsToShelter; use App\Concerns\LogsActivity; +use App\Concerns\Searchable; use Database\Factories\StayFactory; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -19,6 +20,7 @@ class Stay extends Model /** @use HasFactory */ use HasFactory; use LogsActivity; + use Searchable; protected static string $factory = StayFactory::class; @@ -74,6 +76,13 @@ public function hasChildren(): Attribute ); } + public function hasGroup(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes) => filled($attributes['group_id']), + ); + } + public function hasRequest(): Attribute { return Attribute::make( @@ -85,11 +94,87 @@ public function title(): Attribute { return Attribute::make( fn () => \sprintf( - '#%s %s–%s', + '#%s %s - %s', + $this->id, + $this->start_date->toFormattedDate(), + $this->end_date->toFormattedDate() + ) + ); + } + + public function titleWithBeneficiaryName(): Attribute + { + return Attribute::make( + fn () => \sprintf( + '#%s %s %s - %s', $this->id, + $this->beneficiary->name, $this->start_date->toFormattedDate(), $this->end_date->toFormattedDate() ) ); } + + public static function typesenseModelSettings(): array + { + return [ + 'collection-schema' => [ + 'fields' => [ + [ + 'name' => 'id', + 'type' => 'string', + ], + [ + 'name' => 'searchable_id', + 'type' => 'string', + ], + [ + 'name' => 'shelter_id', + 'type' => 'string', + ], + [ + 'name' => 'beneficiary_id', + 'type' => 'string', + ], + [ + 'name' => 'beneficiary_name', + 'type' => 'string', + ], + [ + 'name' => 'start_date', + 'type' => 'string', + ], + [ + 'name' => 'end_date', + 'type' => 'string', + 'optional' => true, + ], + ], + ], + 'search-parameters' => [ + 'query_by' => 'searchable_id,beneficiary_id,beneficiary_name,start_date,end_date', + ], + ]; + } + + protected function makeAllSearchableUsing(Builder $query): Builder + { + return $query->with([ + 'beneficiary:id,name', + 'group', + ]); + } + + public function toSearchableArray(): array + { + return [ + 'id' => (string) $this->id, + 'searchable_id' => (string) $this->id, + 'shelter_id' => (string) $this->shelter_id, + 'beneficiary_id' => (string) $this->beneficiary_id, + 'beneficiary_name' => $this->beneficiary->name, + 'start_date' => $this->start_date->toFormattedDate(), + 'end_date' => $this->end_date?->toFormattedDate(), + ]; + } } diff --git a/app/Providers/ScoutServiceProvider.php b/app/Providers/ScoutServiceProvider.php new file mode 100644 index 0000000..6c6c7c6 --- /dev/null +++ b/app/Providers/ScoutServiceProvider.php @@ -0,0 +1,50 @@ +app->singleton('searchable-models', function (): Collection { + $classes = ClassFinder::getClassesInNamespace('App\\Models', ClassFinder::RECURSIVE_MODE); + + return collect($classes) + ->filter(function (string $class) { + if (! is_subclass_of($class, Model::class)) { + return false; + } + + return \in_array(Searchable::class, class_uses_recursive($class)); + }) + ->values(); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + Config::set( + 'scout.typesense.model-settings', + app('searchable-models') + ->mapWithKeys(fn (string $model) => [ + $model => $model::typesenseModelSettings(), + ]) + ->all() + ); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index b4c3cca..9e9b0da 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -4,6 +4,7 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\ScoutServiceProvider::class, App\Providers\FilamentServiceProvider::class, App\Providers\Filament\AdminPanelProvider::class, App\Providers\Filament\ShelterPanelProvider::class, diff --git a/composer.json b/composer.json index 3904f79..2481518 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "filament/spatie-laravel-media-library-plugin": "^3.3", "filament/spatie-laravel-translatable-plugin": "^3.3", "laravel/framework": "^12.5", + "haydenpierce/class-finder": "^0.5.3", "laravel/scout": "^10.14", "laravel/tinker": "^2.10", "league/flysystem-aws-s3-v3": "^3.29", @@ -27,7 +28,8 @@ "sentry/sentry-laravel": "^4.13", "spatie/laravel-activitylog": "^4.10", "spatie/laravel-data": "^4.14", - "tpetry/laravel-query-expressions": "^1.5" + "tpetry/laravel-query-expressions": "^1.5", + "typesense/typesense-php": "^5.0" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.15", diff --git a/composer.lock b/composer.lock index 8d2d29c..26e4057 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "579eba2c63c18ea06c758958a06737b3", + "content-hash": "1098190a0d2e81f47eaa4b06ec7b7764", "packages": [ { "name": "amphp/amp", @@ -1385,6 +1385,72 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, { "name": "codeat3/blade-majestic-icons", "version": "2.2.0", @@ -3411,6 +3477,48 @@ ], "time": "2025-02-03T10:55:03+00:00" }, + { + "name": "haydenpierce/class-finder", + "version": "0.5.3", + "source": { + "type": "git", + "url": "git@gitlab.com:hpierce1102/ClassFinder.git", + "reference": "40703445c18784edcc6411703e7c3869af11ec8c" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/hpierce1102%2FClassFinder/repository/archive.zip?sha=40703445c18784edcc6411703e7c3869af11ec8c", + "reference": "40703445c18784edcc6411703e7c3869af11ec8c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HaydenPierce\\ClassFinder\\": "src/", + "HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hayden Pierce", + "email": "hayden@haydenpierce.com" + } + ], + "description": "A library that can provide of a list of classes in a given namespace", + "time": "2023-06-18T17:43:01+00:00" + }, { "name": "jean85/pretty-package-versions", "version": "2.1.1", @@ -5966,6 +6074,332 @@ ], "time": "2025-01-30T13:51:11+00:00" }, + { + "name": "php-http/client-common", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.2" + }, + "time": "2024-09-24T06:21:48+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" + }, + "time": "2024-10-02T11:34:13+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, { "name": "phpdocumentor/reflection", "version": "6.1.0", @@ -10894,6 +11328,77 @@ }, "time": "2025-02-14T12:27:16+00:00" }, + { + "name": "typesense/typesense-php", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/typesense/typesense-php.git", + "reference": "513270e6a124101c25b03ee27598efd6b87fbec0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/typesense/typesense-php/zipball/513270e6a124101c25b03ee27598efd6b87fbec0", + "reference": "513270e6a124101c25b03ee27598efd6b87fbec0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "monolog/monolog": "^2.1 || ^3.0 || ^3.3", + "nyholm/psr7": "^1.3", + "php": ">=7.4", + "php-http/client-common": "^1.0 || ^2.3", + "php-http/discovery": "^1.0", + "php-http/httplug": "^1.0 || ^2.2", + "psr/http-client-implementation": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpunit/phpunit": "^11.2", + "squizlabs/php_codesniffer": "3.*", + "symfony/http-client": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Typesense\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Typesense", + "email": "contact@typesense.org", + "homepage": "https://typesense.org", + "role": "Developer" + }, + { + "name": "Abdullah Al-Faqeir", + "email": "abdullah@devloops.net", + "homepage": "https://www.devloops.net", + "role": "Developer" + } + ], + "description": "PHP client for Typesense Search Server: https://github.com/typesense/typesense", + "homepage": "https://github.com/typesense/typesense-php", + "support": { + "docs": "https://typesense.org/api", + "issues": "https://github.com/typesense/typesense-php/issues", + "source": "https://github.com/typesense/typesense-php" + }, + "funding": [ + { + "url": "https://github.com/typesense", + "type": "github" + } + ], + "time": "2025-02-24T21:13:28+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.1", From b71b0adc0eec8ac15695d3a566f09b15f3b396ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Thu, 3 Apr 2025 15:26:10 +0100 Subject: [PATCH 3/5] wip --- .../RelationManagers/StaysRelationManager.php | 14 ++++++++++++-- app/Models/Stay.php | 9 ++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php b/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php index a42e0f7..c273656 100644 --- a/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php +++ b/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php @@ -6,6 +6,7 @@ use App\Models\Shelter; use App\Models\Stay; +use Carbon\Carbon; use Filament\Forms\Components\Select; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables; @@ -45,7 +46,16 @@ public function table(Table $table): Table TextColumn::make('end_date') ->label(__('app.field.end_date')) - ->date() + ->default(__('app.stay.indefinite')) + ->formatStateUsing(function (TextColumn $column, $state) { + if (blank($state) || $state === __('app.stay.indefinite')) { + return $state; + } + + return Carbon::parse($state) + ->setTimezone($column->getTimezone()) + ->translatedFormat($column->evaluate(Table::$defaultDateDisplayFormat)); + }) ->sortable(), ]) ->headerActions([ @@ -79,9 +89,9 @@ protected function getOptions(Shelter $shelter, ?string $search = null): array fn (Builder $query) => $query ->whereBelongsTo($shelter) ->with('beneficiary:id,name') + ->limit(50) ) ->where('shelter_id', $shelter->id) - ->limit(50) ->get(); } else { $options = Stay::query() diff --git a/app/Models/Stay.php b/app/Models/Stay.php index 6e167ce..ea73722 100644 --- a/app/Models/Stay.php +++ b/app/Models/Stay.php @@ -120,7 +120,7 @@ public function titleWithBeneficiaryName(): Attribute $this->id, $this->beneficiary->name, $this->start_date->toFormattedDate(), - $this->end_date->toFormattedDate() + $this->end_date?->toFormattedDate() ?? __('app.stay.indefinite') ) ); } @@ -157,7 +157,6 @@ public static function typesenseModelSettings(): array [ 'name' => 'end_date', 'type' => 'string', - 'optional' => true, ], ], ], @@ -184,7 +183,11 @@ public function toSearchableArray(): array 'beneficiary_id' => (string) $this->beneficiary_id, 'beneficiary_name' => $this->beneficiary->name, 'start_date' => $this->start_date->toFormattedDate(), - 'end_date' => $this->end_date?->toFormattedDate(), + 'end_date' => $this->end_date?->toFormattedDate() ?? + locales() + ->map(fn (Language $language) => __('app.stay.indefinite', locale: $language->code)) + ->unique() + ->join(', '), ]; } } From f1cb437b3ccea5ebd2aaa87171e90017d593430b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Thu, 3 Apr 2025 16:35:18 +0100 Subject: [PATCH 4/5] wip --- .../Schemas/StayInfolist.php | 7 +++--- .../Widgets/StaysWidget.php | 24 +++++++------------ .../RelationManagers/StaysRelationManager.php | 6 +++++ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php index 43dcbf4..02bba5e 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php @@ -4,6 +4,7 @@ namespace App\Filament\Shelter\Resources\BeneficiaryResource\Schemas; +use App\Filament\Shelter\Resources\GroupResource; use App\Filament\Shelter\Resources\RequestResource; use App\Models\Stay; use Carbon\Carbon; @@ -66,9 +67,9 @@ public static function getSchema(): array TextEntry::make('group.title') ->visible(fn (Stay $record) => $record->has_group) ->label(__('app.field.group')) - - // TODO: implement - // ->action() + ->url(fn (Stay $record) => GroupResource::getUrl('view', [ + 'record' => $record->group_id, + ])) ->color('primary'), ]), ]; diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php index 4eb76ae..61cb02d 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php @@ -6,12 +6,11 @@ use App\Filament\Shelter\Resources\BeneficiaryResource; use App\Filament\Shelter\Resources\BeneficiaryResource\Schemas\StayForm; -use App\Filament\Shelter\Resources\GroupResource\Schemas\GroupInfolist; +use App\Filament\Shelter\Resources\GroupResource; use App\Filament\Shelter\Resources\RequestResource; use App\Models\Beneficiary; use App\Models\Stay; use Carbon\Carbon; -use Filament\Tables\Actions\Action; use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\ViewAction; use Filament\Tables\Columns\TextColumn; @@ -63,20 +62,15 @@ public function table(Table $table): Table TextColumn::make('group.title') ->label(__('app.field.group')) - ->action( - fn (Stay $record) => Action::make('view') - ->record($record->group) - ->infolist(GroupInfolist::getSchema()) - ->modal() - ) - // ->url(function (Stay $record) { - // if (blank($record->group_id)) { - // return null; - // } + ->url(function (Stay $record) { + if (blank($record->group_id)) { + return null; + } - // // TODO: use action to open infolist modal - // return '#'; - // }) + return GroupResource::getUrl('view', [ + 'record' => $record->group_id, + ]); + }) ->color('primary') ->wrap() ->shrink(), diff --git a/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php b/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php index c273656..cab8fd8 100644 --- a/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php +++ b/app/Filament/Shelter/Resources/GroupResource/RelationManagers/StaysRelationManager.php @@ -4,6 +4,7 @@ namespace App\Filament\Shelter\Resources\GroupResource\RelationManagers; +use App\Filament\Shelter\Resources\BeneficiaryResource; use App\Models\Shelter; use App\Models\Stay; use Carbon\Carbon; @@ -76,6 +77,11 @@ public function table(Table $table): Table }), ]) ->actions([ + Tables\Actions\ViewAction::make() + ->url(fn (Stay $record) => BeneficiaryResource::getUrl('view', [ + 'record' => $record->beneficiary_id, + ])), + Tables\Actions\DissociateAction::make(), ]) ->paginated(false); From 39fb4daa6462d0e525065e2cc848b1c073d64500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20Ioni=C8=9B=C4=83?= Date: Thu, 3 Apr 2025 17:02:08 +0100 Subject: [PATCH 5/5] wip --- .../BeneficiaryResource/Schemas/StayForm.php | 80 +++++++++++++++---- app/Models/Group.php | 33 ++++---- app/Models/Request.php | 42 ++++++++++ app/Models/Stay.php | 1 + lang/en/app.php | 1 + 5 files changed, 127 insertions(+), 30 deletions(-) diff --git a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayForm.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayForm.php index da798c1..da52676 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayForm.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayForm.php @@ -4,7 +4,9 @@ namespace App\Filament\Shelter\Resources\BeneficiaryResource\Schemas; +use App\Models\Group as GroupModel; use App\Models\Request; +use App\Models\Shelter; use Filament\Facades\Filament; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\DatePicker; @@ -16,7 +18,7 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Get; use Filament\Forms\Set; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Builder; class StayForm { @@ -57,6 +59,21 @@ public static function getSchema(?int $beneficiary_id = null): array ->rows(5), ]), + Checkbox::make('has_group') + ->label(__('app.field.has_group')) + ->columnSpanFull() + ->live(), + + Select::make('group_id') + ->label(__('app.field.group')) + ->visible(fn (Get $get) => $get('has_group')) + ->columnSpanFull() + ->searchable() + ->preload() + ->required() + ->getSearchResultsUsing(fn (string $search) => static::getGroupOptions(Filament::getTenant(), $search)) + ->options(fn () => static::getGroupOptions(Filament::getTenant())), + Checkbox::make('has_request') ->label(__('app.field.has_request')) ->columnSpanFull() @@ -69,19 +86,8 @@ public static function getSchema(?int $beneficiary_id = null): array ->searchable() ->preload() ->required() - ->getSearchResultsUsing( - fn (string $search) => Request::query() - ->whereAllocatable() - ->where('shelter_id', Filament::getTenant()->getKey()) - ->where(function ($query) use ($search) { - $query->whereLike('id', Str::remove('#', $search) . '%') - ->orWhereLike('beneficiary->name', "%{$search}%"); - }) - ->get() - ->pluck('title', 'id') - ) - ->getOptionLabelUsing(fn ($value) => Request::find($value)->title()), - + ->getSearchResultsUsing(fn (string $search) => static::getRequestOptions(Filament::getTenant(), $search)) + ->options(fn () => static::getRequestOptions(Filament::getTenant())), ]), ]; } @@ -106,4 +112,50 @@ public static function getEndDateGroup(): Group }), ]); } + + protected static function getGroupOptions(Shelter $shelter, ?string $search = null): array + { + if (filled($search)) { + $options = GroupModel::search($search) + ->query( + fn (Builder $query) => $query + ->whereBelongsTo($shelter) + ->limit(50) + ) + ->where('shelter_id', $shelter->id) + ->get(); + } else { + $options = GroupModel::query() + ->whereBelongsTo($shelter) + ->limit(50) + ->get(); + } + + return $options + ->pluck('title', 'id') + ->all(); + } + + protected static function getRequestOptions(Shelter $shelter, ?string $search = null): array + { + if (filled($search)) { + $options = Request::search($search) + ->query( + fn (Builder $query) => $query + ->whereBelongsTo($shelter) + ->limit(50) + ) + ->where('shelter_id', $shelter->id) + ->get(); + } else { + $options = Request::query() + ->whereBelongsTo($shelter) + ->limit(50) + ->get(); + } + + return $options + ->pluck('title', 'id') + ->all(); + } } diff --git a/app/Models/Group.php b/app/Models/Group.php index a3772b1..bf454f0 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -8,7 +8,6 @@ use App\Concerns\LogsActivity; use App\Concerns\Searchable; use Database\Factories\GroupFactory; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -20,7 +19,7 @@ class Group extends Model /** @use HasFactory */ use HasFactory; use LogsActivity; - // use Searchable; + use Searchable; protected static string $factory = GroupFactory::class; @@ -49,31 +48,33 @@ public static function typesenseModelSettings(): array 'name' => 'id', 'type' => 'string', ], + [ + 'name' => 'searchable_id', + 'type' => 'string', + ], + [ + 'name' => 'shelter_id', + 'type' => 'string', + ], + [ + 'name' => 'name', + 'type' => 'string', + ], ], ], 'search-parameters' => [ - 'query_by' => 'id', + 'query_by' => 'searchable_id,name', ], ]; } - protected function makeAllSearchableUsing(Builder $query): Builder - { - return $query->with([ - // 'stays:id,start_date,end_date,beneficiary_id,group_id', - 'stays.beneficiary:id,name', - ]); - } - public function toSearchableArray(): array { return [ 'id' => (string) $this->id, - // 'stay' => $this->stay, - // 'beneficiary_id' => (string) $this->stay->beneficiary_id, - // 'beneficiary_name' => $this->stay->beneficiary->name, - // 'start_date' => $this->stay->start_date->toFormattedDate(), - // 'end_date' => $this->stay->end_date->toFormattedDate(), + 'searchable_id' => (string) $this->id, + 'shelter_id' => (string) $this->shelter_id, + 'name' => $this->name, ]; } } diff --git a/app/Models/Request.php b/app/Models/Request.php index 352f72e..bc9a5bb 100644 --- a/app/Models/Request.php +++ b/app/Models/Request.php @@ -6,6 +6,7 @@ use App\Concerns\HasRequestStatus; use App\Concerns\LogsActivity; +use App\Concerns\Searchable; use App\Data\GroupMemberData; use App\Data\PersonData; use App\Enums\Gender; @@ -26,6 +27,7 @@ class Request extends Model use HasFactory; use HasRequestStatus; use LogsActivity; + use Searchable; protected static string $factory = RequestFactory::class; @@ -125,4 +127,44 @@ public function title(): Attribute ) ); } + + public static function typesenseModelSettings(): array + { + return [ + 'collection-schema' => [ + 'fields' => [ + [ + 'name' => 'id', + 'type' => 'string', + ], + [ + 'name' => 'searchable_id', + 'type' => 'string', + ], + [ + 'name' => 'shelter_id', + 'type' => 'int64', + 'optional' => true, + ], + [ + 'name' => 'beneficiary_name', + 'type' => 'string', + ], + ], + ], + 'search-parameters' => [ + 'query_by' => 'searchable_id,beneficiary_name', + ], + ]; + } + + public function toSearchableArray(): array + { + return [ + 'id' => (string) $this->id, + 'searchable_id' => (string) $this->id, + 'shelter_id' => $this->shelter_id, + 'beneficiary_name' => $this->beneficiary->name, + ]; + } } diff --git a/app/Models/Stay.php b/app/Models/Stay.php index ea73722..17058a0 100644 --- a/app/Models/Stay.php +++ b/app/Models/Stay.php @@ -28,6 +28,7 @@ class Stay extends Model 'start_date', 'end_date', 'beneficiary_id', + 'group_id', 'request_id', 'children_count', 'children_notes', diff --git a/lang/en/app.php b/lang/en/app.php index 9cd79ee..5f4e6a1 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -48,6 +48,7 @@ 'group_size' => 'No.', 'group' => 'Group', 'has_children' => 'Accompanied by children', + 'has_group' => 'The beneficiary is part of a group', 'has_request' => 'The stay is linked to an existing request', 'help' => 'Help text', 'id_number' => 'ID number',