Add Membership management
This commit is contained in:
parent
a134be5f5b
commit
2e9ab78203
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Group\Actions;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Group;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class ListAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function asController(): Response
|
||||||
|
{
|
||||||
|
session()->put('menu', 'group');
|
||||||
|
session()->put('title', 'Gruppen');
|
||||||
|
$activities = Activity::with('subactivities')->get();
|
||||||
|
|
||||||
|
return Inertia::render('group/Index', [
|
||||||
|
'activities' => $activities->pluck('name', 'id'),
|
||||||
|
'subactivities' => $activities->mapWithKeys(fn (Activity $activity) => [$activity->id => $activity->subactivities()->pluck('name', 'id')]),
|
||||||
|
'groups' => Group::pluck('name', 'id'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,16 +17,17 @@ class SearchAction
|
||||||
/**
|
/**
|
||||||
* @return LengthAwarePaginator<int, Member>
|
* @return LengthAwarePaginator<int, Member>
|
||||||
*/
|
*/
|
||||||
public function handle(FilterScope $filter): LengthAwarePaginator
|
public function handle(FilterScope $filter, int $perPage): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
return Member::search($filter->search)->query(fn ($q) => $q->select('*')
|
return Member::search($filter->search)->query(
|
||||||
->withFilter($filter)
|
fn ($q) => $q->select('*')
|
||||||
->ordered()
|
->withFilter($filter)
|
||||||
)->paginate(15);
|
->ordered()
|
||||||
|
)->paginate($perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function asController(ActionRequest $request): AnonymousResourceCollection
|
public function asController(ActionRequest $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
return MemberResource::collection($this->handle(FilterScope::fromRequest($request->input('filter', ''))));
|
return MemberResource::collection($this->handle(FilterScope::fromRequest($request->input('filter', '')), $request->input('per_page', 15)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,10 +67,11 @@ class FilterScope extends Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
if (false === $this->hasFullAddress) {
|
if (false === $this->hasFullAddress) {
|
||||||
$query->where(fn ($q) => $q
|
$query->where(
|
||||||
->orWhere('address', '')->orWhereNull('address')
|
fn ($q) => $q
|
||||||
->orWhere('zip', '')->orWhereNull('zip')
|
->orWhere('address', '')->orWhereNull('address')
|
||||||
->orWhere('location', '')->orWhereNull('location')
|
->orWhere('zip', '')->orWhereNull('zip')
|
||||||
|
->orWhere('location', '')->orWhereNull('location')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ class MemberResource extends JsonResource
|
||||||
'subscription' => new SubscriptionResource($this->whenLoaded('subscription')),
|
'subscription' => new SubscriptionResource($this->whenLoaded('subscription')),
|
||||||
'gender_id' => $this->gender_id,
|
'gender_id' => $this->gender_id,
|
||||||
'gender_name' => $this->gender?->name ?: 'keine Angabe',
|
'gender_name' => $this->gender?->name ?: 'keine Angabe',
|
||||||
'fullname' => ($this->gender ? $this->gender->salutation.' ' : '').$this->fullname,
|
'fullname' => ($this->gender ? $this->gender->salutation . ' ' : '') . $this->fullname,
|
||||||
'further_address' => $this->further_address,
|
'further_address' => $this->further_address,
|
||||||
'work_phone' => $this->work_phone,
|
'work_phone' => $this->work_phone,
|
||||||
'mobile_phone' => $this->mobile_phone,
|
'mobile_phone' => $this->mobile_phone,
|
||||||
|
@ -74,7 +74,7 @@ class MemberResource extends JsonResource
|
||||||
'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')),
|
'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')),
|
||||||
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
|
'nationality' => new NationalityResource($this->whenLoaded('nationality')),
|
||||||
|
|
|
@ -53,6 +53,14 @@ class Membership extends Model
|
||||||
return $this->belongsTo(Subactivity::class);
|
return $this->belongsTo(Subactivity::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Member, self>
|
||||||
|
*/
|
||||||
|
public function member(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Member::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Builder<Membership> $query
|
* @param Builder<Membership> $query
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Membership\Actions;
|
||||||
|
|
||||||
|
use App\Member\Membership;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class ApiListAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function asController(ActionRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json(Membership::active()->where([
|
||||||
|
'group_id' => $request->group_id,
|
||||||
|
'activity_id' => $request->activity_id,
|
||||||
|
'subactivity_id' => $request->subactivity_id,
|
||||||
|
])->pluck('member_id'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Membership\Actions;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Group;
|
||||||
|
use App\Lib\JobMiddleware\WithJobState;
|
||||||
|
use App\Lib\Queue\TracksJob;
|
||||||
|
use App\Maildispatcher\Actions\ResyncAction;
|
||||||
|
use App\Member\Member;
|
||||||
|
use App\Member\Membership;
|
||||||
|
use App\Setting\NamiSettings;
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class SyncAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
use TracksJob;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'group_id' => 'required|numeric|exists:groups,id',
|
||||||
|
'activity_id' => 'required|numeric|exists:activities,id',
|
||||||
|
'subactivity_id' => 'required|numeric|exists:subactivities,id',
|
||||||
|
'members' => 'array',
|
||||||
|
'members.*' => 'numeric|exists:members,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, int> $members
|
||||||
|
*/
|
||||||
|
public function handle(Group $group, Activity $activity, Subactivity $subactivity, array $members): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () use ($activity, $subactivity, $group, $members) {
|
||||||
|
$attributes = [
|
||||||
|
'group_id' => $group->id,
|
||||||
|
'activity_id' => $activity->id,
|
||||||
|
'subactivity_id' => $subactivity->id,
|
||||||
|
];
|
||||||
|
|
||||||
|
Membership::where($attributes)->active()->whereNotIn('member_id', $members)->get()
|
||||||
|
->each(fn ($membership) => MembershipDestroyAction::run($membership->member, $membership, app(NamiSettings::class)));
|
||||||
|
|
||||||
|
collect($members)
|
||||||
|
->except(Membership::where($attributes)->active()->pluck('member_id'))
|
||||||
|
->map(fn ($memberId) => Member::findOrFail($memberId))
|
||||||
|
->each(fn ($member) => MembershipStoreAction::run(
|
||||||
|
$member,
|
||||||
|
$activity,
|
||||||
|
$subactivity,
|
||||||
|
$group,
|
||||||
|
null,
|
||||||
|
app(NamiSettings::class),
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
ResyncAction::dispatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function asController(ActionRequest $request): void
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array{members: array<int, int>, group_id: int, activity_id: int, subactivity_id: int}
|
||||||
|
*/
|
||||||
|
$input = $request->validated();
|
||||||
|
|
||||||
|
$this->startJob(
|
||||||
|
Group::findOrFail($input['group_id']),
|
||||||
|
Activity::findOrFail($input['activity_id']),
|
||||||
|
Subactivity::findOrFail($input['subactivity_id']),
|
||||||
|
$input['members'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $parameters
|
||||||
|
*/
|
||||||
|
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
|
||||||
|
{
|
||||||
|
return $jobState
|
||||||
|
->before('Gruppen werden aktualisiert')
|
||||||
|
->after('Gruppen aktualisiert')
|
||||||
|
->failed('Aktualisieren von Gruppen fehlgeschlagen');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jobChannel(): string
|
||||||
|
{
|
||||||
|
return 'group';
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
|
@ -3,14 +3,14 @@
|
||||||
<div class="text-sm text-gray-500" v-html="desc"></div>
|
<div class="text-sm text-gray-500" v-html="desc"></div>
|
||||||
<div class="-mx-1 items-baseline" :class="{hidden: value.last_page == 1, flex: value.last_page > 1}">
|
<div class="-mx-1 items-baseline" :class="{hidden: value.last_page == 1, flex: value.last_page > 1}">
|
||||||
<div class="pl-1 pr-3 text-gray-500 text-sm">Seite:</div>
|
<div class="pl-1 pr-3 text-gray-500 text-sm">Seite:</div>
|
||||||
<div class="px-1" v-for="(link, index) in links" :key="index">
|
<div v-for="(link, index) in links" :key="index" class="px-1">
|
||||||
<button
|
<button
|
||||||
href="#"
|
|
||||||
@click.prevent="goto(link)"
|
|
||||||
class="rounded text-sm w-8 h-8 text-primary-100 flex items-center justify-center leading-none shadow"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
v-text="link.page"
|
href="#"
|
||||||
|
class="rounded text-sm w-8 h-8 text-primary-100 flex items-center justify-center leading-none shadow"
|
||||||
:class="{'bg-primary-700': link.current, 'bg-primary-900': !link.current}"
|
:class="{'bg-primary-700': link.current, 'bg-primary-900': !link.current}"
|
||||||
|
@click.prevent="goto(link)"
|
||||||
|
v-text="link.page"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,22 +32,6 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
goto(page) {
|
|
||||||
if (this.$attrs.onReload) {
|
|
||||||
this.$emit('reload', page.page);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var params = new URLSearchParams(window.location.search);
|
|
||||||
params.set('page', page.page);
|
|
||||||
|
|
||||||
this.$inertia.visit(window.location.pathname + '?' + params.toString(), {
|
|
||||||
only: this.only,
|
|
||||||
preserveState: this.preserve,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
links() {
|
links() {
|
||||||
|
@ -69,5 +53,21 @@ export default {
|
||||||
return `${this.value.from} - ${this.value.to} von ${this.value.total} Einträgen`;
|
return `${this.value.from} - ${this.value.to} von ${this.value.total} Einträgen`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
goto(page) {
|
||||||
|
if (this.$attrs.onReload) {
|
||||||
|
this.$emit('reload', page.page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
params.set('page', page.page);
|
||||||
|
|
||||||
|
this.$inertia.visit(window.location.pathname + '?' + params.toString(), {
|
||||||
|
only: this.only,
|
||||||
|
preserveState: this.preserve,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {ref, computed, onBeforeUnmount} from 'vue';
|
import {ref, computed, onBeforeUnmount} from 'vue';
|
||||||
import {router} from '@inertiajs/vue3';
|
import {router} from '@inertiajs/vue3';
|
||||||
import {useToast} from 'vue-toastification';
|
import useQueueEvents from './useQueueEvents.js';
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
export function useIndex(props, siteName) {
|
export function useIndex(props, siteName) {
|
||||||
|
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
|
||||||
const rawProps = JSON.parse(JSON.stringify(props));
|
const rawProps = JSON.parse(JSON.stringify(props));
|
||||||
const inner = {
|
const inner = {
|
||||||
data: ref(rawProps.data),
|
data: ref(rawProps.data),
|
||||||
|
@ -61,22 +61,8 @@ export function useIndex(props, siteName) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleJobEvent(event, type = 'success') {
|
startListener(;
|
||||||
if (event.message) {
|
onBeforeUnmount(() => stopListener());
|
||||||
toast[type](event.message);
|
|
||||||
}
|
|
||||||
if (event.reload) {
|
|
||||||
reload(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Echo.channel('jobs').listen('\\App\\Lib\\Events\\ClientMessage', (e) => handleJobEvent(e));
|
|
||||||
window.Echo.channel(siteName)
|
|
||||||
.listen('\\App\\Lib\\Events\\JobStarted', (e) => handleJobEvent(e))
|
|
||||||
.listen('\\App\\Lib\\Events\\JobFinished', (e) => handleJobEvent(e))
|
|
||||||
.listen('\\App\\Lib\\Events\\JobFailed', (e) => handleJobEvent(e, 'error'));
|
|
||||||
onBeforeUnmount(() => window.Echo.leave(siteName));
|
|
||||||
onBeforeUnmount(() => window.Echo.leave('jobs'));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: inner.data,
|
data: inner.data,
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {useToast} from 'vue-toastification';
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
function handleJobEvent(event, type = 'success', reloadCallback) {
|
||||||
|
if (event.message) {
|
||||||
|
toast[type](event.message);
|
||||||
|
}
|
||||||
|
if (event.reload) {
|
||||||
|
reloadCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (siteName, reloadCallback) {
|
||||||
|
return {
|
||||||
|
startListener: function () {
|
||||||
|
window.Echo.channel('jobs').listen('\\App\\Lib\\Events\\ClientMessage', (e) => handleJobEvent(e, 'success', reloadCallback));
|
||||||
|
window.Echo.channel(siteName)
|
||||||
|
.listen('\\App\\Lib\\Events\\JobStarted', (e) => handleJobEvent(e, 'success', reloadCallback))
|
||||||
|
.listen('\\App\\Lib\\Events\\JobFinished', (e) => handleJobEvent(e, 'success', reloadCallback))
|
||||||
|
.listen('\\App\\Lib\\Events\\JobFailed', (e) => handleJobEvent(e, 'error', reloadCallback));
|
||||||
|
},
|
||||||
|
stopListener() {
|
||||||
|
window.Echo.leave(siteName);
|
||||||
|
window.Echo.leave('jobs');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -2,26 +2,25 @@
|
||||||
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
|
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
|
||||||
|
|
||||||
<!-- ******************************** Sidebar ******************************** -->
|
<!-- ******************************** Sidebar ******************************** -->
|
||||||
<div
|
<div class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
|
||||||
class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
|
|
||||||
:class="{
|
:class="{
|
||||||
'-left-[14rem]': !menuStore.isShifted,
|
'-left-[14rem]': !menuStore.isShifted,
|
||||||
'left-0': menuStore.isShifted,
|
'left-0': menuStore.isShifted,
|
||||||
}"
|
}">
|
||||||
>
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
|
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
|
||||||
<v-link href="/member" menu="member" icon="user">Mitglieder</v-link>
|
<v-link href="/member" menu="member" icon="user">Mitglieder</v-link>
|
||||||
<v-link href="/subscription" v-show="hasModule('bill')" menu="subscription" icon="money">Beiträge</v-link>
|
<v-link v-show="hasModule('bill')" href="/subscription" menu="subscription" icon="money">Beiträge</v-link>
|
||||||
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
|
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
|
||||||
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
|
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
|
||||||
|
<v-link href="/group" menu="group" icon="group">Gruppen</v-link>
|
||||||
<v-link href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</v-link>
|
<v-link href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</v-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
|
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
|
||||||
<v-link @click.prevent="$inertia.post('/logout')" icon="logout" href="/logout">Abmelden</v-link>
|
<v-link icon="logout" href="/logout" @click.prevent="$inertia.post('/logout')">Abmelden</v-link>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" @click.prevent="menuStore.hide()" v-if="menuStore.hideable" class="absolute right-0 top-0 mr-2 mt-2">
|
<a v-if="menuStore.hideable" href="#" class="absolute right-0 top-0 mr-2 mt-2" @click.prevent="menuStore.hide()">
|
||||||
<ui-sprite src="close" class="w-5 h-5 text-gray-300"></ui-sprite>
|
<ui-sprite src="close" class="w-5 h-5 text-gray-300"></ui-sprite>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,19 +30,19 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import VLink from './_VLink.vue';
|
import VLink from './_VLink.vue';
|
||||||
import {menuStore} from '../stores/menuStore.js';
|
import { menuStore } from '../stores/menuStore.js';
|
||||||
import VNotification from '../components/VNotification.vue';
|
import VNotification from '../components/VNotification.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
VNotification,
|
||||||
|
VLink,
|
||||||
|
},
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
menuStore: menuStore(),
|
menuStore: menuStore(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
VNotification,
|
|
||||||
VLink,
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filterMenu() {
|
filterMenu() {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<page-layout>
|
||||||
|
<template #right>
|
||||||
|
<f-save-button form="actionform"></f-save-button>
|
||||||
|
</template>
|
||||||
|
<form id="actionform" class="grow p-3" @submit.prevent="submit">
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<f-select :model-value="meta.activity_id" :options="props.activities" label="Tätigkeit" size="sm" name="activity_id" @update:model-value="setActivityId"></f-select>
|
||||||
|
<f-select
|
||||||
|
:model-value="meta.subactivity_id"
|
||||||
|
:options="props.subactivities[meta.activity_id]"
|
||||||
|
name="subactivity_id"
|
||||||
|
label="Untertätigkeit"
|
||||||
|
size="sm"
|
||||||
|
@update:model-value="reload('subactivity_id', $event)"
|
||||||
|
></f-select>
|
||||||
|
<f-select :model-value="meta.group_id" :options="props.groups" label="Gruppierung" size="sm" name="group_id" @update:model-value="reload('group_id', $event)"></f-select>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2 grid-cols-6 mt-4">
|
||||||
|
<f-switch v-for="member in members.data" :id="`member-${member.id}`" :key="member.id" v-model="selected" :value="member.id" :label="member.fullname" size="sm"></f-switch>
|
||||||
|
</div>
|
||||||
|
<div v-if="members.meta.last_page" class="px-6">
|
||||||
|
<ui-pagination class="mt-4" :value="members.meta" :only="['data']" @reload="reloadReal($event, false)"></ui-pagination>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</page-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js" setup>
|
||||||
|
import { onBeforeUnmount, ref, defineProps, reactive, inject } from 'vue';
|
||||||
|
import useQueueEvents from '../../composables/useQueueEvents.js';
|
||||||
|
const {startListener, stopListener} = useQueueEvents('group', () => null);
|
||||||
|
const axios = inject('axios');
|
||||||
|
|
||||||
|
startListener();
|
||||||
|
onBeforeUnmount(() => stopListener());
|
||||||
|
|
||||||
|
const meta = reactive({
|
||||||
|
activity_id: null,
|
||||||
|
subactivity_id: null,
|
||||||
|
group_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
activities: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { },
|
||||||
|
},
|
||||||
|
subactivities: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { },
|
||||||
|
},
|
||||||
|
groups: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const members = ref({ meta: {}, data: [] });
|
||||||
|
const selected = ref([]);
|
||||||
|
|
||||||
|
async function reload(key, v) {
|
||||||
|
meta[key] = v;
|
||||||
|
|
||||||
|
reloadReal(1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadReal(page, update) {
|
||||||
|
if (meta.activity_id && meta.subactivity_id && meta.group_id) {
|
||||||
|
const memberResponse = await axios.post('/api/member/search', {
|
||||||
|
page: page,
|
||||||
|
per_page: 80,
|
||||||
|
});
|
||||||
|
members.value = memberResponse.data;
|
||||||
|
|
||||||
|
if (update) {
|
||||||
|
const membershipResponse = await axios.post('/api/membership/member-list', meta);
|
||||||
|
selected.value = membershipResponse.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setActivityId(id) {
|
||||||
|
meta.subactivity_id = null;
|
||||||
|
await reload('activity_id', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
await axios.post('/api/membership/sync', {
|
||||||
|
...meta,
|
||||||
|
members: selected.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -15,6 +15,7 @@ use App\Contribution\Actions\ValidateAction as ContributionValidateAction;
|
||||||
use App\Course\Controllers\CourseController;
|
use App\Course\Controllers\CourseController;
|
||||||
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
|
||||||
use App\Efz\ShowEfzDocumentAction;
|
use App\Efz\ShowEfzDocumentAction;
|
||||||
|
use App\Group\Actions\ListAction;
|
||||||
use App\Initialize\Actions\InitializeAction;
|
use App\Initialize\Actions\InitializeAction;
|
||||||
use App\Initialize\Actions\InitializeFormAction;
|
use App\Initialize\Actions\InitializeFormAction;
|
||||||
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
||||||
|
@ -34,9 +35,11 @@ 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\ApiListAction;
|
||||||
use App\Membership\Actions\MembershipDestroyAction;
|
use App\Membership\Actions\MembershipDestroyAction;
|
||||||
use App\Membership\Actions\MembershipStoreAction;
|
use App\Membership\Actions\MembershipStoreAction;
|
||||||
use App\Membership\Actions\MembershipUpdateAction;
|
use App\Membership\Actions\MembershipUpdateAction;
|
||||||
|
use App\Membership\Actions\SyncAction;
|
||||||
use App\Payment\Actions\AllpaymentPageAction;
|
use App\Payment\Actions\AllpaymentPageAction;
|
||||||
use App\Payment\Actions\AllpaymentStoreAction;
|
use App\Payment\Actions\AllpaymentStoreAction;
|
||||||
use App\Payment\PaymentController;
|
use App\Payment\PaymentController;
|
||||||
|
@ -54,6 +57,8 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::post('/nami/get-search-layer', NamiGetSearchLayerAction::class)->name('nami.get-search-layer');
|
Route::post('/nami/get-search-layer', NamiGetSearchLayerAction::class)->name('nami.get-search-layer');
|
||||||
Route::post('/nami/search', NamiSearchAction::class)->name('nami.search');
|
Route::post('/nami/search', NamiSearchAction::class)->name('nami.search');
|
||||||
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/sync', SyncAction::class)->name('membership.sync');
|
||||||
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');
|
||||||
|
@ -98,4 +103,7 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::patch('/maildispatcher/{maildispatcher}', MaildispatcherUpdateAction::class)->name('maildispatcher.update');
|
Route::patch('/maildispatcher/{maildispatcher}', MaildispatcherUpdateAction::class)->name('maildispatcher.update');
|
||||||
Route::post('/maildispatcher', MaildispatcherStoreAction::class)->name('maildispatcher.store');
|
Route::post('/maildispatcher', MaildispatcherStoreAction::class)->name('maildispatcher.store');
|
||||||
Route::delete('/maildispatcher/{maildispatcher}', DestroyAction::class)->name('maildispatcher.destroy');
|
Route::delete('/maildispatcher/{maildispatcher}', DestroyAction::class)->name('maildispatcher.destroy');
|
||||||
|
|
||||||
|
// ----------------------------------- group -----------------------------------
|
||||||
|
Route::get('/group', ListAction::class)->name('group.index');
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\EndToEnd;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Group;
|
||||||
|
use App\Maildispatcher\Actions\ResyncAction;
|
||||||
|
use App\Member\Member;
|
||||||
|
use App\Member\Membership;
|
||||||
|
use App\Membership\Actions\MembershipDestroyAction;
|
||||||
|
use App\Membership\Actions\MembershipStoreAction;
|
||||||
|
use App\Membership\Actions\SyncAction;
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||||
|
use Illuminate\Support\Facades\Queue;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use Throwable;
|
||||||
|
use Zoomyboy\LaravelNami\Fakes\MembershipFake;
|
||||||
|
|
||||||
|
class SyncActionTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
use DatabaseMigrations;
|
||||||
|
|
||||||
|
public function testItFiresActionJobWhenUsingController(): void
|
||||||
|
{
|
||||||
|
Queue::fake();
|
||||||
|
$this->login()->loginNami()->withoutExceptionHandling();
|
||||||
|
$member = Member::factory()->defaults()->create();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
$subactivity = Subactivity::factory()->create();
|
||||||
|
$group = Group::factory()->create();
|
||||||
|
|
||||||
|
$this->postJson('/api/membership/sync', [
|
||||||
|
'members' => [$member->id],
|
||||||
|
'activity_id' => $activity->id,
|
||||||
|
'subactivity_id' => $subactivity->id,
|
||||||
|
'group_id' => $group->id,
|
||||||
|
]);
|
||||||
|
SyncAction::assertPushed(fn ($action, $params) => $params[0]->is($group) && $params[1]->is($activity) && $params[2]->is($subactivity) && $params[3][0] === $member->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCreatesAMembership(): void
|
||||||
|
{
|
||||||
|
MembershipDestroyAction::partialMock()->shouldReceive('handle')->never();
|
||||||
|
MembershipStoreAction::partialMock()->shouldReceive('handle')->once();
|
||||||
|
$member = Member::factory()->defaults()->create();
|
||||||
|
$activity = Activity::factory()->create();
|
||||||
|
$subactivity = Subactivity::factory()->create();
|
||||||
|
$group = Group::factory()->create();
|
||||||
|
|
||||||
|
SyncAction::run($group, $activity, $subactivity, [$member->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItDeletesAMembership(): void
|
||||||
|
{
|
||||||
|
MembershipDestroyAction::partialMock()->shouldReceive('handle')->once();
|
||||||
|
MembershipStoreAction::partialMock()->shouldReceive('handle')->never();
|
||||||
|
ResyncAction::partialMock()->shouldReceive('handle')->once();
|
||||||
|
|
||||||
|
$member = Member::factory()->defaults()->has(Membership::factory()->inLocal('Leiter*in', 'Rover'))->create();
|
||||||
|
|
||||||
|
SyncAction::run($member->memberships->first()->group, $member->memberships->first()->activity, $member->memberships->first()->subactivity, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItRollsbackWhenDeletionFails(): void
|
||||||
|
{
|
||||||
|
app(MembershipFake::class)
|
||||||
|
->shows(3, ['id' => 55])
|
||||||
|
->shows(3, ['id' => 56])
|
||||||
|
->destroysSuccessfully(3, 55)
|
||||||
|
->failsDeleting(3, 56);
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$member = Member::factory()->defaults()->inNami(3)
|
||||||
|
->has(Membership::factory()->in('Leiter*in', 10, 'Rover', 11)->inNami(55))
|
||||||
|
->has(Membership::factory()->in('Leiter*in', 10, 'Jungpfadfinder', 12)->inNami(56))
|
||||||
|
->create();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SyncAction::run($member->memberships->first()->group, $member->memberships->first()->activity, $member->memberships->first()->subactivity, []);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
}
|
||||||
|
$this->assertDatabaseCount('memberships', 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Group;
|
||||||
|
|
||||||
|
use App\Activity;
|
||||||
|
use App\Group;
|
||||||
|
use App\Subactivity;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class IndexTest extends TestCase
|
||||||
|
{
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItDisplaysAllActivitiesAndSubactivities(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$leiter = Activity::factory()->name('Leiter*in')->hasAttached(Subactivity::factory()->name('Rover'))->create();
|
||||||
|
$intern = Activity::factory()->name('Intern')->hasAttached(Subactivity::factory()->name('Lager'))->create();
|
||||||
|
$group = Group::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->get('/group');
|
||||||
|
|
||||||
|
$this->assertInertiaHas('Leiter*in', $response, "activities.{$leiter->id}");
|
||||||
|
$this->assertInertiaHas('Intern', $response, "activities.{$intern->id}");
|
||||||
|
$this->assertInertiaHas('Rover', $response, "subactivities.{$leiter->id}.{$leiter->subactivities->first()->id}");
|
||||||
|
$this->assertInertiaHas('Lager', $response, "subactivities.{$intern->id}.{$intern->subactivities->first()->id}");
|
||||||
|
$this->assertInertiaHas($group->name, $response, "groups.{$group->id}");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue