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.php b/app/Filament/Shelter/Resources/BeneficiaryResource.php index 7f29ea0..9566c39 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/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/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php b/app/Filament/Shelter/Resources/BeneficiaryResource/Schemas/StayInfolist.php index 084165a..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; @@ -23,10 +24,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 +63,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')) + ->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 5ead1bc..61cb02d 100644 --- a/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php +++ b/app/Filament/Shelter/Resources/BeneficiaryResource/Widgets/StaysWidget.php @@ -6,6 +6,7 @@ use App\Filament\Shelter\Resources\BeneficiaryResource; use App\Filament\Shelter\Resources\BeneficiaryResource\Schemas\StayForm; +use App\Filament\Shelter\Resources\GroupResource; use App\Filament\Shelter\Resources\RequestResource; use App\Models\Beneficiary; use App\Models\Stay; @@ -59,6 +60,21 @@ 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; + } + + return GroupResource::getUrl('view', [ + 'record' => $record->group_id, + ]); + }) + ->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..dee4566 --- /dev/null +++ b/app/Filament/Shelter/Resources/GroupResource.php @@ -0,0 +1,106 @@ +columns(1) + ->schema(GroupForm::getSchema()); + } + + public static function infolist(Infolist $infolist): Infolist + { + return $infolist + ->columns(1) + ->schema(GroupInfolist::getSchema()); + } + + 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 getRelations(): array + { + return [ + StaysRelationManager::class, + ]; + } + + public static function getPages(): array + { + return [ + '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')) + ->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([ + 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\ViewAction::make() + ->url(fn (Stay $record) => BeneficiaryResource::getUrl('view', [ + 'record' => $record->beneficiary_id, + ])), + + 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') + ->limit(50) + ) + ->where('shelter_id', $shelter->id) + ->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 new file mode 100644 index 0000000..bf454f0 --- /dev/null +++ b/app/Models/Group.php @@ -0,0 +1,80 @@ + */ + use HasFactory; + use LogsActivity; + use Searchable; + + 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) + ); + } + + public static function typesenseModelSettings(): array + { + return [ + 'collection-schema' => [ + 'fields' => [ + [ + 'name' => 'id', + 'type' => 'string', + ], + [ + 'name' => 'searchable_id', + 'type' => 'string', + ], + [ + 'name' => 'shelter_id', + 'type' => 'string', + ], + [ + 'name' => 'name', + 'type' => 'string', + ], + ], + ], + 'search-parameters' => [ + 'query_by' => 'searchable_id,name', + ], + ]; + } + + public function toSearchableArray(): array + { + return [ + 'id' => (string) $this->id, + '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 21ac28f..17058a0 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; @@ -26,6 +28,7 @@ class Stay extends Model 'start_date', 'end_date', 'beneficiary_id', + 'group_id', 'request_id', 'children_count', 'children_notes', @@ -45,6 +48,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); @@ -79,6 +87,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( @@ -97,4 +112,83 @@ public function title(): Attribute ) ); } + + 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() ?? __('app.stay.indefinite') + ) + ); + } + + 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', + ], + ], + ], + '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() ?? + locales() + ->map(fn (Language $language) => __('app.stay.indefinite', locale: $language->code)) + ->unique() + ->join(', '), + ]; + } } 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/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", 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 d899e95..00ef965 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_000008_create_groups_table.php b/database/migrations/0001_01_04_000008_create_groups_table.php new file mode 100644 index 0000000..caac439 --- /dev/null +++ b/database/migrations/0001_01_04_000008_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 dc4de8b..5f4e6a1 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -43,9 +43,12 @@ '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', + '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', @@ -144,6 +147,12 @@ ], ], ], + 'group' => [ + 'label' => [ + 'singular' => 'group', + 'plural' => 'groups', + ], + ], 'country' => [ 'label' => [ 'singular' => 'country',