Add Broadcast event when deleting member
continuous-integration/drone/push Build is passing Details

This commit is contained in:
philipp lang 2023-08-16 00:43:28 +02:00
parent 3263e93da7
commit aeb926e165
16 changed files with 368 additions and 210 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

@ -2,7 +2,7 @@
namespace App\Member\Actions;
use App\Lib\Events\ClientMessage;
use App\Lib\JobMiddleware\WithJobState;
use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Member;
use Illuminate\Http\RedirectResponse;
@ -13,16 +13,37 @@ class MemberDeleteAction
use AsAction;
public function handle(Member $member): RedirectResponse
public function handle(int $memberId): void
{
$member = Member::findOrFail($memberId);
if ($member->nami_id) {
NamiDeleteMemberAction::dispatch($member->nami_id);
NamiDeleteMemberAction::run($member->nami_id);
}
$member->delete();
ResyncAction::dispatch();
ClientMessage::make('Mitglied ' . $member->fullname . ' gelöscht.')->shouldReload()->dispatch();
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

@ -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:
@ -127,7 +128,7 @@ parameters:
count: 1
path: app/Member/Member.php
-
-
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
path: app/Member/Member.php
@ -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

@ -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\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');
NamiDeleteMemberAction::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(NamiDeleteMemberAction::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();
NamiDeleteMemberAction::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'));
}
}