Add Broadcast event when deleting member
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
3263e93da7
commit
aeb926e165
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Events;
|
||||
|
||||
class JobFinished extends JobEvent
|
||||
{
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\Events;
|
||||
|
||||
class JobStarted extends JobEvent
|
||||
{
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
13
phpstan.neon
13
phpstan.neon
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue