From 3d154c4154683b2aff72f0bd90dfabf67437f15d Mon Sep 17 00:00:00 2001 From: Philipp Lang Date: Tue, 12 Sep 2023 16:54:13 +0200 Subject: [PATCH] Add api for fetching memberships --- app/Member/MemberController.php | 2 +- app/Member/MemberResource.php | 3 +- app/Membership/Actions/ApiIndexAction.php | 33 +++++ .../Actions/MembershipDestroyAction.php | 9 +- .../Actions/MembershipUpdateAction.php | 9 +- app/Membership/MembershipResource.php | 31 ++++ .../factories/Member/MembershipFactory.php | 5 +- resources/js/components/ui/BooleanDisplay.vue | 9 +- .../js/views/member/MemberMemberships.vue | 133 +++++++----------- resources/js/views/member/MemberPayments.vue | 38 ++--- resources/js/views/member/VIndex.vue | 25 ++-- resources/js/views/member/index/Actions.vue | 16 +-- routes/web.php | 8 +- tests/Feature/Member/IndexTest.php | 51 +------ tests/Feature/Membership/DestroyTest.php | 7 +- tests/Feature/Membership/IndexTest.php | 68 +++++++++ tests/Feature/Membership/UpdateTest.php | 4 +- 17 files changed, 252 insertions(+), 199 deletions(-) create mode 100644 app/Membership/Actions/ApiIndexAction.php create mode 100644 tests/Feature/Membership/IndexTest.php diff --git a/app/Member/MemberController.php b/app/Member/MemberController.php index d31b5d4f..a9904aa5 100644 --- a/app/Member/MemberController.php +++ b/app/Member/MemberController.php @@ -23,7 +23,7 @@ class MemberController extends Controller 'data' => MemberResource::collection(Member::search($filter->search)->query( fn ($q) => $q->select('*') ->withFilter($filter) - ->with(['payments.subscription', 'memberships', 'courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships']) + ->with(['payments.subscription', 'courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships']) ->withPendingPayment() ->ordered() )->paginate(15)), diff --git a/app/Member/MemberResource.php b/app/Member/MemberResource.php index 99e46acd..fa47a36f 100644 --- a/app/Member/MemberResource.php +++ b/app/Member/MemberResource.php @@ -73,10 +73,10 @@ class MemberResource extends JsonResource 'has_nami' => null !== $this->nami_id, 'children_phone' => $this->children_phone, 'payments' => PaymentResource::collection($this->whenLoaded('payments')), - 'memberships' => MembershipResource::collection($this->whenLoaded('memberships')), 'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null, 'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug, 'courses' => CourseMemberResource::collection($this->whenLoaded('courses')), + 'memberships' => MembershipResource::collection($this->whenLoaded('memberships')), 'nationality' => new NationalityResource($this->whenLoaded('nationality')), 'region' => new RegionResource($this->whenLoaded('region')), 'full_address' => $this->fullAddress, @@ -105,6 +105,7 @@ class MemberResource extends JsonResource 'lat' => $this->lat, 'lon' => $this->lon, 'links' => [ + 'membership_index' => route('member.membership.index', ['member' => $this->getModel()]), 'show' => route('member.show', ['member' => $this->getModel()]), 'edit' => route('member.edit', ['member' => $this->getModel()]), ], diff --git a/app/Membership/Actions/ApiIndexAction.php b/app/Membership/Actions/ApiIndexAction.php new file mode 100644 index 00000000..5e3c59bc --- /dev/null +++ b/app/Membership/Actions/ApiIndexAction.php @@ -0,0 +1,33 @@ + + */ + public function handle(Member $member): Collection + { + return $member->memberships; + } + + public function asController(Member $member): AnonymousResourceCollection + { + return MembershipResource::collection($this->handle($member)) + ->additional([ + 'meta' => MembershipResource::memberMeta($member) + ]); + } +} diff --git a/app/Membership/Actions/MembershipDestroyAction.php b/app/Membership/Actions/MembershipDestroyAction.php index fbda6c13..57141d33 100644 --- a/app/Membership/Actions/MembershipDestroyAction.php +++ b/app/Membership/Actions/MembershipDestroyAction.php @@ -6,7 +6,7 @@ use App\Maildispatcher\Actions\ResyncAction; use App\Member\Member; use App\Member\Membership; use App\Setting\NamiSettings; -use Illuminate\Http\RedirectResponse; +use Illuminate\Http\JsonResponse; use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\Concerns\AsAction; @@ -32,16 +32,15 @@ class MembershipDestroyAction } } - public function asController(Member $member, Membership $membership, ActionRequest $request, NamiSettings $settings): RedirectResponse + public function asController(Membership $membership, NamiSettings $settings): JsonResponse { $this->handle( - $member, + $membership->member, $membership, $settings, ); ResyncAction::dispatch(); - - return redirect()->back(); + return response()->json([]); } } diff --git a/app/Membership/Actions/MembershipUpdateAction.php b/app/Membership/Actions/MembershipUpdateAction.php index 3f10b20e..bf1b02bb 100644 --- a/app/Membership/Actions/MembershipUpdateAction.php +++ b/app/Membership/Actions/MembershipUpdateAction.php @@ -4,11 +4,10 @@ namespace App\Membership\Actions; use App\Activity; use App\Maildispatcher\Actions\ResyncAction; -use App\Member\Member; use App\Member\Membership; use App\Subactivity; use Carbon\Carbon; -use Illuminate\Http\RedirectResponse; +use Illuminate\Http\JsonResponse; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\In; use Lorisleiva\Actions\ActionRequest; @@ -20,8 +19,6 @@ class MembershipUpdateAction public function handle(Membership $membership, Activity $activity, ?Subactivity $subactivity, ?Carbon $promisedAt): Membership { - $from = now()->startOfDay(); - $membership->update([ 'activity_id' => $activity->id, 'subactivity_id' => $subactivity ? $subactivity->id : null, @@ -56,7 +53,7 @@ class MembershipUpdateAction ]; } - public function asController(Member $member, Membership $membership, ActionRequest $request): RedirectResponse + public function asController(Membership $membership, ActionRequest $request): JsonResponse { $this->handle( $membership, @@ -67,6 +64,6 @@ class MembershipUpdateAction ResyncAction::dispatch(); - return redirect()->back(); + return response()->json([]); } } diff --git a/app/Membership/MembershipResource.php b/app/Membership/MembershipResource.php index e13d649e..a7bcf9f2 100644 --- a/app/Membership/MembershipResource.php +++ b/app/Membership/MembershipResource.php @@ -2,6 +2,10 @@ namespace App\Membership; +use App\Activity; +use App\Lib\HasMeta; +use App\Member\Data\NestedGroup; +use App\Member\Member; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -28,6 +32,33 @@ class MembershipResource extends JsonResource 'human_date' => $this->from->format('d.m.Y'), 'promised_at' => $this->promised_at?->format('Y-m-d'), 'is_active' => $this->isActive(), + 'links' => [ + 'update' => route('membership.update', ['membership' => $this->getModel()]), + 'destroy' => route('membership.destroy', ['membership' => $this->getModel()]), + ] + ]; + } + + /** + * @return array + */ + public static function memberMeta(Member $member): array + { + $activities = Activity::with('subactivities')->get(); + + return [ + 'links' => [ + 'store' => route('member.membership.store', ['member' => $member]), + ], + 'groups' => NestedGroup::cacheForSelect(), + 'activities' => $activities->map(fn ($activity) => ['id' => $activity->id, 'name' => $activity->name]), + 'subactivities' => $activities->mapWithKeys(fn ($activity) => [$activity->id => $activity->subactivities->map(fn ($subactivity) => ['id' => $subactivity->id, 'name' => $subactivity->name])]), + 'default' => [ + 'group_id' => $member->group_id, + 'activity_id' => null, + 'subactivity_id' => null, + 'promised_at' => null, + ], ]; } } diff --git a/database/factories/Member/MembershipFactory.php b/database/factories/Member/MembershipFactory.php index 9b292d64..cde9a5ad 100644 --- a/database/factories/Member/MembershipFactory.php +++ b/database/factories/Member/MembershipFactory.php @@ -63,10 +63,11 @@ class MembershipFactory extends Factory public function in(string $activity, int $activityNamiId, ?string $subactivity = null, ?int $subactivityNamiId = null): self { - $instance = $this->for(Activity::factory()->name($activity)->inNami($activityNamiId)); + $activityModel = Activity::factory()->name($activity)->inNami($activityNamiId)->create(); + $instance = $this->for($activityModel); if ($subactivity) { - $instance = $instance->for(Subactivity::factory()->name($subactivity)->inNami($subactivityNamiId)); + $instance = $instance->for(Subactivity::factory()->name($subactivity)->inNami($subactivityNamiId)->hasAttached($activityModel)); } return $instance; diff --git a/resources/js/components/ui/BooleanDisplay.vue b/resources/js/components/ui/BooleanDisplay.vue index 7b5d6916..4536b3ed 100644 --- a/resources/js/components/ui/BooleanDisplay.vue +++ b/resources/js/components/ui/BooleanDisplay.vue @@ -1,10 +1,7 @@ - diff --git a/resources/js/views/member/MemberPayments.vue b/resources/js/views/member/MemberPayments.vue index d90823ef..f08d0597 100644 --- a/resources/js/views/member/MemberPayments.vue +++ b/resources/js/views/member/MemberPayments.vue @@ -1,20 +1,20 @@ @@ -125,7 +124,6 @@ import Actions from './index/Actions.vue'; import { indexProps, useIndex } from '../../composables/useIndex.js'; import { ref, defineProps } from 'vue'; -const sidebar = ref(null); const single = ref(null); const deleting = ref(null); @@ -147,12 +145,13 @@ async function remove(member) { .catch(() => (deleting.value = null)); } -function openSidebar(index, name) { - single.value = index; - sidebar.value = name; +function openSidebar(type, model) { + single.value = { + type: type, + model: model, + }; } function closeSidebar() { single.value = null; - sidebar.value = null; } diff --git a/resources/js/views/member/index/Actions.vue b/resources/js/views/member/index/Actions.vue index f2bfa591..fcaf9da3 100644 --- a/resources/js/views/member/index/Actions.vue +++ b/resources/js/views/member/index/Actions.vue @@ -1,16 +1,14 @@ diff --git a/routes/web.php b/routes/web.php index c4bc5850..6dafbb86 100644 --- a/routes/web.php +++ b/routes/web.php @@ -35,6 +35,7 @@ use App\Member\Actions\MemberResyncAction; use App\Member\Actions\MemberShowAction; use App\Member\Actions\SearchAction; use App\Member\MemberController; +use App\Membership\Actions\ApiIndexAction; use App\Membership\Actions\ApiListAction; use App\Membership\Actions\MembershipDestroyAction; use App\Membership\Actions\MembershipStoreAction; @@ -59,6 +60,7 @@ Route::group(['middleware' => 'auth:web'], function (): void { Route::post('/api/member/search', SearchAction::class)->name('member.search'); Route::post('/api/membership/member-list', ApiListAction::class)->name('membership.member-list'); Route::post('/api/membership/sync', SyncAction::class)->name('membership.sync'); + Route::post('/api/member/{member}/membership', ApiIndexAction::class)->name('member.membership.index'); Route::get('/initialize', InitializeFormAction::class)->name('initialize.form'); Route::post('/initialize', InitializeAction::class)->name('initialize.store'); Route::resource('member', MemberController::class)->except('show', 'destroy'); @@ -72,9 +74,9 @@ Route::group(['middleware' => 'auth:web'], function (): void { ->name('member.singlepdf'); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf'); - Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('membership.store'); - Route::patch('/member/{member}/membership/{membership}', MembershipUpdateAction::class)->name('membership.store'); - Route::delete('/member/{member}/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy'); + Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store'); + Route::patch('/membership/{membership}', MembershipUpdateAction::class)->name('membership.update'); + Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy'); Route::resource('member.course', CourseController::class); Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz'); Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync'); diff --git a/tests/Feature/Member/IndexTest.php b/tests/Feature/Member/IndexTest.php index b2442565..958f2a0b 100644 --- a/tests/Feature/Member/IndexTest.php +++ b/tests/Feature/Member/IndexTest.php @@ -8,8 +8,6 @@ use App\Member\Member; use App\Member\Membership; use App\Payment\Payment; use App\Subactivity; -use Carbon\Carbon; -use Generator; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\RequestFactories\Child; use Tests\TestCase; @@ -22,7 +20,7 @@ class IndexTest extends TestCase { $this->withoutExceptionHandling()->login()->loginNami(); $group = Group::factory()->create(); - Member::factory()->defaults()->for($group)->create([ + $member = Member::factory()->defaults()->for($group)->create([ 'firstname' => '::firstname::', 'address' => 'Kölner Str 3', 'zip' => 33333, @@ -36,6 +34,8 @@ class IndexTest extends TestCase $this->assertInertiaHas(false, $response, 'data.data.0.has_nami'); $this->assertInertiaHas('Kölner Str 3, 33333 Hilden', $response, 'data.data.0.full_address'); $this->assertInertiaHas($group->id, $response, 'data.data.0.group_id'); + $this->assertInertiaHas(null, $response, 'data.data.0.memberships'); + $this->assertInertiaHas(route('member.membership.index', ['member' => $member]), $response, 'data.data.0.links.membership_index'); } public function testFieldsCanBeNull(): void @@ -82,29 +82,6 @@ class IndexTest extends TestCase $this->assertInertiaHas(false, $response, 'data.data.2.is_leader'); } - public function membershipDataProvider(): Generator - { - yield [now()->subMonths(2), null, true]; - yield [now()->subMonths(2), now()->subDay(), false]; - yield [now()->addDays(2), null, false]; - } - - /** - * @dataProvider membershipDataProvider - */ - public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void - { - $this->withoutExceptionHandling()->login()->loginNami(); - $member = Member::factory() - ->defaults() - ->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to])) - ->create(); - - $response = $this->get('/member'); - - $this->assertInertiaHas($isActive, $response, 'data.data.0.memberships.0.is_active'); - } - public function testItHasNoEfzLinkWhenAddressIsMissing(): void { $this->withoutExceptionHandling()->login()->loginNami(); @@ -155,28 +132,6 @@ class IndexTest extends TestCase $this->assertInertiaHas('€ Mitglied', $response, "data.meta.formActivities.{$activity->id}"); } - public function testItShowsActivityAndSubactivityNamesOfMember(): void - { - $this->withoutExceptionHandling()->login()->loginNami(); - $group = Group::factory()->create(); - $member = Member::factory() - ->defaults() - ->has(Membership::factory()->for($group)->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')) - ->create(); - - $response = $this->get('/member'); - - $this->assertInertiaHas([ - 'activity_id' => $member->memberships->first()->activity_id, - 'subactivity_id' => $member->memberships->first()->subactivity_id, - 'activity_name' => '€ Mitglied', - 'subactivity_name' => 'Wölfling', - 'human_date' => '02.11.2022', - 'group_id' => $group->id, - 'id' => $member->memberships->first()->id, - ], $response, 'data.data.0.memberships.0'); - } - public function testItReturnsPayments(): void { $this->withoutExceptionHandling()->login()->loginNami(); diff --git a/tests/Feature/Membership/DestroyTest.php b/tests/Feature/Membership/DestroyTest.php index 2ec66291..f03177a9 100644 --- a/tests/Feature/Membership/DestroyTest.php +++ b/tests/Feature/Membership/DestroyTest.php @@ -44,9 +44,9 @@ class DestroyTest extends TestCase ->inNami(6) ->create(); - $response = $this->from('/member')->delete("/member/{$member->id}/membership/{$member->memberships->first()->id}"); + $response = $this->delete("/membership/{$member->memberships->first()->id}"); - $response->assertRedirect('/member'); + $response->assertOk(); $this->assertEquals(1506, $member->fresh()->version); $this->assertDatabaseMissing('memberships', [ 'member_id' => $member->id, @@ -65,9 +65,8 @@ class DestroyTest extends TestCase ->inNami(6) ->create(); - $response = $this->from('/member')->delete("/member/{$member->id}/membership/{$member->memberships->first()->id}"); + $response = $this->delete("/membership/{$member->memberships->first()->id}"); - $response->assertRedirect('/member'); $this->assertDatabaseMissing('memberships', [ 'member_id' => $member->id, ]); diff --git a/tests/Feature/Membership/IndexTest.php b/tests/Feature/Membership/IndexTest.php new file mode 100644 index 00000000..d084179f --- /dev/null +++ b/tests/Feature/Membership/IndexTest.php @@ -0,0 +1,68 @@ +withoutExceptionHandling()->login()->loginNami(); + $group = Group::factory()->create(['name' => 'aaaaaaaa']); + $member = Member::factory() + ->defaults() + ->for($group) + ->has(Membership::factory()->for($group)->in('€ Mitglied', 122, 'Wölfling', 234)->from('2022-11-02')) + ->create(); + $membership = $member->memberships->first(); + + $this->postJson("/api/member/{$member->id}/membership") + ->assertJsonPath('data.0.activity_id', $membership->activity_id) + ->assertJsonPath('data.0.subactivity_id', $membership->subactivity_id) + ->assertJsonPath('data.0.activity_name', '€ Mitglied') + ->assertJsonPath('data.0.subactivity_name', 'Wölfling') + ->assertJsonPath('data.0.human_date', '02.11.2022') + ->assertJsonPath('data.0.group_id', $group->id) + ->assertJsonPath('data.0.id', $membership->id) + ->assertJsonPath('data.0.links.update', route('membership.update', ['membership' => $membership])) + ->assertJsonPath('data.0.links.destroy', route('membership.destroy', ['membership' => $membership])) + ->assertJsonPath('meta.default.activity_id', null) + ->assertJsonPath('meta.default.group_id', $group->id) + ->assertJsonPath('meta.groups.0.id', $group->id) + ->assertJsonPath('meta.activities.0.id', $membership->activity_id) + ->assertJsonPath("meta.subactivities.{$membership->activity_id}.0.id", $membership->subactivity_id) + ->assertJsonPath('meta.links.store', route('member.membership.store', ['member' => $member])); + } + + public function membershipDataProvider(): Generator + { + yield [now()->subMonths(2), null, true]; + yield [now()->subMonths(2), now()->subDay(), false]; + yield [now()->addDays(2), null, false]; + } + + /** + * @dataProvider membershipDataProvider + */ + public function testItShowsIfMembershipIsActive(Carbon $from, ?Carbon $to, bool $isActive): void + { + $this->withoutExceptionHandling()->login()->loginNami(); + $member = Member::factory() + ->defaults() + ->has(Membership::factory()->in('€ LeiterIn', 455, 'Pfadfinder', 15)->state(['from' => $from, 'to' => $to])) + ->create(); + + $this->postJson("/api/member/{$member->id}/membership") + ->assertJsonPath('data.0.is_active', $isActive); + } +} diff --git a/tests/Feature/Membership/UpdateTest.php b/tests/Feature/Membership/UpdateTest.php index 16c503f6..1542d382 100644 --- a/tests/Feature/Membership/UpdateTest.php +++ b/tests/Feature/Membership/UpdateTest.php @@ -37,11 +37,11 @@ class UpdateTest extends TestCase $membership = $member->memberships->first(); $response = $this->from('/member')->patch( - "/member/{$member->id}/membership/{$membership->id}", + "/membership/{$membership->id}", MembershipRequestFactory::new()->promise(now())->in($membership->activity, $membership->subactivity)->create() ); - $response->assertRedirect('/member'); + $response->assertOk(); $this->assertDatabaseHas('memberships', [ 'member_id' => $member->id, 'activity_id' => $activity->id,