Add api for fetching memberships
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Philipp Lang 2023-09-12 16:54:13 +02:00
parent 85f0d6c515
commit 3d154c4154
17 changed files with 252 additions and 199 deletions

View File

@ -23,7 +23,7 @@ class MemberController extends Controller
'data' => MemberResource::collection(Member::search($filter->search)->query( 'data' => MemberResource::collection(Member::search($filter->search)->query(
fn ($q) => $q->select('*') fn ($q) => $q->select('*')
->withFilter($filter) ->withFilter($filter)
->with(['payments.subscription', 'memberships', 'courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships']) ->with(['payments.subscription', 'courses', 'subscription', 'leaderMemberships', 'ageGroupMemberships'])
->withPendingPayment() ->withPendingPayment()
->ordered() ->ordered()
)->paginate(15)), )->paginate(15)),

View File

@ -73,10 +73,10 @@ class MemberResource extends JsonResource
'has_nami' => null !== $this->nami_id, 'has_nami' => null !== $this->nami_id,
'children_phone' => $this->children_phone, 'children_phone' => $this->children_phone,
'payments' => PaymentResource::collection($this->whenLoaded('payments')), '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, 'pending_payment' => $this->pending_payment ? number_format($this->pending_payment / 100, 2, ',', '.') . ' €' : null,
'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug, 'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug,
'courses' => CourseMemberResource::collection($this->whenLoaded('courses')), 'courses' => CourseMemberResource::collection($this->whenLoaded('courses')),
'memberships' => MembershipResource::collection($this->whenLoaded('memberships')),
'nationality' => new NationalityResource($this->whenLoaded('nationality')), 'nationality' => new NationalityResource($this->whenLoaded('nationality')),
'region' => new RegionResource($this->whenLoaded('region')), 'region' => new RegionResource($this->whenLoaded('region')),
'full_address' => $this->fullAddress, 'full_address' => $this->fullAddress,
@ -105,6 +105,7 @@ class MemberResource extends JsonResource
'lat' => $this->lat, 'lat' => $this->lat,
'lon' => $this->lon, 'lon' => $this->lon,
'links' => [ 'links' => [
'membership_index' => route('member.membership.index', ['member' => $this->getModel()]),
'show' => route('member.show', ['member' => $this->getModel()]), 'show' => route('member.show', ['member' => $this->getModel()]),
'edit' => route('member.edit', ['member' => $this->getModel()]), 'edit' => route('member.edit', ['member' => $this->getModel()]),
], ],

View File

@ -0,0 +1,33 @@
<?php
namespace App\Membership\Actions;
use App\Member\Member;
use App\Member\Membership;
use App\Membership\MembershipResource;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ApiIndexAction
{
use AsAction;
/**
* @return Collection<int, Membership>
*/
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)
]);
}
}

View File

@ -6,7 +6,7 @@ use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member; use App\Member\Member;
use App\Member\Membership; use App\Member\Membership;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; 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( $this->handle(
$member, $membership->member,
$membership, $membership,
$settings, $settings,
); );
ResyncAction::dispatch(); ResyncAction::dispatch();
return response()->json([]);
return redirect()->back();
} }
} }

View File

