Compare commits

...

3 Commits

Author SHA1 Message Date
philipp lang aeb926e165 Add Broadcast event when deleting member
continuous-integration/drone/push Build is passing Details
2023-08-16 00:43:28 +02:00
philipp lang 3263e93da7 Move member deletion to action 2023-08-15 23:03:51 +02:00
philipp lang 0ae11f753f Move nami delete member action 2023-08-15 23:00:01 +02:00
20 changed files with 399 additions and 225 deletions

View File

@ -0,0 +1,50 @@
<?php
namespace App\Lib\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class JobEvent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public bool $reload = false;
public string $message = '';
final private function __construct(public string $channel)
{
}
public static function on(string $channel): static
{
return new static($channel);
}
public function withMessage(string $message): static
{
$this->message = $message;
return $this;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel
*/
public function broadcastOn()
{
return new Channel($this->channel);
}
public function shouldReload(): static
{
$this->reload = true;
return $this;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Lib\Events;
class JobFinished extends JobEvent
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Lib\Events;
class JobStarted extends JobEvent
{
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Lib\JobMiddleware;
use App\Lib\Events\JobFinished;
use App\Lib\Events\JobStarted;
use Closure;
use Lorisleiva\Actions\Decorators\JobDecorator;
class WithJobState
{
public JobStarted $beforeMessage;
public JobFinished $afterMessage;
private function __construct(public string $channel)
{
}
public static function make(string $channel): self
{
return new self($channel);
}
public function before(string $message): self
{
$this->beforeMessage = JobStarted::on($this->channel)->withMessage($message);
return $this;
}
public function after(string $message): self
{
$this->afterMessage = JobFinished::on($this->channel)->withMessage($message);
return $this;
}
public function shouldReload(): self
{
$this->afterMessage->shouldReload();
return $this;
}
public function handle(JobDecorator $job, Closure $next): void
{
event($this->beforeMessage);
$next($job);
event($this->afterMessage);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Member\Actions;
use App\Lib\JobMiddleware\WithJobState;
use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class MemberDeleteAction
{
use AsAction;
public function handle(int $memberId): void
{
$member = Member::findOrFail($memberId);
if ($member->nami_id) {
NamiDeleteMemberAction::run($member->nami_id);
}
$member->delete();
ResyncAction::run();
}
public function asController(Member $member): RedirectResponse
{
static::dispatch($member->id);
return redirect()->back();
}
/**
* @return array<int, object>
*/
public function getJobMiddleware(int $memberId): array
{
$member = Member::findOrFail($memberId);
return [
WithJobState::make('member')
->before('Lösche Mitglied ' . $member->fullname)
->after('Mitglied ' . $member->fullname . ' gelöscht')
->shouldReload(),
];
}
}

View File

@ -1,11 +1,11 @@
<?php
namespace App\Member;
namespace App\Member\Actions;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteAction
class NamiDeleteMemberAction
{
use AsAction;

View File

@ -4,8 +4,6 @@ namespace App\Member;
use App\Country;
use App\Http\Controllers\Controller;
use App\Lib\Events\ClientMessage;
use App\Maildispatcher\Actions\ResyncAction;
use App\Setting\GeneralSettings;
use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse;
@ -82,17 +80,4 @@ class MemberController extends Controller
return redirect()->route('member.index');
}
public function destroy(Member $member): RedirectResponse
{
if ($member->nami_id) {
DeleteAction::dispatch($member->nami_id);
}
$member->delete();
ResyncAction::dispatch();
ClientMessage::make('Mitglied ' . $member->fullname . ' gelöscht.')->shouldReload()->dispatch();
return redirect()->back();
}
}

View File

@ -6,6 +6,7 @@ use App\Activity;
use App\Group;
use App\Invoice\BillKind;
use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Actions\NamiDeleteMemberAction;
use App\Member\Actions\NamiPutMemberAction;
use App\Setting\NamiSettings;
use App\Subactivity;
@ -113,7 +114,7 @@ class MemberRequest extends FormRequest
NamiPutMemberAction::run($member->fresh(), null, null);
}
if (!$this->input('has_nami') && null !== $member->nami_id) {
DeleteAction::dispatch($member->nami_id);
NamiDeleteMemberAction::dispatch($member->nami_id);
}
ResyncAction::dispatch();
}

View File

@ -60,6 +60,7 @@
"mockery/mockery": "^1.4.4",
"nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^7.0",
"phpstan/phpstan-mockery": "^1.1",
"phpunit/phpunit": "^9.5.10"
},
"config": {

54
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "62586d4169459f71189b880d8a86e1ec",
"content-hash": "9b301aa44118f3ff66ba16b8688726a3",
"packages": [
{
"name": "beyondcode/laravel-dump-server",
@ -10700,7 +10700,7 @@
"dist": {
"type": "path",
"url": "./packages/tex",
"reference": "48251272de62e3fea044a7ad31e1a411c15eb4c6"
"reference": "6f162102ef7ceca41822d18c3e694abd926f550b"
},
"type": "library",
"extra": {
@ -11618,6 +11618,56 @@
],
"time": "2023-08-08T12:33:42+00:00"
},
{
"name": "phpstan/phpstan-mockery",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-mockery.git",
"reference": "6aa86bd8e9c9a1be97baf0558d4a2ed1374736a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-mockery/zipball/6aa86bd8e9c9a1be97baf0558d4a2ed1374736a6",
"reference": "6aa86bd8e9c9a1be97baf0558d4a2ed1374736a6",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.10"
},
"require-dev": {
"mockery/mockery": "^1.2.4",
"nikic/php-parser": "^4.13.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpunit/phpunit": "^9.5"
},
"type": "phpstan-extension",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
}
},
"autoload": {
"psr-4": {
"PHPStan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPStan Mockery extension",
"support": {
"issues": "https://github.com/phpstan/phpstan-mockery/issues",
"source": "https://github.com/phpstan/phpstan-mockery/tree/1.1.1"
},
"time": "2023-02-18T13:54:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.27",

View File

@ -1,5 +1,6 @@
includes:
- ./vendor/nunomaduro/larastan/extension.neon
- ./vendor/phpstan/phpstan-mockery/extension.neon
parameters:
@ -558,16 +559,6 @@ parameters:
count: 1
path: packages/laravel-nami/tests/TestCase.php
-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:never\\(\\)\\.$#"
count: 1
path: tests/Feature/Initialize/InitializeActionTest.php
-
message: "#^Call to an undefined method Mockery\\\\ExpectationInterface\\|Mockery\\\\HigherOrderMessage\\:\\:once\\(\\)\\.$#"
count: 2
path: tests/Feature/Initialize/InitializeMembersTest.php
-
message: "#^Parameter \\#1 \\$mock of static method Phake\\:\\:verify\\(\\) expects Phake\\\\IMock, App\\\\Actions\\\\PullMemberAction given\\.$#"
count: 1

View File

@ -1,9 +1,9 @@
import {ref, computed} from 'vue';
import {ref, computed, onBeforeUnmount} from 'vue';
import {router} from '@inertiajs/vue3';
import Toast, {useToast} from 'vue-toastification';
import {useToast} from 'vue-toastification';
const toast = useToast();
export function useIndex(props) {
export function useIndex(props, siteName) {
const rawProps = JSON.parse(JSON.stringify(props));
const inner = {
data: ref(rawProps.data),
@ -61,14 +61,21 @@ export function useIndex(props) {
};
}
window.Echo.channel('jobs').listen('\\App\\Lib\\Events\\ClientMessage', (e) => {
if (e.message) {
toast.success(e.message);
function handleJobEvent(event) {
if (event.message) {
toast.success(event.message);
}
if (e.reload) {
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));
onBeforeUnmount(() => window.Echo.leave(siteName));
onBeforeUnmount(() => window.Echo.leave('jobs'));
return {
data: inner.data,

View File

@ -1,7 +1,8 @@
<template>
<page-layout page-class="pb-6">
<template #toolbar>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Tätigkeit erstellen</page-toolbar-button>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Tätigkeit
erstellen</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" heading="Bitte bestätigen" @close="deleting = null">
<div>
@ -22,8 +23,10 @@
<td v-text="activity.name"></td>
<td>
<div class="flex space-x-1">
<i-link v-tooltip="`bearbeiten`" :href="activity.links.edit" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="deleting = activity"><ui-sprite src="trash"></ui-sprite></a>
<i-link v-tooltip="`bearbeiten`" :href="activity.links.edit"
class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<a v-tooltip="`Entfernen`" href="#" class="inline-flex btn btn-danger btn-sm"
@click.prevent="deleting = activity"><ui-sprite src="trash"></ui-sprite></a>
</div>
</td>
</tr>
@ -36,11 +39,11 @@
</template>
<script setup>
import {ref, defineProps} from 'vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
import { ref, defineProps } from 'vue';
import { indexProps, useIndex } from '../../composables/useIndex.js';
const props = defineProps(indexProps);
const {router, data, meta} = useIndex(props.data);
const { router, data, meta } = useIndex(props.data, 'activity');
const deleting = ref(null);
function remove() {

View File

@ -10,56 +10,27 @@
<ui-box heading="Metadatem">
<div class="grid gap-4 sm:grid-cols-2">
<f-text id="name" v-model="model.name" name="name" label="Name" size="sm" required></f-text>
<f-select id="gateway_id" v-model="model.gateway_id" name="gateway_id" :options="meta.gateways" label="Verbindung" size="sm" required></f-select>
<f-select id="gateway_id" v-model="model.gateway_id" name="gateway_id" :options="meta.gateways"
label="Verbindung" size="sm" required></f-select>
</div>
</ui-box>
<ui-box v-if="members !== null" heading="Filterregeln">
<div class="grid gap-4 sm:grid-cols-2">
<f-multipleselect
id="activity_ids"
v-model="model.filter.activity_ids"
name="activity_ids"
:options="members.meta.filterActivities"
label="Tätigkeit"
size="sm"
@update:model-value="reload(1)"
></f-multipleselect>
<f-multipleselect
id="subactivity_ids"
v-model="model.filter.subactivity_ids"
name="subactivity_ids"
:options="members.meta.filterSubactivities"
label="Unterttätigkeit"
size="sm"
@update:model-value="reload(1)"
></f-multipleselect>
<f-multipleselect
id="include"
v-model="model.filter.include"
name="include"
:options="members.meta.members"
label="Zusätzliche Mitglieder"
size="sm"
@update:model-value="reload(1)"
></f-multipleselect>
<f-multipleselect
id="exclude"
v-model="model.filter.exclude"
name="exclude"
:options="members.meta.members"
label="Mitglieder ausschließen"
size="sm"
@update:model-value="reload(1)"
></f-multipleselect>
<f-multipleselect
id="groupIds"
v-model="model.filter.group_ids"
name="groupIds"
:options="members.meta.groups"
label="Gruppierungen"
size="sm"
@update:model-value="reload(1)"
></f-multipleselect>
<f-multipleselect id="activity_ids" v-model="model.filter.activity_ids" name="activity_ids"
:options="members.meta.filterActivities" label="Tätigkeit" size="sm"
@update:model-value="reload(1)"></f-multipleselect>
<f-multipleselect id="subactivity_ids" v-model="model.filter.subactivity_ids" name="subactivity_ids"
:options="members.meta.filterSubactivities" label="Unterttätigkeit" size="sm"
@update:model-value="reload(1)"></f-multipleselect>
<f-multipleselect id="include" v-model="model.filter.include" name="include"
:options="members.meta.members" label="Zusätzliche Mitglieder" size="sm"
@update:model-value="reload(1)"></f-multipleselect>
<f-multipleselect id="exclude" v-model="model.filter.exclude" name="exclude"
:options="members.meta.members" label="Mitglieder ausschließen" size="sm"
@update:model-value="reload(1)"></f-multipleselect>
<f-multipleselect id="groupIds" v-model="model.filter.group_ids" name="groupIds"
:options="members.meta.groups" label="Gruppierungen" size="sm"
@update:model-value="reload(1)"></f-multipleselect>
</div>
</ui-box>
<ui-box v-if="members !== null" heading="Mitglieder">
@ -87,8 +58,8 @@
</template>
<script setup>
import {ref, inject, defineProps} from 'vue';
import {useIndex} from '../../composables/useIndex.js';
import { ref, inject, defineProps } from 'vue';
import { useIndex } from '../../composables/useIndex.js';
const props = defineProps({
data: {
@ -97,13 +68,13 @@ const props = defineProps({
},
meta: {
type: Object,
default: () => {},
default: () => { },
},
});
const {toFilterString, router} = useIndex({data: [], meta: {}});
const { toFilterString, router } = useIndex({ data: [], meta: {} }, 'maildispatcher');
const model = ref(props.data === undefined ? {...props.meta.default_model} : {...props.data});
const model = ref(props.data === undefined ? { ...props.meta.default_model } : { ...props.data });
const members = ref(null);
const axios = inject('axios');

View File

@ -1,39 +1,26 @@
<template>
<page-layout>
<template #toolbar>
<page-toolbar-button color="primary" icon="plus" @click.prevent="model = {...meta.default}">Neue Verbindung</page-toolbar-button>
<page-toolbar-button color="primary" icon="plus" @click.prevent="model = { ...meta.default }">Neue
Verbindung</page-toolbar-button>
</template>
<ui-popup v-if="model !== null" :heading="model.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" @close="model = null">
<ui-popup v-if="model !== null" :heading="model.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'"
@close="model = null">
<form @submit.prevent="submit">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="name" v-model="model.name" name="name" label="Bezeichnung" required></f-text>
<f-text id="domain" v-model="model.domain" name="domain" label="Domain" required></f-text>
<f-select
id="type"
:model-value="model.type.cls"
label="Typ"
name="type"
:options="meta.types"
:placeholder="''"
required
@update:model-value="
<f-select id="type" :model-value="model.type.cls" label="Typ" name="type" :options="meta.types"
:placeholder="''" required @update:model-value="
model.type = {
cls: $event,
params: {...getType($event).defaults},
params: { ...getType($event).defaults },
}
"
></f-select>
"></f-select>
<template v-for="(field, index) in getType(model.type.cls).fields">
<f-text
v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.name"
:key="index"
v-model="model.type.params[field.name]"
:label="field.label"
:type="field.type"
:name="field.name"
:required="field.is_required"
></f-text>
<f-text v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'"
:id="field.name" :key="index" v-model="model.type.params[field.name]" :label="field.label"
:type="field.type" :name="field.name" :required="field.is_required"></f-text>
</template>
</section>
<section class="flex mt-4 space-x-2">
@ -58,14 +45,12 @@
<td v-text="gateway.domain"></td>
<td v-text="gateway.type_human"></td>
<td>
<ui-boolean-display
:value="gateway.works"
long-label="Verbindungsstatus"
:label="gateway.works ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
></ui-boolean-display>
<ui-boolean-display :value="gateway.works" long-label="Verbindungsstatus"
:label="gateway.works ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"></ui-boolean-display>
</td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="model = {...gateway}"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm"
@click.prevent="model = { ...gateway }"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
</table>
@ -79,12 +64,12 @@
</template>
<script setup>
import {ref, inject} from 'vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
import { ref, inject } from 'vue';
import { indexProps, useIndex } from '../../composables/useIndex.js';
import SettingLayout from '../setting/Layout.vue';
const props = defineProps(indexProps);
const {meta, data, reload} = useIndex(props.data);
const { meta, data, reload } = useIndex(props.data, 'mailgateway');
const model = ref(null);
const axios = inject('axios');

View File

@ -1,17 +1,23 @@
<template>
<page-layout page-class="pb-6">
<template #toolbar>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied anlegen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary" icon="invoice">Rechnungen erstellen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info" icon="envelope">Rechnungen versenden</page-toolbar-button>
<page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied
anlegen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary"
icon="invoice">Rechnungen erstellen</page-toolbar-button>
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.sendpayment" color="info"
icon="envelope">Rechnungen versenden</page-toolbar-button>
</template>
<ui-popup v-if="deleting !== null" heading="Mitglied löschen?" @close="deleting.reject()">
<div>
<p class="mt-4">Das Mitglied "{{ deleting.member.fullname }}" löschen?</p>
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls entfernt.</p>
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
<p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls
entfernt.</p>
<ui-note v-if="!deleting.member.has_nami" class="mt-5" type="warning"> Dieses Mitglied ist nicht in NaMi
vorhanden und wird daher nur in der AdReMa gelöscht werden. </ui-note>
<ui-note v-if="deleting.member.has_nami" class="mt-5" type="danger">
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern "Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
Dieses Mitglied ist in NaMi vorhanden und wird daher in NaMi abgemeldet werden. Sofern
"Datenweiterverwendung" eingeschaltet ist, wird das Mitglied auf inaktiv gesetzt.
</ui-note>
<div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a>
@ -20,45 +26,22 @@
</div>
</ui-popup>
<div class="px-6 py-2 flex border-b border-gray-600 items-center space-x-3">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
<f-multipleselect
id="group_ids"
:options="meta.groups"
:model-value="getFilter('group_ids')"
label="Gruppierungen"
size="sm"
name="group_ids"
@update:model-value="setFilter('group_ids', $event)"
></f-multipleselect>
<f-select
v-show="hasModule('bill')"
id="billKinds"
name="billKinds"
:options="meta.billKinds"
:model-value="getFilter('bill_kind')"
label="Rechnung"
size="sm"
@update:model-value="setFilter('bill_kind', $event)"
></f-select>
<f-multipleselect
id="activity_ids"
:options="meta.filterActivities"
:model-value="getFilter('activity_ids')"
label="Tätigkeiten"
size="sm"
name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"
></f-multipleselect>
<f-multipleselect
id="subactivity_ids"
:options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')"
label="Untertätigkeiten"
size="sm"
name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"
></f-multipleselect>
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
@update:model-value="setFilter('search', $event)"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände"
size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
<f-multipleselect id="group_ids" :options="meta.groups" :model-value="getFilter('group_ids')"
label="Gruppierungen" size="sm" name="group_ids"
@update:model-value="setFilter('group_ids', $event)"></f-multipleselect>
<f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
:model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
@update:model-value="setFilter('bill_kind', $event)"></f-select>
<f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
label="Tätigkeiten" size="sm" name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
<f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
:model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite>
<span class="hidden xl:inline">Exportieren</span>
@ -107,11 +90,14 @@
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
<div class="flex items-center mt-1 space-x-4">
<tags :member="member"></tags>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment" fallback=""></ui-label>
<ui-label v-show="hasModule('bill')" class="text-gray-100 block" :value="member.pending_payment"
fallback=""></ui-label>
</div>
<actions class="mt-2" :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"> </actions>
<actions class="mt-2" :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)">
</actions>
<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-down" class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
<i-link v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron-down"
class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
</div>
</ui-box>
</div>
@ -120,22 +106,13 @@
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div>
<member-payments
v-if="single !== null && sidebar === 'payment.index'"
:subscriptions="meta.subscriptions"
:statuses="meta.statuses"
:value="data[single]"
@close="closeSidebar"
></member-payments>
<member-memberships
v-if="single !== null && sidebar === 'membership.index'"
:groups="meta.groups"
:activities="meta.formActivities"
:subactivities="meta.formSubactivities"
:value="data[single]"
@close="closeSidebar"
></member-memberships>
<member-courses v-if="single !== null && sidebar === 'courses.index'" :courses="meta.courses" :value="data[single]" @close="closeSidebar"></member-courses>
<member-payments v-if="single !== null && sidebar === 'payment.index'" :subscriptions="meta.subscriptions"
:statuses="meta.statuses" :value="data[single]" @close="closeSidebar"></member-payments>
<member-memberships v-if="single !== null && sidebar === 'membership.index'" :groups="meta.groups"
:activities="meta.formActivities" :subactivities="meta.formSubactivities" :value="data[single]"
@close="closeSidebar"></member-memberships>
<member-courses v-if="single !== null && sidebar === 'courses.index'" :courses="meta.courses" :value="data[single]"
@close="closeSidebar"></member-courses>
</page-layout>
</template>
@ -145,15 +122,15 @@ import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue';
import Tags from './Tags.vue';
import Actions from './index/Actions.vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
import {ref, defineProps} from '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);
const props = defineProps(indexProps);
var {router, data, meta, getFilter, setFilter, filterString} = useIndex(props.data);
var { router, data, meta, getFilter, setFilter, filterString } = useIndex(props.data, 'member');
function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`);
@ -161,7 +138,7 @@ function exportMembers() {
async function remove(member) {
new Promise((resolve, reject) => {
deleting.value = {resolve, reject, member};
deleting.value = { resolve, reject, member };
})
.then(() => {
router.delete(`/member/${member.id}`);

View File

@ -29,6 +29,7 @@ use App\Maildispatcher\Actions\UpdateAction as MaildispatcherUpdateAction;
use App\Mailgateway\Actions\StoreAction;
use App\Mailgateway\Actions\UpdateAction;
use App\Member\Actions\ExportAction;
use App\Member\Actions\MemberDeleteAction;
use App\Member\Actions\MemberResyncAction;
use App\Member\Actions\MemberShowAction;
use App\Member\Actions\SearchAction;
@ -55,7 +56,8 @@ Route::group(['middleware' => 'auth:web'], function (): void {
Route::post('/api/member/search', SearchAction::class)->name('member.search');
Route::get('/initialize', InitializeFormAction::class)->name('initialize.form');
Route::post('/initialize', InitializeAction::class)->name('initialize.store');
Route::resource('member', MemberController::class)->except('show');
Route::resource('member', MemberController::class)->except('show', 'destroy');
Route::delete('/member/{member}', MemberDeleteAction::class);
Route::get('/member/{member}', MemberShowAction::class)->name('member.show');
Route::apiResource('member.payment', PaymentController::class);
Route::get('allpayment', AllpaymentPageAction::class)->name('allpayment.page');

View File

@ -22,7 +22,8 @@ class InitializeMembersTest extends TestCase
app(SearchFake::class)->fetches(1, 0, 100, [
MemberEntry::factory()->toMember(['groupId' => 100, 'id' => 20]),
]);
FullMemberAction::shouldRun()->once()->shouldReceive('configureJob');
FullMemberAction::partialMock()->shouldReceive('configureJob')->once();
FullMemberAction::partialMock()->shouldReceive('handle')->once();
app(InitializeMembers::class)->handle($api);
}
@ -33,7 +34,8 @@ class InitializeMembersTest extends TestCase
app(SearchFake::class)->fetches(1, 0, 100, [
MemberEntry::factory()->toMember(['groupId' => 100, 'id' => 20]),
]);
FullMemberAction::shouldRun()->once()->shouldReceive('configureJob');
FullMemberAction::partialMock()->shouldReceive('configureJob')->once();
FullMemberAction::partialMock()->shouldReceive('handle')->once();
Artisan::call('member:pull');
}

View File

@ -4,19 +4,22 @@ namespace Tests\Feature\Member;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Member\DeleteAction;
use App\Lib\Events\ClientMessage;
use App\Lib\Events\JobFinished;
use App\Lib\Events\JobStarted;
use App\Member\Actions\MemberDeleteAction;
use App\Member\Actions\NamiDeleteMemberAction;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
use Zoomyboy\LaravelNami\Fakes\MemberFake;
class DeleteTest extends TestCase
{
use DatabaseTransactions;
public function testItDeletesMemberFromNami(): void
public function testItFiresJob(): void
{
Queue::fake();
$this->login()->loginNami();
@ -26,7 +29,17 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member');
DeleteAction::assertPushed();
MemberDeleteAction::assertPushed(fn ($action, $parameters) => $parameters[0] === $member->id);
}
public function testItDeletesMemberFromNami(): void
{
$this->login()->loginNami();
NamiDeleteMemberAction::partialMock()->shouldReceive('handle')->with(123)->once();
$member = Member::factory()->defaults()->inNami(123)->create();
MemberDeleteAction::run($member->id);
$this->assertDatabaseMissing('members', [
'id' => $member->id,
]);
@ -34,38 +47,33 @@ class DeleteTest extends TestCase
public function testItDoesntRunActionWhenMemberIsNotInNami(): void
{
Queue::fake();
$this->login()->loginNami();
NamiDeleteMemberAction::partialMock()->shouldReceive('handle')->never();
$member = Member::factory()->defaults()->create();
$response = $this->from('/member')->delete("/member/{$member->id}");
$response->assertRedirect('/member');
Queue::assertNotPushed(DeleteAction::class);
$this->assertDatabaseMissing('members', [
'id' => $member->id,
]);
MemberDeleteAction::run($member->id);
}
public function testTheActionDeletesNamiMember(): void
{
app(MemberFake::class)->deletes(123, Carbon::parse('yesterday'));
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->inNami(123)->create();
DeleteAction::dispatch(123);
app(MemberFake::class)->assertDeleted(123, Carbon::parse('yesterday'));
}
public function testItDeletesMembersWithCourses(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->has(CourseMember::factory()->for(Course::factory()), 'courses')->create();
$member->delete();
MemberDeleteAction::run($member->id);
$this->assertDatabaseCount('members', 0);
}
public function testItFiresEventWhenFinished(): void
{
Event::fake([JobStarted::class, JobFinished::class]);
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->create(['firstname' => 'Max', 'lastname' => 'Muster']);
MemberDeleteAction::dispatch($member->id);
Event::assertDispatched(JobStarted::class, fn ($event) => $event->broadcastOn()->name === 'member' && $event->message === 'Lösche Mitglied Max Muster' && $event->reload === false);
Event::assertDispatched(JobFinished::class, fn ($event) => $event->message === 'Mitglied Max Muster gelöscht' && $event->reload === true);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Tests\Feature\Member;
use App\Member\Actions\NamiDeleteMemberAction;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Zoomyboy\LaravelNami\Fakes\MemberFake;
class NamiDeleteMemberActionTest extends TestCase
{
use DatabaseTransactions;
public function testTheActionDeletesNamiMember(): void
{
app(MemberFake::class)->deletes(123, Carbon::parse('yesterday'));
$this->withoutExceptionHandling()->login()->loginNami();
Member::factory()->defaults()->inNami(123)->create();
NamiDeleteMemberAction::dispatch(123);
app(MemberFake::class)->assertDeleted(123, Carbon::parse('yesterday'));
}
}