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
namespace App\Member\Actions;
namespace App\Member;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
class NamiDeleteMemberAction
class DeleteAction
{
use AsAction;

View File

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

View File

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

View File

@ -60,7 +60,6 @@
"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": "9b301aa44118f3ff66ba16b8688726a3",
"content-hash": "62586d4169459f71189b880d8a86e1ec",
"packages": [
{
"name": "beyondcode/laravel-dump-server",
@ -10700,7 +10700,7 @@
"dist": {
"type": "path",
"url": "./packages/tex",
"reference": "6f162102ef7ceca41822d18c3e694abd926f550b"
"reference": "48251272de62e3fea044a7ad31e1a411c15eb4c6"
},
"type": "library",
"extra": {
@ -11618,56 +11618,6 @@
],
"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,6 +1,5 @@
includes:
- ./vendor/nunomaduro/larastan/extension.neon
- ./vendor/phpstan/phpstan-mockery/extension.neon
parameters:
@ -559,6 +558,16 @@ 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, onBeforeUnmount} from 'vue';
import {ref, computed} from 'vue';
import {router} from '@inertiajs/vue3';
import {useToast} from 'vue-toastification';
import Toast, {useToast} from 'vue-toastification';
const toast = useToast();
export function useIndex(props, siteName) {
export function useIndex(props) {
const rawProps = JSON.parse(JSON.stringify(props));
const inner = {
data: ref(rawProps.data),
@ -61,21 +61,14 @@ export function useIndex(props, siteName) {
};
}
function handleJobEvent(event) {
if (event.message) {
toast.success(event.message);
window.Echo.channel('jobs').listen('\\App\\Lib\\Events\\ClientMessage', (e) => {
if (e.message) {
toast.success(e.message);
}
if (event.reload) {
if (e.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,8 +1,7 @@
<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>
@ -23,10 +22,8 @@
<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>
@ -39,11 +36,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, 'activity');
const {router, data, meta} = useIndex(props.data);
const deleting = ref(null);
function remove() {

View File

@ -10,27 +10,56 @@
<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">
@ -58,8 +87,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: {
@ -68,13 +97,13 @@ const props = defineProps({
},
meta: {
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 axios = inject('axios');

View File

@ -1,26 +1,39 @@
<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">
@ -45,12 +58,14 @@
<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>
@ -64,12 +79,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, 'mailgateway');
const {meta, data, reload} = useIndex(props.data);
const model = ref(null);
const axios = inject('axios');

View File

@ -1,23 +1,17 @@
<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>
@ -26,22 +20,45 @@
</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>
@ -90,14 +107,11 @@
<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>
@ -106,13 +120,22 @@
<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>
@ -122,15 +145,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, 'member');
var {router, data, meta, getFilter, setFilter, filterString} = useIndex(props.data);
function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`);
@ -138,7 +161,7 @@ function exportMembers() {
async function remove(member) {
new Promise((resolve, reject) => {
deleting.value = { resolve, reject, member };
deleting.value = {resolve, reject, member};
})
.then(() => {
router.delete(`/member/${member.id}`);

View File

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

View File

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

View File

@ -4,22 +4,19 @@ 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\DeleteAction;
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 testItFiresJob(): void
public function testItDeletesMemberFromNami(): void
{
Queue::fake();
$this->login()->loginNami();
@ -29,17 +26,7 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member');
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);
DeleteAction::assertPushed();
$this->assertDatabaseMissing('members', [
'id' => $member->id,
]);
@ -47,33 +34,38 @@ class DeleteTest extends TestCase
public function testItDoesntRunActionWhenMemberIsNotInNami(): void
{
Queue::fake();
$this->login()->loginNami();
NamiDeleteMemberAction::partialMock()->shouldReceive('handle')->never();
$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
{
$this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->has(CourseMember::factory()->for(Course::factory()), 'courses')->create();
MemberDeleteAction::run($member->id);
$member->delete();
$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'));
}
}