@ -4,11 +4,10 @@ namespace App\Membership\Actions;
use App\Activity; use App\Activity;
use App\Maildispatcher\Actions\ResyncAction; use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member;
use App\Member\Membership; use App\Member\Membership;
use App\Subactivity; use App\Subactivity;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\In;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
@ -20,8 +19,6 @@ class MembershipUpdateAction
public function handle(Membership $membership, Activity $activity, ?Subactivity $subactivity, ?Carbon $promisedAt): Membership public function handle(Membership $membership, Activity $activity, ?Subactivity $subactivity, ?Carbon $promisedAt): Membership
{ {
$from = now()->startOfDay();
$membership->update([ $membership->update([
'activity_id' => $activity->id, 'activity_id' => $activity->id,
'subactivity_id' => $subactivity ? $subactivity->id : null, '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( $this->handle(
$membership, $membership,
@ -67,6 +64,6 @@ class MembershipUpdateAction
ResyncAction::dispatch(); ResyncAction::dispatch();
return redirect()->back(); return response()->json([]);
} }
} }

View File

@ -2,6 +2,10 @@
namespace App\Membership; 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; use Illuminate\Http\Resources\Json\JsonResource;
/** /**
@ -28,6 +32,33 @@ class MembershipResource extends JsonResource
'human_date' => $this->from->format('d.m.Y'), 'human_date' => $this->from->format('d.m.Y'),
'promised_at' => $this->promised_at?->format('Y-m-d'), 'promised_at' => $this->promised_at?->format('Y-m-d'),
'is_active' => $this->isActive(), 'is_active' => $this->isActive(),
'links' => [
'update' => route('membership.update', ['membership' => $this->getModel()]),
'destroy' => route('membership.destroy', ['membership' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
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,
],
]; ];
} }
} }

View File

@ -63,10 +63,11 @@ class MembershipFactory extends Factory
public function in(string $activity, int $activityNamiId, ?string $subactivity = null, ?int $subactivityNamiId = null): self 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) { if ($subactivity) {
$instance = $instance->for(Subactivity::factory()->name($subactivity)->inNami($subactivityNamiId)); $instance = $instance->for(Subactivity::factory()->name($subactivity)->inNami($subactivityNamiId)->hasAttached($activityModel));
} }
return $instance; return $instance;

View File

@ -1,10 +1,7 @@
<template> <template>
<div v-tooltip="longLabel" class="flex space-x-2 items-center"> <div v-tooltip="longLabel" class="flex space-x-2 items-center">
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" <div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? (dark ? 'border-green-500' : 'border-green-700') : dark ? 'border-red-500' : 'border-red-700'">
:class="value ? (dark ? 'border-green-500' : 'border-green-700') : dark ? 'border-red-500' : 'border-red-700'"> <ui-sprite :src="value ? 'check' : 'close'" :class="value ? (dark ? 'text-green-600' : 'text-green-800') : dark ? 'text-red-600' : 'text-red-800'" class="w-3 h-3 flex-none"></ui-sprite>
<ui-sprite :src="value ? 'check' : 'close'"
:class="value ? (dark ? 'text-green-600' : 'text-green-800') : dark ? 'text-red-600' : 'text-red-800'"
class="w-3 h-3 flex-none"></ui-sprite>
</div> </div>
<div class="text-gray-400 text-xs" v-text="label"></div> <div class="text-gray-400 text-xs" v-text="label"></div>
</div> </div>
@ -18,8 +15,8 @@ export default {
type: Boolean, type: Boolean,
}, },
label: { label: {
required: true,
type: String, type: String,
default: () => '',
}, },
longLabel: { longLabel: {
default: function () { default: function () {

View File

@ -2,24 +2,24 @@
<div class="sidebar flex flex-col group is-bright"> <div class="sidebar flex flex-col group is-bright">
<page-header title="Mitgliedschaften" @close="$emit('close')"> <page-header title="Mitgliedschaften" @close="$emit('close')">
<template #toolbar> <template #toolbar>
<page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue <page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue Mitgliedschaft</page-toolbar-button>
Mitgliedschaft</page-toolbar-button> <page-toolbar-button v-if="single !== null" color="primary" icon="undo" @click.prevent="single = null">Zurück</page-toolbar-button>
<page-toolbar-button v-if="single !== null" color="primary" icon="undo"
@click.prevent="cancel">Zurück</page-toolbar-button>
</template> </template>
</page-header> </page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit"> <form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-select id="group_id" v-model="single.group_id" name="group_id" :options="groups" label="Gruppierung" <f-select id="group_id" v-model="single.group_id" name="group_id" :options="data.meta.groups" label="Gruppierung" required></f-select>
required></f-select> <f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="data.meta.activities" label="Tätigkeit" required></f-select>
<f-select id="activity_id" v-model="single.activity_id" name="activity_id" :options="activities" <f-select
label="Tätigkeit" required></f-select> v-if="single.activity_id"
<f-select v-if="single.activity_id" id="subactivity_id" v-model="single.subactivity_id" name="subactivity_id" id="subactivity_id"
:options="subactivities[single.activity_id]" label="Untertätigkeit" size="sm"></f-select> v-model="single.subactivity_id"
<f-switch id="has_promise" :model-value="single.promised_at !== null" label="Hat Versprechen" name="subactivity_id"
@update:modelValue="single.promised_at = $event ? '2000-02-02' : null"></f-switch> :options="data.meta.subactivities[single.activity_id]"
<f-text v-show="single.promised_at !== null" id="promised_at" v-model="single.promised_at" type="date" label="Untertätigkeit"
label="Versprechensdatum" size="sm"></f-text> ></f-select>
<f-switch id="has_promise" :model-value="single.promised_at !== null" label="Hat Versprechen" @update:modelValue="single.promised_at = $event ? '2000-02-02' : null"></f-switch>
<f-text v-show="single.promised_at !== null" id="promised_at" v-model="single.promised_at" type="date" label="Versprechensdatum" size="sm"></f-text>
<button type="submit" class="btn btn-primary">Absenden</button> <button type="submit" class="btn btn-primary">Absenden</button>
</form> </form>
@ -33,18 +33,14 @@
<th></th> <th></th>
</thead> </thead>
<tr v-for="(membership, index) in value.memberships" :key="index"> <tr v-for="(membership, index) in data.data" :key="index">
<td v-text="membership.activity_name"></td> <td v-text="membership.activity_name"></td>
<td v-text="membership.subactivity_name"></td> <td v-text="membership.subactivity_name"></td>
<td v-text="membership.human_date"></td> <td v-text="membership.human_date"></td>
<td><ui-boolean-display :value="membership.is_active" dark></ui-boolean-display></td> <td><ui-boolean-display :value="membership.is_active" dark></ui-boolean-display></td>
<td class="flex"> <td class="flex space-x-1">
<a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent=" <a href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(membership)"><ui-sprite src="pencil"></ui-sprite></a>
single = membership; <a href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(membership)"><ui-sprite src="trash"></ui-sprite></a>
mode = 'edit';
"><ui-sprite src="pencil"></ui-sprite></a>
<i-link href="#" class="inline-flex btn btn-danger btn-sm"
@click.prevent="remove(membership)"><ui-sprite src="trash"></ui-sprite></i-link>
</td> </td>
</tr> </tr>
</table> </table>
@ -52,70 +48,47 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import {ref, inject, onBeforeMount} from 'vue';
props: { const axios = inject('axios');
value: {},
activities: {},
subactivities: {},
groups: {},
},
data: function () {
return {
mode: null,
single: null,
};
},
computed: { const props = defineProps({
def() { value: {
return { type: Object,
group_id: this.value.group_id, required: true,
activity_id: null,
subactivity_id: null,
promised_at: null,
};
},
}, },
});
methods: { const single = ref(null);
create() { const data = ref({
this.mode = 'create'; meta: {},
this.single = { ...this.def }; data: [],
}, });
cancel() {
this.mode = this.single = null;
},
remove(membership) {
this.$inertia.delete(`/member/${this.value.id}/membership/${membership.id}`);
},
accept(payment) { async function reload() {
this.$inertia.patch(`/member/${this.value.id}/payment/${payment.id}`, { ...payment, status_id: 3 }); data.value = (await axios.post(props.value.links.membership_index)).data;
}, }
openLink(link) { onBeforeMount(async () => {
if (link.disabled) { await reload();
return; });
}
window.open(link.href); function create() {
}, single.value = JSON.parse(JSON.stringify(data.value.meta.default));
}
submit() { function edit(membership) {
var _self = this; single.value = JSON.parse(JSON.stringify(membership));
}
var options = { async function submit() {
onSuccess() { single.value.id ? await axios.patch(single.value.links.update, single.value) : await axios.post(data.value.meta.links.store, single.value);
_self.single = null; await reload();
_self.mode = null; single.value = null;
}, }
};
this.mode === 'create' async function remove(membership) {
? this.$inertia.post(`/member/${this.value.id}/membership`, this.single, options) await axios.delete(membership.links.destroy);
: this.$inertia.patch(`/member/${this.value.id}/membership/${this.single.id}`, this.single, options); await reload();
}, }
},
};
</script> </script>

View File

@ -1,20 +1,20 @@
<template> <template>
<div class="sidebar flex flex-col group is-bright"> <div class="sidebar flex flex-col group is-bright">
<page-header @close="$emit('close')" title="Zahlungen"> <page-header title="Zahlungen" @close="$emit('close')">
<template #toolbar> <template #toolbar>
<page-toolbar-button @click.prevent="create" color="primary" icon="plus" v-if="single === null">Neue Zahlung</page-toolbar-button> <page-toolbar-button v-if="single === null" color="primary" icon="plus" @click.prevent="create">Neue Zahlung</page-toolbar-button>
<page-toolbar-button @click.prevent="cancel" color="primary" icon="undo" v-if="single !== null">Zurück</page-toolbar-button> <page-toolbar-button v-if="single !== null" color="primary" icon="undo" @click.prevent="cancel">Zurück</page-toolbar-button>
</template> </template>
</page-header> </page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit"> <form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-text id="nr" v-model="single.nr" label="Jahr" required></f-text> <f-text id="nr" v-model="single.nr" label="Jahr" required></f-text>
<f-select id="subscription_id" name="subscription_id" :options="subscriptions" v-model="single.subscription_id" label="Beitrag" required></f-select> <f-select id="subscription_id" v-model="single.subscription_id" name="subscription_id" :options="subscriptions" label="Beitrag" required></f-select>
<f-select id="status_id" name="status_id" :options="statuses" v-model="single.status_id" label="Status" required></f-select> <f-select id="status_id" v-model="single.status_id" name="status_id" :options="statuses" label="Status" required></f-select>
<button type="submit" class="btn btn-primary">Absenden</button> <button type="submit" class="btn btn-primary">Absenden</button>
</form> </form>
<div class="grow" v-else> <div v-else class="grow">
<table class="custom-table custom-table-light custom-table-sm text-sm"> <table class="custom-table custom-table-light custom-table-sm text-sm">
<thead> <thead>
<th>Nr</th> <th>Nr</th>
@ -30,28 +30,28 @@
<td class="flex"> <td class="flex">
<a <a
href="#" href="#"
class="inline-flex btn btn-warning btn-sm"
@click.prevent=" @click.prevent="
single = payment; single = payment;
mode = 'edit'; mode = 'edit';
" "
class="inline-flex btn btn-warning btn-sm"
><ui-sprite src="pencil"></ui-sprite ><ui-sprite src="pencil"></ui-sprite
></a> ></a>
<i-link v-show="!payment.is_accepted" href="#" @click.prevent="accept(payment)" class="inline-flex btn btn-success btn-sm"><ui-sprite src="check"></ui-sprite></i-link> <i-link v-show="!payment.is_accepted" href="#" class="inline-flex btn btn-success btn-sm" @click.prevent="accept(payment)"><ui-sprite src="check"></ui-sprite></i-link>
<i-link href="#" @click.prevent="remove(payment)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></i-link> <i-link href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(payment)"><ui-sprite src="trash"></ui-sprite></i-link>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="flex flex-col pb-6 px-6"> <div class="flex flex-col pb-6 px-6">
<a <a
href="#"
@click.prevent="openLink(link)"
:class="{disabled: link.disabled}"
target="_BLANK"
v-for="(link, index) in value.payment_links" v-for="(link, index) in value.payment_links"
:key="index" :key="index"
href="#"
:class="{disabled: link.disabled}"
target="_BLANK"
class="mt-1 text-center btn btn-primary" class="mt-1 text-center btn btn-primary"
@click.prevent="openLink(link)"
v-text="link.label" v-text="link.label"
></a> ></a>
</div> </div>
@ -60,6 +60,12 @@
<script> <script>
export default { export default {
props: {
value: {},
subscriptions: {},
statuses: {},
},
data: function () { data: function () {
return { return {
mode: null, mode: null,
@ -107,11 +113,5 @@ export default {
}); });
}, },
}, },
props: {
value: {},
subscriptions: {},
statuses: {},
},
}; };
</script> </script>

View File

@ -77,7 +77,7 @@
<ui-label :value="member.pending_payment" fallback="---"></ui-label> <ui-label :value="member.pending_payment" fallback="---"></ui-label>
</td> </td>
<td> <td>
<actions :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"></actions> <actions :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)"></actions>
</td> </td>
</tr> </tr>
</table> </table>
@ -93,7 +93,7 @@
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" <ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment"
fallback=""></ui-label> fallback=""></ui-label>
</div> </div>
<actions class="mt-2" :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"> <actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @remove="remove(member)">
</actions> </actions>
<div class="absolute right-0 top-0 h-full flex items-center mr-2"> <div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" <i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron"
@ -106,12 +106,11 @@
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination> <ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div> </div>
<member-payments v-if="single !== null && sidebar === 'payment.index'" :subscriptions="meta.subscriptions" <member-payments v-if="single !== null && single.type === 'payment'" :subscriptions="meta.subscriptions"
:statuses="meta.statuses" :value="data[single]" @close="closeSidebar"></member-payments> :statuses="meta.statuses" :value="single.model" @close="closeSidebar"></member-payments>
<member-memberships v-if="single !== null && sidebar === 'membership.index'" :groups="meta.groups" <member-memberships v-if="single !== null && single.type === 'membership'" :activities="meta.formActivities"
:activities="meta.formActivities" :subactivities="meta.formSubactivities" :value="data[single]" :subactivities="meta.formSubactivities" :value="single.model" @close="closeSidebar"></member-memberships>
@close="closeSidebar"></member-memberships> <member-courses v-if="single !== null && single.type === 'courses'" :courses="meta.courses" :value="single.model"
<member-courses v-if="single !== null && sidebar === 'courses.index'" :courses="meta.courses" :value="data[single]"
@close="closeSidebar"></member-courses> @close="closeSidebar"></member-courses>
</page-layout> </page-layout>
</template> </template>
@ -125,7 +124,6 @@ import Actions from './index/Actions.vue';
import { indexProps, useIndex } from '../../composables/useIndex.js'; import { indexProps, useIndex } from '../../composables/useIndex.js';
import { ref, defineProps } from 'vue'; import { ref, defineProps } from 'vue';
const sidebar = ref(null);
const single = ref(null); const single = ref(null);
const deleting = ref(null); const deleting = ref(null);
@ -147,12 +145,13 @@ async function remove(member) {
.catch(() => (deleting.value = null)); .catch(() => (deleting.value = null));
} }
function openSidebar(index, name) { function openSidebar(type, model) {
single.value = index; single.value = {
sidebar.value = name; type: type,
model: model,
};
} }
function closeSidebar() { function closeSidebar() {
single.value = null; single.value = null;
sidebar.value = null;
} }
</script> </script>

View File

@ -1,16 +1,14 @@
<template> <template>
<div class="flex space-x-1"> <div class="flex space-x-1">
<i-link :href="member.links.show" class="inline-flex btn btn-primary btn-sm" v-tooltip="`Details`"><ui-sprite src="eye"></ui-sprite></i-link> <i-link v-tooltip="`Details`" :href="member.links.show" class="inline-flex btn btn-primary btn-sm"><ui-sprite src="eye"></ui-sprite></i-link>
<i-link :href="`/member/${member.id}/edit`" class="inline-flex btn btn-warning btn-sm" v-tooltip="`Bearbeiten`"><ui-sprite src="pencil"></ui-sprite></i-link> <i-link v-tooltip="`Bearbeiten`" :href="`/member/${member.id}/edit`" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<a href="#" v-tooltip="`Zahlungen`" v-show="hasModule('bill')" @click.prevent="$emit('sidebar', 'payment.index')" class="inline-flex btn btn-info btn-sm" <a v-show="hasModule('bill')" v-tooltip="`Zahlungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'payment')"><ui-sprite src="money"></ui-sprite></a>
><ui-sprite src="money"></ui-sprite <a v-show="hasModule('courses')" v-tooltip="`Ausbildungen`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'courses')"
></a>
<a href="#" v-tooltip="`Ausbildungen`" v-show="hasModule('courses')" @click.prevent="$emit('sidebar', 'courses.index')" class="inline-flex btn btn-info btn-sm"
><ui-sprite src="course"></ui-sprite ><ui-sprite src="course"></ui-sprite
></a> ></a>
<a href="#" v-tooltip="`Mitgliedschaften`" @click.prevent="$emit('sidebar', 'membership.index')" class="inline-flex btn btn-info btn-sm"><ui-sprite src="user"></ui-sprite></a> <a v-tooltip="`Mitgliedschaften`" href="#" class="inline-flex btn btn-info btn-sm" @click.prevent="$emit('sidebar', 'membership')"><ui-sprite src="user"></ui-sprite></a>
<a :href="member.efz_link" v-show="member.efz_link" class="inline-flex btn btn-info btn-sm" v-tooltip="`EFZ Formular`"><ui-sprite src="report"></ui-sprite></a> <a v-show="member.efz_link" v-tooltip="`EFZ Formular`" :href="member.efz_link" class="inline-flex btn btn-info btn-sm"><ui-sprite src="report"></ui-sprite></a>
<a href="#" @click.prevent="$emit('remove')" class="inline-flex btn btn-danger btn-sm" v-tooltip="`Entfernen`"><ui-sprite src="trash"></ui-sprite></a> <a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="$emit('remove')"><ui-sprite src="trash"></ui-sprite></a>
</div> </div>
</template> </template>

View File

@ -35,6 +35,7 @@ use App\Member\Actions\MemberResyncAction;
use App\Member\Actions\MemberShowAction; use App\Member\Actions\MemberShowAction;
use App\Member\Actions\SearchAction; use App\Member\Actions\SearchAction;
use App\Member\MemberController; use App\Member\MemberController;
use App\Membership\Actions\ApiIndexAction;
use App\Membership\Actions\ApiListAction; use App\Membership\Actions\ApiListAction;
use App\Membership\Actions\MembershipDestroyAction; use App\Membership\Actions\MembershipDestroyAction;
use App\Membership\Actions\MembershipStoreAction; 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/member/search', SearchAction::class)->name('member.search');
Route::post('/api/membership/member-list', ApiListAction::class)->name('membership.member-list'); 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/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::get('/initialize', InitializeFormAction::class)->name('initialize.form');
Route::post('/initialize', InitializeAction::class)->name('initialize.store'); Route::post('/initialize', InitializeAction::class)->name('initialize.store');
Route::resource('member', MemberController::class)->except('show', 'destroy'); Route::resource('member', MemberController::class)->except('show', 'destroy');
@ -72,9 +74,9 @@ Route::group(['middleware' => 'auth:web'], function (): void {
->name('member.singlepdf'); ->name('member.singlepdf');
Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create'); Route::get('/sendpayment', [SendpaymentController::class, 'create'])->name('sendpayment.create');
Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf'); Route::get('/sendpayment/pdf', [SendpaymentController::class, 'send'])->name('sendpayment.pdf');
Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('membership.store'); Route::post('/member/{member}/membership', MembershipStoreAction::class)->name('member.membership.store');
Route::patch('/member/{member}/membership/{membership}', MembershipUpdateAction::class)->name('membership.store'); Route::patch('/membership/{membership}', MembershipUpdateAction::class)->name('membership.update');
Route::delete('/member/{member}/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy'); Route::delete('/membership/{membership}', MembershipDestroyAction::class)->name('membership.destroy');
Route::resource('member.course', CourseController::class); Route::resource('member.course', CourseController::class);
Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz'); Route::get('/member/{member}/efz', ShowEfzDocumentAction::class)->name('efz');
Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync'); Route::get('/member/{member}/resync', MemberResyncAction::class)->name('member.resync');

View File

@ -8,8 +8,6 @@ use App\Member\Member;
use App\Member\Membership; use App\Member\Membership;
use App\Payment\Payment; use App\Payment\Payment;
use App\Subactivity; use App\Subactivity;
use Carbon\Carbon;
use Generator;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\RequestFactories\Child; use Tests\RequestFactories\Child;
use Tests\TestCase; use Tests\TestCase;
@ -22,7 +20,7 @@ class IndexTest extends TestCase
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$group = Group::factory()->create(); $group = Group::factory()->create();
Member::factory()->defaults()->for($group)->create([ $member = Member::factory()->defaults()->for($group)->create([
'firstname' => '::firstname::', 'firstname' => '::firstname::',
'address' => 'Kölner Str 3', 'address' => 'Kölner Str 3',
'zip' => 33333, 'zip' => 33333,
@ -36,6 +34,8 @@ class IndexTest extends TestCase
$this->assertInertiaHas(false, $response, 'data.data.0.has_nami'); $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('Kölner Str 3, 33333 Hilden', $response, 'data.data.0.full_address');
$this->assertInertiaHas($group->id, $response, 'data.data.0.group_id'); $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 public function testFieldsCanBeNull(): void
@ -82,29 +82,6 @@ class IndexTest extends TestCase
$this->assertInertiaHas(false, $response, 'data.data.2.is_leader'); $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 public function testItHasNoEfzLinkWhenAddressIsMissing(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
@ -155,28 +132,6 @@ class IndexTest extends TestCase
$this->assertInertiaHas('€ Mitglied', $response, "data.meta.formActivities.{$activity->id}"); $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 public function testItReturnsPayments(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();

View File

@ -44,9 +44,9 @@ class DestroyTest extends TestCase
->inNami(6) ->inNami(6)
->create(); ->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->assertEquals(1506, $member->fresh()->version);
$this->assertDatabaseMissing('memberships', [ $this->assertDatabaseMissing('memberships', [
'member_id' => $member->id, 'member_id' => $member->id,
@ -65,9 +65,8 @@ class DestroyTest extends TestCase
->inNami(6) ->inNami(6)
->create(); ->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', [ $this->assertDatabaseMissing('memberships', [
'member_id' => $member->id, 'member_id' => $member->id,
]); ]);

View File

@ -0,0 +1,68 @@
<?php
namespace Tests\Feature\Membership;
use App\Group;
use App\Member\Member;
use App\Member\Membership;
use Generator;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class IndexTest extends TestCase
{
use DatabaseTransactions;
public function testItShowsActivityAndSubactivityNamesOfMember(): void
{
$this->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);
}
}

View File

@ -37,11 +37,11 @@ class UpdateTest extends TestCase
$membership = $member->memberships->first(); $membership = $member->memberships->first();
$response = $this->from('/member')->patch( $response = $this->from('/member')->patch(
"/member/{$member->id}/membership/{$membership->id}", "/membership/{$membership->id}",
MembershipRequestFactory::new()->promise(now())->in($membership->activity, $membership->subactivity)->create() MembershipRequestFactory::new()->promise(now())->in($membership->activity, $membership->subactivity)->create()
); );
$response->assertRedirect('/member'); $response->assertOk();
$this->assertDatabaseHas('memberships', [ $this->assertDatabaseHas('memberships', [
'member_id' => $member->id, 'member_id' => $member->id,
'activity_id' => $activity->id, 'activity_id' => $activity->id,