Compare commits

..

No commits in common. "aeb926e1654ff5a9dcf606113bfa2cc1094f4c28" and "6dffb8d4ff0727762e7b205a2602ee43b62c7b6c" have entirely different histories.

20 changed files with 225 additions and 399 deletions

View File

@ -1,50 +0,0 @@
<?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

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

View File

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

View File

@ -1,52 +0,0 @@
<?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

@ -1,49 +0,0 @@
<?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 <?php
namespace App\Member\Actions; namespace App\Member;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class NamiDeleteMemberAction class DeleteAction
{ {
use AsAction; use AsAction;

View File

@ -4,6 +4,8 @@ namespace App\Member;
use App\Country; use App\Country;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Lib\Events\ClientMessage;
use App\Maildispatcher\Actions\ResyncAction;
use App\Setting\GeneralSettings; use App\Setting\GeneralSettings;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@ -80,4 +82,17 @@ class MemberController extends Controller
return redirect()->route('member.index'); 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,7 +6,6 @@ use App\Activity;
use App\Group; use App\Group;
use App\Invoice\BillKind; use App\Invoice\BillKind;
use App\Maildispatcher\Actions\ResyncAction; use App\Maildispatcher\Actions\ResyncAction;
use App\Member\Actions\NamiDeleteMemberAction;
use App\Member\Actions\NamiPutMemberAction; use App\Member\Actions\NamiPutMemberAction;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use App\Subactivity; use App\Subactivity;
@ -114,7 +113,7 @@ class MemberRequest extends FormRequest
NamiPutMemberAction::run($member->fresh(), null, null); NamiPutMemberAction::run($member->fresh(), null, null);
} }
if (!$this->input('has_nami') && null !== $member->nami_id) { if (!$this->input('has_nami') && null !== $member->nami_id) {
NamiDeleteMemberAction::dispatch($member->nami_id); DeleteAction::dispatch($member->nami_id);
} }
ResyncAction::dispatch(); ResyncAction::dispatch();
} }

View File

@ -60,7 +60,6 @@
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/larastan": "^2.0", "nunomaduro/larastan": "^2.0",
"orchestra/testbench": "^7.0", "orchestra/testbench": "^7.0",
"phpstan/phpstan-mockery": "^1.1",
"phpunit/phpunit": "^9.5.10" "phpunit/phpunit": "^9.5.10"
}, },
"config": { "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", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9b301aa44118f3ff66ba16b8688726a3", "content-hash": "62586d4169459f71189b880d8a86e1ec",
"packages": [ "packages": [
{ {
"name": "beyondcode/laravel-dump-server", "name": "beyondcode/laravel-dump-server",
@ -10700,7 +10700,7 @@
"dist": { "dist": {
"type": "path", "type": "path",
"url": "./packages/tex", "url": "./packages/tex",
"reference": "6f162102ef7ceca41822d18c3e694abd926f550b" "reference": "48251272de62e3fea044a7ad31e1a411c15eb4c6"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -11618,56 +11618,6 @@
], ],
"time": "2023-08-08T12:33:42+00:00" "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", "name": "phpunit/php-code-coverage",
"version": "9.2.27", "version": "9.2.27",

View File

@ -1,6 +1,5 @@
includes: includes:
- ./vendor/nunomaduro/larastan/extension.neon - ./vendor/nunomaduro/larastan/extension.neon
- ./vendor/phpstan/phpstan-mockery/extension.neon
parameters: parameters:
@ -128,7 +127,7 @@ parameters:
count: 1 count: 1
path: app/Member/Member.php path: app/Member/Member.php
- -
message: "#^Unsafe usage of new static\\(\\)\\.$#" message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1 count: 1
path: app/Member/Member.php path: app/Member/Member.php
@ -559,6 +558,16 @@ parameters:
count: 1 count: 1
path: packages/laravel-nami/tests/TestCase.php 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\\.$#" message: "#^Parameter \\#1 \\$mock of static method Phake\\:\\:verify\\(\\) expects Phake\\\\IMock, App\\\\Actions\\\\PullMemberAction given\\.$#"
count: 1 count: 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,22 +4,19 @@ namespace Tests\Feature\Member;
use App\Course\Models\Course; use App\Course\Models\Course;
use App\Course\Models\CourseMember; use App\Course\Models\CourseMember;
use App\Lib\Events\ClientMessage; use App\Member\DeleteAction;
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 App\Member\Member;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Tests\TestCase; use Tests\TestCase;
use Zoomyboy\LaravelNami\Fakes\MemberFake;
class DeleteTest extends TestCase class DeleteTest extends TestCase
{ {
use DatabaseTransactions; use DatabaseTransactions;
public function testItFiresJob(): void public function testItDeletesMemberFromNami(): void
{ {
Queue::fake(); Queue::fake();
$this->login()->loginNami(); $this->login()->loginNami();
@ -29,17 +26,7 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member'); $response->assertRedirect('/member');
MemberDeleteAction::assertPushed(fn ($action, $parameters) => $parameters[0] === $member->id); DeleteAction::assertPushed();
}
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', [ $this->assertDatabaseMissing('members', [
'id' => $member->id, 'id' => $member->id,
]); ]);
@ -47,33 +34,38 @@ class DeleteTest extends TestCase
public function testItDoesntRunActionWhenMemberIsNotInNami(): void public function testItDoesntRunActionWhenMemberIsNotInNami(): void
{ {
Queue::fake();
$this->login()->loginNami(); $this->login()->loginNami();
NamiDeleteMemberAction::partialMock()->shouldReceive('handle')->never();
$member = Member::factory()->defaults()->create(); $member = Member::factory()->defaults()->create();
MemberDeleteAction::run($member->id); $response = $this->from('/member')->delete("/member/{$member->id}");
$response->assertRedirect('/member');
Queue::assertNotPushed(DeleteAction::class);
$this->assertDatabaseMissing('members', [
'id' => $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 public function testItDeletesMembersWithCourses(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->has(CourseMember::factory()->for(Course::factory()), 'courses')->create(); $member = Member::factory()->defaults()->has(CourseMember::factory()->for(Course::factory()), 'courses')->create();
MemberDeleteAction::run($member->id); $member->delete();
$this->assertDatabaseCount('members', 0); $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

@ -1,26 +0,0 @@
<?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'));
}
}