From 6032b9c289abefcd1be9cec236a98cda99bb7c0f Mon Sep 17 00:00:00 2001 From: philipp lang Date: Fri, 13 Jun 2025 23:54:29 +0200 Subject: [PATCH] Add membership view --- app/Activity/Resources/ActivityResource.php | 1 + app/Invoice/Scopes/InvoiceFilterScope.php | 1 - app/Lib/Filter.php | 16 ++-- app/Lib/HasMeta.php | 10 +++ app/Member/Data/MemberData.php | 22 ++++++ app/Member/Data/MembershipData.php | 20 +++++ app/Member/Membership.php | 13 ++++ .../Actions/MembershipIndexAction.php | 8 +- app/Membership/FilterScope.php | 52 +++++++++++++ .../factories/Member/MembershipFactory.php | 5 ++ .../js/components/form/Multipleselect.vue | 38 +++++----- resources/js/composables/useIndex.js | 12 +-- resources/js/views/activity/VIndex.vue | 11 +-- resources/js/views/membership/Index.vue | 74 +++++++++++++++++++ tests/Feature/Membership/ManageTest.php | 71 ++++++++++++++++++ 15 files changed, 307 insertions(+), 47 deletions(-) create mode 100644 app/Member/Data/MemberData.php create mode 100644 app/Membership/FilterScope.php create mode 100644 resources/js/views/membership/Index.vue diff --git a/app/Activity/Resources/ActivityResource.php b/app/Activity/Resources/ActivityResource.php index 78f5339e..a99f5629 100644 --- a/app/Activity/Resources/ActivityResource.php +++ b/app/Activity/Resources/ActivityResource.php @@ -54,6 +54,7 @@ class ActivityResource extends JsonResource 'index' => route('activity.index'), 'create' => route('activity.create'), 'membership_masslist' => route('membership.masslist.index'), + 'membership_index' => route('membership.index'), ], ]; } diff --git a/app/Invoice/Scopes/InvoiceFilterScope.php b/app/Invoice/Scopes/InvoiceFilterScope.php index 2d16d992..cf479f8a 100644 --- a/app/Invoice/Scopes/InvoiceFilterScope.php +++ b/app/Invoice/Scopes/InvoiceFilterScope.php @@ -4,7 +4,6 @@ namespace App\Invoice\Scopes; use App\Invoice\Enums\InvoiceStatus; use App\Invoice\Models\Invoice; -use App\Lib\Filter; use App\Lib\ScoutFilter; use Laravel\Scout\Builder; use Spatie\LaravelData\Attributes\MapInputName; diff --git a/app/Lib/Filter.php b/app/Lib/Filter.php index f7a556a0..0114e4fd 100644 --- a/app/Lib/Filter.php +++ b/app/Lib/Filter.php @@ -14,10 +14,12 @@ abstract class Filter extends Data { /** - * @param Builder $query * @return Builder */ - abstract public function apply(Builder $query): Builder; + abstract public function getQuery(): Builder; + + /** @var Builder */ + protected Builder $query; /** * @param array|string|null $request @@ -36,14 +38,6 @@ abstract class Filter extends Data */ public static function fromPost(?array $post = null): static { - return static::factory()->withoutMagicalCreation()->from($post ?: [])->toDefault(); - } - - /** - * @return static - */ - public function toDefault(): self - { - return $this; + return static::factory()->withoutMagicalCreation()->from($post ?: []); } } diff --git a/app/Lib/HasMeta.php b/app/Lib/HasMeta.php index 9fbdf14d..bc27aeaa 100644 --- a/app/Lib/HasMeta.php +++ b/app/Lib/HasMeta.php @@ -2,6 +2,8 @@ namespace App\Lib; +use Spatie\LaravelData\PaginatedDataCollection; + /** @mixin \Illuminate\Http\Resources\Json\JsonResource */ trait HasMeta { @@ -41,4 +43,12 @@ trait HasMeta { return []; } + + public static function collectPages(mixed $items): array { + $source = parent::collect($items, PaginatedDataCollection::class)->toArray(); + return [ + ...parent::collect($items, PaginatedDataCollection::class)->toArray(), + 'meta' => [...$source['meta'], ...static::meta()] + ]; + } } diff --git a/app/Member/Data/MemberData.php b/app/Member/Data/MemberData.php new file mode 100644 index 00000000..c0f96808 --- /dev/null +++ b/app/Member/Data/MemberData.php @@ -0,0 +1,22 @@ +withoutMagicalCreation()->from([ + 'fullname' => $member->fullname + ]); + } + +} diff --git a/app/Member/Data/MembershipData.php b/app/Member/Data/MembershipData.php index c563cbc8..0fc72582 100644 --- a/app/Member/Data/MembershipData.php +++ b/app/Member/Data/MembershipData.php @@ -2,14 +2,21 @@ namespace App\Member\Data; +use App\Activity; +use App\Group; use Spatie\LaravelData\Data; use App\Lib\Data\DateData; use App\Lib\Data\RecordData; +use App\Lib\HasMeta; use App\Member\Membership; +use App\Membership\FilterScope; +use App\Subactivity; class MembershipData extends Data { + use HasMeta; + public function __construct( public int $id, public RecordData $activity, @@ -17,6 +24,8 @@ class MembershipData extends Data public RecordData $group, public ?DateData $promisedAt, public DateData $from, + public ?DateData $to, + public MemberData $member, public bool $isActive, public array $links, ) {} @@ -29,8 +38,10 @@ class MembershipData extends Data 'subactivity' => $membership->subactivity, 'isActive' => $membership->isActive(), 'from' => $membership->from, + 'to' => $membership->to, 'group' => $membership->group, 'promisedAt' => $membership->promised_at, + 'member' => $membership->member, 'links' => [ 'update' => route('membership.update', $membership), 'destroy' => route('membership.destroy', $membership), @@ -38,4 +49,13 @@ class MembershipData extends Data ]); } + public static function meta(): array { + return [ + 'activities' => RecordData::collect(Activity::get()), + 'subactivities' => RecordData::collect(Subactivity::get()), + 'groups' => RecordData::collect(Group::get()), + 'filter' => FilterScope::fromRequest(request()->input('filter', '')), + ]; + } + } diff --git a/app/Member/Membership.php b/app/Member/Membership.php index 8d4fc9fe..93ed6a26 100644 --- a/app/Member/Membership.php +++ b/app/Member/Membership.php @@ -79,6 +79,19 @@ class Membership extends Model ->where(fn ($query) => $query->whereNull('to')->orWhere('to', '>=', now())); } + /** + * @param Builder $query + * + * @return Builder + */ + public function scopeInactive(Builder $query): Builder + { + return $query->where(fn ($q) => $q + ->orWhere('from', '>=', now()) + ->orWhere('to', '<=', now()) + ); + } + /** * @param Builder $query * diff --git a/app/Membership/Actions/MembershipIndexAction.php b/app/Membership/Actions/MembershipIndexAction.php index 641f820a..680b98a7 100644 --- a/app/Membership/Actions/MembershipIndexAction.php +++ b/app/Membership/Actions/MembershipIndexAction.php @@ -3,21 +3,21 @@ namespace App\Membership\Actions; use App\Member\Data\MembershipData; -use App\Member\Membership; +use App\Membership\FilterScope; use Inertia\Inertia; use Inertia\Response; +use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\Concerns\AsAction; -use Spatie\LaravelData\PaginatedDataCollection; class MembershipIndexAction { use AsAction; - public function asController(): Response + public function asController(ActionRequest $request): Response { return Inertia::render( 'membership/Index', - MembershipData::collect(Membership::orderByRaw('member_id, activity_id, subactivity_id')->paginate(20), PaginatedDataCollection::class)->wrap('data') + ['data' => MembershipData::collectPages(FilterScope::fromRequest($request->input('filter', ''))->getQuery()->paginate(20))] ); } } diff --git a/app/Membership/FilterScope.php b/app/Membership/FilterScope.php new file mode 100644 index 00000000..ec053a0c --- /dev/null +++ b/app/Membership/FilterScope.php @@ -0,0 +1,52 @@ + + */ +class FilterScope extends Filter +{ + /** + * @param array $activities + * @param array $subactivities + * @param array $groups + */ + public function __construct( + public array $activities = [], + public array $subactivities = [], + public array $groups = [], + public ?bool $active = true, + ) {} + + public function getQuery(): Builder + { + $query = (new Membership())->newQuery(); + + if ($this->active === true) { + $query = $query->active(); + } + + if ($this->active === false) { + $query = $query->inactive(); + } + + if (count($this->groups)) { + $query = $query->whereIn('group_id', $this->groups); + } + + if (count($this->activities)) { + $query = $query->whereIn('activity_id', $this->activities); + } + + if (count($this->subactivities)) { + $query = $query->whereIn('subactivity_id', $this->subactivities); + } + + return $query; + } +} diff --git a/database/factories/Member/MembershipFactory.php b/database/factories/Member/MembershipFactory.php index cde9a5ad..11d28789 100644 --- a/database/factories/Member/MembershipFactory.php +++ b/database/factories/Member/MembershipFactory.php @@ -4,6 +4,7 @@ namespace Database\Factories\Member; use App\Activity; use App\Group; +use App\Member\Member; use App\Member\Membership; use App\Subactivity; use Carbon\Carbon; @@ -30,6 +31,10 @@ class MembershipFactory extends Factory ]; } + public function defaults(): self { + return $this->for(Member::factory()->defaults())->for(Group::factory())->for(Activity::factory())->for(Subactivity::factory()); + } + public function inNami(int $namiId): self { return $this->state(['nami_id' => $namiId]); diff --git a/resources/js/components/form/Multipleselect.vue b/resources/js/components/form/Multipleselect.vue index 160ff31f..efafa87f 100644 --- a/resources/js/components/form/Multipleselect.vue +++ b/resources/js/components/form/Multipleselect.vue @@ -1,24 +1,22 @@ diff --git a/resources/js/views/membership/Index.vue b/resources/js/views/membership/Index.vue new file mode 100644 index 00000000..99f1117d --- /dev/null +++ b/resources/js/views/membership/Index.vue @@ -0,0 +1,74 @@ + + + diff --git a/tests/Feature/Membership/ManageTest.php b/tests/Feature/Membership/ManageTest.php index 791d9913..bd9539fb 100644 --- a/tests/Feature/Membership/ManageTest.php +++ b/tests/Feature/Membership/ManageTest.php @@ -32,14 +32,45 @@ it('lists memberships of users', function () { $activity->subactivities()->first(); $this->callFilter('membership.index', []) ->assertInertia(fn(Assert $page) => $page + ->has('data', 1) ->has('data.0', fn(Assert $page) => $page ->where('activity.name', 'Act') ->where('subactivity.name', 'SubAct') + ->where('member.fullname', 'Max Muster') ->where('group.name', 'GG') ->where('promisedAt', null) ->where('links.update', route('membership.update', $member->memberships->first())) ->where('links.destroy', route('membership.destroy', $member->memberships->first())) ->etc() + )->has('meta', fn (Assert $page) => $page + ->where('current_page', 1) + ->where('activities.0.name', 'Act') + ->where('subactivities.0.name', 'SubAct') + ->where('groups.1.name', 'GG') + ->where('filter.active', true) + ->where('filter.groups', []) + ->where('filter.activities', []) + ->where('filter.subactivities', []) + ->etc() + ) + ); +}); + +it('lists end date', function () { + $this->login()->loginNami()->withoutExceptionHandling(); + $member = Member::factory()->defaults() + ->has(Membership::factory()->for(Activity::factory())->for(Subactivity::factory())->for(Group::factory())->ended()) + ->male() + ->name('Max Muster') + ->create(); + + $this->callFilter('membership.index', ['active' => null]) + ->assertInertia(fn(Assert $page) => $page + ->has('data.0', fn(Assert $page) => $page + ->where('to.human', now()->subDays(2)->format('d.m.Y')) + ->where('links.update', route('membership.update', $member->memberships->first())) + ->where('links.destroy', route('membership.destroy', $member->memberships->first())) + ->etc() )->has('meta', fn (Assert $page) => $page ->where('current_page', 1) ->etc() @@ -47,3 +78,43 @@ it('lists memberships of users', function () { ); }); +it('filters for active', function () { + $this->login()->loginNami()->withoutExceptionHandling(); + Membership::factory()->defaults()->ended()->create(); + Membership::factory()->defaults()->count(2)->create(); + + $this->callFilter('membership.index', [])->assertInertia(fn(Assert $page) => $page->has('data', 2)); + $this->callFilter('membership.index', ['active' => null])->assertInertia(fn(Assert $page) => $page->has('data', 3)); + $this->callFilter('membership.index', ['active' => false])->assertInertia(fn(Assert $page) => $page->has('data', 1)); + $this->callFilter('membership.index', ['active' => true])->assertInertia(fn(Assert $page) => $page->has('data', 2)); +}); + +it('filters for group', function () { + $this->login()->loginNami()->withoutExceptionHandling(); + $m1 = Membership::factory()->defaults()->count(2)->create(); + $m2 = Membership::factory()->defaults()->create(); + + $this->callFilter('membership.index', [])->assertInertia(fn(Assert $page) => $page->has('data', 3)); + $this->callFilter('membership.index', ['groups' => [$m1->first()->group_id]])->assertInertia(fn(Assert $page) => $page->has('data', 2)->where('meta.filter.groups', [$m1->first()->group_id])); + $this->callFilter('membership.index', ['groups' => [$m2->group_id]])->assertInertia(fn(Assert $page) => $page->has('data', 1)->where('meta.filter.groups', [$m2->group_id])); +}); + +it('filters for activity', function () { + $this->login()->loginNami()->withoutExceptionHandling(); + $m1 = Membership::factory()->defaults()->count(2)->create(); + $m2 = Membership::factory()->defaults()->create(); + + $this->callFilter('membership.index', [])->assertInertia(fn(Assert $page) => $page->has('data', 3)); + $this->callFilter('membership.index', ['activities' => [$m1->first()->activity_id]])->assertInertia(fn(Assert $page) => $page->has('data', 2)->where('meta.filter.activities', [$m1->first()->activity_id])); + $this->callFilter('membership.index', ['activities' => [$m2->activity_id]])->assertInertia(fn(Assert $page) => $page->has('data', 1)->where('meta.filter.activities', [$m2->activity_id])); +}); + +it('filters for subactivity', function () { + $this->login()->loginNami()->withoutExceptionHandling(); + $m1 = Membership::factory()->defaults()->count(2)->create(); + $m2 = Membership::factory()->defaults()->create(); + + $this->callFilter('membership.index', [])->assertInertia(fn(Assert $page) => $page->has('data', 3)); + $this->callFilter('membership.index', ['subactivities' => [$m1->first()->subactivity_id]])->assertInertia(fn(Assert $page) => $page->has('data', 2)->where('meta.filter.subactivities', [$m1->first()->subactivity_id])); + $this->callFilter('membership.index', ['subactivities' => [$m2->subactivity_id]])->assertInertia(fn(Assert $page) => $page->has('data', 1)->where('meta.filter.subactivities', [$m2->subactivity_id])); +});