Compare commits

..

8 Commits

Author SHA1 Message Date
philipp lang 4738c077d1 Fixed tests
continuous-integration/drone/push Build is failing Details
2023-08-15 00:01:12 +02:00
philipp lang f178d3ec86 Update client index when Member deleted 2023-08-14 23:57:15 +02:00
philipp lang 7e4961f3d8 Update DeleteJob as Action 2023-08-14 23:48:38 +02:00
philipp lang 2d9f79ee15 Update composer packages 2023-08-14 23:48:11 +02:00
Philipp Lang 6f133954ff Lint 2023-08-10 16:33:36 +02:00
Philipp Lang 59117682d0 Lint 2023-08-10 16:33:36 +02:00
philipp lang 1ac30202b1 Add composable for index 2023-08-10 16:33:36 +02:00
Philipp Lang 69afeeda65 Add eslint prettier 2023-08-10 16:33:36 +02:00
20 changed files with 820 additions and 1689 deletions

View File

@ -3,17 +3,11 @@
"browser": true, "browser": true,
"es2021": true "es2021": true
}, },
"extends": [ "extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
"eslint:recommended",
"plugin:vue/essential"
],
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module" "sourceType": "module"
}, },
"plugins": [ "plugins": ["vue"],
"vue" "rules": {}
],
"rules": {
}
} }

View File

@ -38,7 +38,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function commands() protected function commands()
{ {
$this->load(__DIR__.'/Commands'); $this->load(__DIR__ . '/Commands');
require base_path('routes/console.php'); require base_path('routes/console.php');
} }

View File

@ -3,7 +3,6 @@
namespace App\Dashboard\Actions; namespace App\Dashboard\Actions;
use App\Dashboard\DashboardFactory; use App\Dashboard\DashboardFactory;
use Illuminate\Http\Request;
use Inertia; use Inertia;
use Inertia\Response; use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -22,7 +21,7 @@ class IndexAction
]; ];
} }
public function asController(Request $request): Response public function asController(): Response
{ {
session()->put('menu', 'dashboard'); session()->put('menu', 'dashboard');
session()->put('title', 'Dashboard'); session()->put('title', 'Dashboard');

View File

@ -0,0 +1,54 @@
<?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 ClientMessage implements ShouldBroadcastNow
{
use Dispatchable;
use InteractsWithSockets;
use SerializesModels;
public bool $reload = false;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(public string $message)
{
}
public static function make(string $message): self
{
return new static($message);
}
public function shouldReload(): self
{
$this->reload = true;
return $this;
}
public function dispatch(): void
{
event($this);
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel
*/
public function broadcastOn()
{
return new Channel('jobs');
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Member;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
class DeleteAction
{
use AsAction;
public function handle(int $namiId)
{
app(NamiSettings::class)->login()->deleteMember($namiId);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Member;
use App\Setting\NamiSettings;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class DeleteJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public int $namiId;
public function __construct(int $namiId)
{
$this->namiId = $namiId;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(NamiSettings $setting)
{
$setting->login()->deleteMember($this->namiId);
}
}

View File

@ -4,6 +4,7 @@ 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\Maildispatcher\Actions\ResyncAction;
use App\Setting\GeneralSettings; use App\Setting\GeneralSettings;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
@ -21,11 +22,12 @@ class MemberController extends Controller
$filter = FilterScope::fromRequest($request->input('filter', '')); $filter = FilterScope::fromRequest($request->input('filter', ''));
return \Inertia::render('member/VIndex', [ return \Inertia::render('member/VIndex', [
'data' => MemberResource::collection(Member::search($filter->search)->query(fn ($q) => $q->select('*') 'data' => MemberResource::collection(Member::search($filter->search)->query(
->withFilter($filter) fn ($q) => $q->select('*')
->with('payments.subscription')->with(['memberships' => fn ($query) => $query->active()])->with('courses')->with('subscription')->with('leaderMemberships')->with('ageGroupMemberships') ->withFilter($filter)
->withPendingPayment() ->with('payments.subscription')->with(['memberships' => fn ($query) => $query->active()])->with('courses')->with('subscription')->with('leaderMemberships')->with('ageGroupMemberships')
->ordered() ->withPendingPayment()
->ordered()
)->paginate(15)), )->paginate(15)),
]); ]);
} }
@ -84,11 +86,12 @@ class MemberController extends Controller
public function destroy(Member $member): RedirectResponse public function destroy(Member $member): RedirectResponse
{ {
if ($member->nami_id) { if ($member->nami_id) {
DeleteJob::dispatch($member->nami_id); DeleteAction::dispatch($member->nami_id);
} }
$member->delete(); $member->delete();
ResyncAction::dispatch(); ResyncAction::dispatch();
ClientMessage::make('Mitglied ' . $member->fullname . ' gelöscht.')->shouldReload()->dispatch();
return redirect()->back(); return redirect()->back();
} }

View File

@ -113,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) {
DeleteJob::dispatch($member->nami_id); DeleteAction::dispatch($member->nami_id);
} }
ResyncAction::dispatch(); ResyncAction::dispatch();
} }

1771
composer.lock generated

File diff suppressed because it is too large Load Diff

40
package-lock.json generated
View File

@ -28,9 +28,11 @@
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0", "axios": "^1.4.0",
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "^8.7.1",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.1.6",
"vue-axios": "^3.5.2" "vue-axios": "^3.5.2"
} }
}, },
@ -1800,6 +1802,18 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint-config-prettier": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-plugin-vue": { "node_modules/eslint-plugin-vue": {
"version": "8.7.1", "version": "8.7.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz",
@ -3792,6 +3806,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
@ -5332,6 +5359,13 @@
"text-table": "^0.2.0" "text-table": "^0.2.0"
} }
}, },
"eslint-config-prettier": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
"integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
"dev": true,
"requires": {}
},
"eslint-plugin-vue": { "eslint-plugin-vue": {
"version": "8.7.1", "version": "8.7.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.7.1.tgz",
@ -6734,6 +6768,12 @@
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true "dev": true
}, },
"typescript": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"devOptional": true
},
"update-browserslist-db": { "update-browserslist-db": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",

View File

@ -15,9 +15,11 @@
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0", "axios": "^1.4.0",
"eslint": "^8.43.0", "eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "^8.7.1",
"postcss": "^8.4.24", "postcss": "^8.4.24",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.1.6",
"vue-axios": "^3.5.2" "vue-axios": "^3.5.2"
}, },
"dependencies": { "dependencies": {

1
resources/js/app.js vendored
View File

@ -51,6 +51,7 @@ createInertiaApp({
requireModules(import.meta.glob('./components/ui/*.vue'), app, 'ui'); requireModules(import.meta.glob('./components/ui/*.vue'), app, 'ui');
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), app, 'page'); requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), app, 'page');
app.provide('axios', app.config.globalProperties.axios);
app.mount(el); app.mount(el);
}, },
}); });

View File

@ -3,7 +3,7 @@
<Head :title="$page.props.title"></Head> <Head :title="$page.props.title"></Head>
<page-header :title="$page.props.title"> <page-header :title="$page.props.title">
<template #before-title> <template #before-title>
<a href="#" @click.prevent="menuStore.toggle()" class="mr-2 lg:hidden"> <a href="#" class="mr-2 lg:hidden" @click.prevent="menuStore.toggle()">
<ui-sprite src="menu" class="text-gray-100 w-5 h-5"></ui-sprite> <ui-sprite src="menu" class="text-gray-100 w-5 h-5"></ui-sprite>
</a> </a>
</template> </template>

96
resources/js/composables/useIndex.js vendored Normal file
View File

@ -0,0 +1,96 @@
import {ref, computed} from 'vue';
import {router} from '@inertiajs/vue3';
import Toast, {useToast} from 'vue-toastification';
const toast = useToast();
export function useIndex(props) {
const rawProps = JSON.parse(JSON.stringify(props));
const inner = {
data: ref(rawProps.data),
meta: ref(rawProps.meta),
};
function toFilterString(data) {
return btoa(encodeURIComponent(JSON.stringify(data)));
}
const filterString = computed(() => toFilterString(inner.meta.value.filter));
function reload(resetPage = true) {
var data = {
filter: filterString.value,
page: 1,
};
data['page'] = resetPage ? 1 : inner.meta.value.current_page;
router.visit(window.location.pathname, {
data,
preserveState: true,
onSuccess: (page) => {
inner.data.value = page.props.data.data;
inner.meta.value = page.props.data.meta;
},
});
}
function can(permission) {
return inner.meta.value.can[permission];
}
function getFilter(value) {
return inner.meta.value.filter[value];
}
function setFilter(key, value) {
inner.meta.value.filter[key] = value;
reload();
}
function requestCallback(successMessage, failureMessage) {
return {
onSuccess: () => {
this.$success(successMessage);
reload(false);
},
onFailure: () => {
this.$error(failureMessage);
reload(false);
},
preserveState: true,
};
}
window.Echo.channel('jobs').listen('\\App\\Lib\\Events\\ClientMessage', (e) => {
if (e.message) {
toast.success(e.message);
}
if (e.reload) {
reload(false);
}
});
return {
data: inner.data,
reload,
can,
getFilter,
setFilter,
requestCallback,
meta: inner.meta,
filterString,
router,
toFilterString,
};
}
const indexProps = {
data: {
default: () => {
return {data: [], meta: {}};
},
type: Object,
},
};
export {indexProps};

View File

@ -1,60 +0,0 @@
export default {
data: function () {
return {
inner: {...this.data},
};
},
props: {
data: {},
},
computed: {
filterString() {
return this.toFilterString(this.inner.meta.filter);
},
},
methods: {
toFilterString(data) {
return btoa(encodeURIComponent(JSON.stringify(data)));
},
reload(resetPage = true) {
var _self = this;
var data = {
filter: this.filterString,
page: 1,
};
data['page'] = resetPage ? 1 : this.inner.meta.current_page;
this.$inertia.visit(window.location.pathname, {
data,
preserveState: true,
onSuccess(page) {
_self.inner = page.props.data;
},
});
},
can(permission) {
return this.inner.meta.can[permission];
},
getFilter(value) {
return this.inner.meta.filter[value];
},
setFilter(key, value) {
this.inner.meta.filter[key] = value;
this.reload();
},
requestCallback(successMessage, failureMessage) {
return {
onSuccess: () => {
this.$success(successMessage);
this.reload(false);
},
onFailure: () => {
this.$error(failureMessage);
this.reload(false);
},
preserveState: true,
};
},
},
};

View File

@ -1,14 +1,14 @@
<template> <template>
<page-layout page-class="pb-6"> <page-layout page-class="pb-6">
<template #toolbar> <template #toolbar>
<page-toolbar-button :href="data.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> </template>
<ui-popup heading="Bitte bestätigen" v-if="deleting !== null"> <ui-popup v-if="deleting !== null" heading="Bitte bestätigen" @close="deleting = null">
<div> <div>
<p class="mt-4">Diese Aktivität löschen?</p> <p class="mt-4">Diese Aktivität löschen?</p>
<div class="grid grid-cols-2 gap-3 mt-6"> <div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" @click.prevent="remove" class="text-center btn btn-danger">Löschen</a> <a href="#" class="text-center btn btn-danger" @click.prevent="remove">Löschen</a>
<a href="#" @click.prevent="deleting = null" class="text-center btn btn-primary">Abbrechen</a> <a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
</div> </div>
</div> </div>
</ui-popup> </ui-popup>
@ -18,46 +18,39 @@
<th></th> <th></th>
</thead> </thead>
<tr v-for="(activity, index) in inner.data" :key="index"> <tr v-for="(activity, index) in data" :key="index">
<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 :href="activity.links.edit" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"><ui-sprite src="pencil"></ui-sprite></i-link> <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>
<i-link href="#" @click.prevent="deleting = activity" class="inline-flex btn btn-danger btn-sm" v-tooltip="`Entfernen`"><ui-sprite src="trash"></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> </div>
</td> </td>
</tr> </tr>
</table> </table>
<div class="px-6"> <div class="px-6">
<ui-pagination class="mt-4" :value="data.meta" :only="['data']"></ui-pagination> <ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div> </div>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import indexHelpers from '../../mixins/indexHelpers'; import {ref, defineProps} from 'vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
export default { const props = defineProps(indexProps);
data: function () { const {router, data, meta} = useIndex(props.data);
return { const deleting = ref(null);
deleting: null,
};
},
methods: { function remove() {
remove() { router.delete(deleting.value.links.destroy, {
var _self = this; preserveState: true,
this.$inertia.delete(this.deleting.links.destroy, { onSuccess: (page) => {
preserveState: true, data.value = page.props.data.data;
onSuccess(page) { meta.value = page.props.data.meta;
_self.inner = page.props.data; deleting.value = null;
_self.deleting = null;
},
});
}, },
}, });
}
mixins: [indexHelpers],
};
</script> </script>

View File

@ -9,60 +9,60 @@
<form id="form" class="p-3 grid gap-3" @submit.prevent="submit"> <form id="form" class="p-3 grid gap-3" @submit.prevent="submit">
<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" name="name" v-model="model.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" name="gateway_id" :options="meta.gateways" v-model="model.gateway_id" 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> </div>
</ui-box> </ui-box>
<ui-box heading="Filterregeln" v-if="members !== null"> <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 <f-multipleselect
id="activity_ids" id="activity_ids"
v-model="model.filter.activity_ids"
name="activity_ids" name="activity_ids"
:options="members.meta.filterActivities" :options="members.meta.filterActivities"
v-model="model.filter.activity_ids"
@update:modelValue="reload(1)"
label="Tätigkeit" label="Tätigkeit"
size="sm" size="sm"
@update:model-value="reload(1)"
></f-multipleselect> ></f-multipleselect>
<f-multipleselect <f-multipleselect
id="subactivity_ids" id="subactivity_ids"
v-model="model.filter.subactivity_ids"
name="subactivity_ids" name="subactivity_ids"
:options="members.meta.filterSubactivities" :options="members.meta.filterSubactivities"
v-model="model.filter.subactivity_ids"
@update:modelValue="reload(1)"
label="Unterttätigkeit" label="Unterttätigkeit"
size="sm" size="sm"
@update:model-value="reload(1)"
></f-multipleselect> ></f-multipleselect>
<f-multipleselect <f-multipleselect
id="include" id="include"
v-model="model.filter.include"
name="include" name="include"
:options="members.meta.members" :options="members.meta.members"
v-model="model.filter.include"
@update:modelValue="reload(1)"
label="Zusätzliche Mitglieder" label="Zusätzliche Mitglieder"
size="sm" size="sm"
@update:model-value="reload(1)"
></f-multipleselect> ></f-multipleselect>
<f-multipleselect <f-multipleselect
id="exclude" id="exclude"
v-model="model.filter.exclude"
name="exclude" name="exclude"
:options="members.meta.members" :options="members.meta.members"
v-model="model.filter.exclude"
@update:modelValue="reload(1)"
label="Mitglieder ausschließen" label="Mitglieder ausschließen"
size="sm" size="sm"
@update:model-value="reload(1)"
></f-multipleselect> ></f-multipleselect>
<f-multipleselect <f-multipleselect
id="groupIds" id="groupIds"
v-model="model.filter.group_ids"
name="groupIds" name="groupIds"
:options="members.meta.groups" :options="members.meta.groups"
v-model="model.filter.group_ids"
@update:modelValue="reload(1)"
label="Gruppierungen" label="Gruppierungen"
size="sm" size="sm"
@update:model-value="reload(1)"
></f-multipleselect> ></f-multipleselect>
</div> </div>
</ui-box> </ui-box>
<ui-box heading="Mitglieder" v-if="members !== null"> <ui-box v-if="members !== null" heading="Mitglieder">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table"> <table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead> <thead>
<th></th> <th></th>
@ -80,49 +80,46 @@
<td v-text="member.email_parents"></td> <td v-text="member.email_parents"></td>
</tr> </tr>
</table> </table>
<ui-pagination class="mt-4" @reload="reload" :value="members.meta" :only="['data']"></ui-pagination> <ui-pagination class="mt-4" :value="members.meta" :only="['data']" @reload="reload"></ui-pagination>
</ui-box> </ui-box>
</form> </form>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import indexHelpers from '../../mixins/indexHelpers.js'; import {ref, inject, defineProps} from 'vue';
import hasFlash from '../../mixins/hasFlash.js'; import {useIndex} from '../../composables/useIndex.js';
export default { const props = defineProps({
mixins: [indexHelpers, hasFlash], data: {
default: () => undefined,
data: function () { type: Object,
return {
model: this.data === undefined ? {...this.meta.default_model} : {...this.data},
members: null,
};
}, },
meta: {
props: { type: Object,
data: {}, default: () => {},
meta: {},
}, },
});
methods: { const {toFilterString, router} = useIndex({data: [], meta: {}});
async reload(page) {
this.members = (
await this.axios.post('/api/member/search', {
page: page || 1,
filter: this.toFilterString(this.model.filter),
})
).data;
},
async submit() { const model = ref(props.data === undefined ? {...props.meta.default_model} : {...props.data});
this.model.id ? await this.axios.patch(this.model.links.update, this.model) : await this.axios.post('/maildispatcher', this.model); const members = ref(null);
this.$inertia.visit(this.meta.links.index); const axios = inject('axios');
},
},
async created() { async function reload(page) {
this.reload(); members.value = (
}, await axios.post('/api/member/search', {
}; page: page || 1,
filter: toFilterString(model.value.filter),
})
).data;
}
reload();
async function submit() {
model.value.id ? await axios.patch(model.value.links.update, model.value) : await axios.post('/maildispatcher', model.value);
router.visit(props.meta.links.index);
}
</script> </script>

View File

@ -1,44 +1,44 @@
<template> <template>
<page-layout> <page-layout>
<template #toolbar> <template #toolbar>
<page-toolbar-button @click.prevent="model = {...data.meta.default}" color="primary" icon="plus">Neue Verbindung</page-toolbar-button> <page-toolbar-button color="primary" icon="plus" @click.prevent="model = {...meta.default}">Neue Verbindung</page-toolbar-button>
</template> </template>
<ui-popup :heading="model.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" v-if="model !== null" @close="model = null"> <ui-popup v-if="model !== null" :heading="model.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" @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 v-model="model.name" name="name" id="name" label="Bezeichnung" required></f-text> <f-text id="name" v-model="model.name" name="name" label="Bezeichnung" required></f-text>
<f-text v-model="model.domain" name="domain" id="domain" label="Domain" required></f-text> <f-text id="domain" v-model="model.domain" name="domain" label="Domain" required></f-text>
<f-select <f-select
:modelValue="model.type.cls" id="type"
@update:modelValue=" :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},
} }
" "
label="Typ"
name="type"
id="type"
:options="data.meta.types"
:placeholder="''"
required
></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 <f-text
:key="index"
v-if="field.type === 'text' || field.type === 'password' || field.type === 'email'" 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" :label="field.label"
:type="field.type" :type="field.type"
:name="field.name" :name="field.name"
:id="field.name"
v-model="model.type.params[field.name]"
:required="field.is_required" :required="field.is_required"
></f-text> ></f-text>
</template> </template>
</section> </section>
<section class="flex mt-4 space-x-2"> <section class="flex mt-4 space-x-2">
<ui-button type="submit" class="btn-danger">Speichern</ui-button> <ui-button type="submit" class="btn-danger">Speichern</ui-button>
<ui-button @click.prevent="model = null" class="btn-primary">Abbrechen</ui-button> <ui-button class="btn-primary" @click.prevent="model = null">Abbrechen</ui-button>
</section> </section>
</form> </form>
</ui-popup> </ui-popup>
@ -53,7 +53,7 @@
<th>Aktion</th> <th>Aktion</th>
</thead> </thead>
<tr v-for="(gateway, index) in inner.data" :key="index"> <tr v-for="(gateway, index) in data" :key="index">
<td v-text="gateway.name"></td> <td v-text="gateway.name"></td>
<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>
@ -65,49 +65,36 @@
></ui-boolean-display> ></ui-boolean-display>
</td> </td>
<td> <td>
<a href="#" v-tooltip="`Bearbeiten`" @click.prevent="model = {...gateway}" class="inline-flex btn btn-warning btn-sm"><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> </td>
</tr> </tr>
</table> </table>
<div class="px-6"> <div class="px-6">
<ui-pagination class="mt-4" :value="data.meta" :only="['data']"></ui-pagination> <ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div> </div>
</div> </div>
</setting-layout> </setting-layout>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import {ref, inject} from 'vue';
import {indexProps, useIndex} from '../../composables/useIndex.js';
import SettingLayout from '../setting/Layout.vue'; import SettingLayout from '../setting/Layout.vue';
import indexHelpers from '../../mixins/indexHelpers.js';
export default { const props = defineProps(indexProps);
mixins: [indexHelpers], const {meta, data, reload} = useIndex(props.data);
const model = ref(null);
const axios = inject('axios');
data: function () { function getType(type) {
return { return meta.value.types.find((t) => t.id === type);
model: null, }
inner: {...this.data}, async function submit() {
}; await axios[model.value.id ? 'patch' : 'post'](model.value.id ? model.value.links.update : meta.value.links.store, model.value);
},
props: {
data: {},
},
methods: { reload();
getType(type) { model.value = null;
return this.data.meta.types.find((t) => t.id === type); }
},
async submit() {
await this.axios[this.model.id ? 'patch' : 'post'](this.model.id ? this.model.links.update : this.data.meta.links.store, this.model);
this.reload();
this.model = null;
},
},
components: {
SettingLayout,
},
};
</script> </script>

View File

@ -1,63 +1,63 @@
<template> <template>
<page-layout page-class="pb-6"> <page-layout page-class="pb-6">
<template #toolbar> <template #toolbar>
<page-toolbar-button :href="data.meta.links.create" color="primary" icon="plus">Mitglied anlegen</page-toolbar-button> <page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied anlegen</page-toolbar-button>
<page-toolbar-button :href="data.meta.links.allpayment" color="primary" icon="invoice" v-if="hasModule('bill')">Rechnungen erstellen</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 :href="data.meta.links.sendpayment" color="info" icon="envelope" v-if="hasModule('bill')">Rechnungen versenden</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 heading="Mitglied löschen?" v-if="deleting !== null" @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 entfernt.</p> <p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls entfernt.</p>
<ui-note class="mt-5" type="warning" v-if="!deleting.member.has_nami"> 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 class="mt-5" type="danger" v-if="deleting.member.has_nami"> <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> </ui-note>
<div class="grid grid-cols-2 gap-3 mt-6"> <div class="grid grid-cols-2 gap-3 mt-6">
<a href="#" @click.prevent="deleting.resolve" class="text-center btn btn-danger">Mitglied loschen</a> <a href="#" class="text-center btn btn-danger" @click.prevent="deleting.resolve">Mitglied loschen</a>
<a href="#" @click.prevent="deleting.reject" class="text-center btn btn-primary">Abbrechen</a> <a href="#" class="text-center btn btn-primary" @click.prevent="deleting.reject">Abbrechen</a>
</div> </div>
</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 :modelValue="getFilter('search')" @update:modelValue="setFilter('search', $event)" id="search" name="search" label="Suchen …" size="sm"></f-text> <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" @update:modelValue="setFilter('ausstand', $event)" :modelValue="getFilter('ausstand')" label="Nur Ausstände" size="sm"></f-switch> <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 <f-multipleselect
id="group_ids" id="group_ids"
@update:modelValue="setFilter('group_ids', $event)" :options="meta.groups"
:options="data.meta.groups" :model-value="getFilter('group_ids')"
:modelValue="getFilter('group_ids')"
label="Gruppierungen" label="Gruppierungen"
size="sm" size="sm"
name="group_ids" name="group_ids"
@update:model-value="setFilter('group_ids', $event)"
></f-multipleselect> ></f-multipleselect>
<f-select <f-select
v-show="hasModule('bill')" v-show="hasModule('bill')"
name="billKinds"
id="billKinds" id="billKinds"
@update:modelValue="setFilter('bill_kind', $event)" name="billKinds"
:options="data.meta.billKinds" :options="meta.billKinds"
:modelValue="getFilter('bill_kind')" :model-value="getFilter('bill_kind')"
label="Rechnung" label="Rechnung"
size="sm" size="sm"
@update:model-value="setFilter('bill_kind', $event)"
></f-select> ></f-select>
<f-multipleselect <f-multipleselect
id="activity_ids" id="activity_ids"
@update:modelValue="setFilter('activity_ids', $event)" :options="meta.filterActivities"
:options="data.meta.filterActivities" :model-value="getFilter('activity_ids')"
:modelValue="getFilter('activity_ids')"
label="Tätigkeiten" label="Tätigkeiten"
size="sm" size="sm"
name="activity_ids" name="activity_ids"
@update:model-value="setFilter('activity_ids', $event)"
></f-multipleselect> ></f-multipleselect>
<f-multipleselect <f-multipleselect
id="subactivity_ids" id="subactivity_ids"
@update:modelValue="setFilter('subactivity_ids', $event)" :options="meta.filterSubactivities"
:options="data.meta.filterSubactivities" :model-value="getFilter('subactivity_ids')"
:modelValue="getFilter('subactivity_ids')"
label="Untertätigkeiten" label="Untertätigkeiten"
size="sm" size="sm"
name="subactivity_ids" name="subactivity_ids"
@update:model-value="setFilter('subactivity_ids', $event)"
></f-multipleselect> ></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>
@ -73,19 +73,21 @@
<th class="hidden 2xl:table-cell">Ort</th> <th class="hidden 2xl:table-cell">Ort</th>
<th>Tags</th> <th>Tags</th>
<th class="hidden xl:table-cell">Alter</th> <th class="hidden xl:table-cell">Alter</th>
<th class="hidden xl:table-cell" v-show="hasModule('bill')">Rechnung</th> <th v-show="hasModule('bill')" class="hidden xl:table-cell">Rechnung</th>
<th v-show="hasModule('bill')">Ausstand</th> <th v-show="hasModule('bill')">Ausstand</th>
<th></th> <th></th>
</thead> </thead>
<tr v-for="(member, index) in inner.data" :key="index"> <tr v-for="(member, index) in data" :key="index">
<td><ui-age-groups :member="member"></ui-age-groups></td> <td><ui-age-groups :member="member"></ui-age-groups></td>
<td v-text="member.lastname"></td> <td v-text="member.lastname"></td>
<td v-text="member.firstname"></td> <td v-text="member.firstname"></td>
<td class="hidden 2xl:table-cell" v-text="member.full_address"></td> <td class="hidden 2xl:table-cell" v-text="member.full_address"></td>
<td><tags :member="member"></tags></td> <td>
<tags :member="member"></tags>
</td>
<td class="hidden xl:table-cell" v-text="member.age"></td> <td class="hidden xl:table-cell" v-text="member.age"></td>
<td class="hidden xl:table-cell" v-show="hasModule('bill')"> <td v-show="hasModule('bill')" class="hidden xl:table-cell">
<ui-label :value="member.bill_kind_name" fallback="kein"></ui-label> <ui-label :value="member.bill_kind_name" fallback="kein"></ui-label>
</td> </td>
<td v-show="hasModule('bill')"> <td v-show="hasModule('bill')">
@ -98,101 +100,82 @@
</table> </table>
<div class="md:hidden p-3 grid gap-3"> <div class="md:hidden p-3 grid gap-3">
<ui-box class="relative" :heading="member.fullname" v-for="(member, index) in data.data" :key="index"> <ui-box v-for="(member, index) in data.data" :key="index" class="relative" :heading="member.fullname">
<div slot="in-title"> <template #in-title>
<ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></ui-age-groups> <ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></ui-age-groups>
</div> </template>
<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 class="text-gray-100 block" v-show="hasModule('bill')" :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> </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"> <div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link :href="member.links.show" v-tooltip="`Details`"><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> </div>
</ui-box> </ui-box>
</div> </div>
<div class="px-6"> <div class="px-6">
<ui-pagination class="mt-4" :value="data.meta" :only="['data']"></ui-pagination> <ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div> </div>
<member-payments <member-payments
v-if="single !== null && sidebar === 'payment.index'" v-if="single !== null && sidebar === 'payment.index'"
:subscriptions="meta.subscriptions"
:statuses="meta.statuses"
:value="data[single]"
@close="closeSidebar" @close="closeSidebar"
:subscriptions="data.meta.subscriptions"
:statuses="data.meta.statuses"
:value="data.data[single]"
></member-payments> ></member-payments>
<member-memberships <member-memberships
v-if="single !== null && sidebar === 'membership.index'" v-if="single !== null && sidebar === 'membership.index'"
:groups="meta.groups"
:activities="meta.formActivities"
:subactivities="meta.formSubactivities"
:value="data[single]"
@close="closeSidebar" @close="closeSidebar"
:groups="data.meta.groups"
:activities="data.meta.formActivities"
:subactivities="data.meta.formSubactivities"
:value="data.data[single]"
></member-memberships> ></member-memberships>
<member-courses v-if="single !== null && sidebar === 'courses.index'" @close="closeSidebar" :courses="data.meta.courses" :value="data.data[single]"></member-courses> <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>
<script> <script setup>
import MemberPayments from './MemberPayments.vue'; import MemberPayments from './MemberPayments.vue';
import MemberMemberships from './MemberMemberships.vue'; import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue'; import MemberCourses from './MemberCourses.vue';
import indexHelpers from '../../mixins/indexHelpers.js';
import hasModule from '../../mixins/hasModule.js';
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 {ref, defineProps} from 'vue';
export default { const sidebar = ref(null);
data: function () { const single = ref(null);
return { const deleting = ref(null);
sidebar: null,
single: null,
deleting: null,
};
},
mixins: [indexHelpers, hasModule], const props = defineProps(indexProps);
var {router, data, meta, getFilter, setFilter, filterString} = useIndex(props.data);
components: { function exportMembers() {
MemberMemberships, window.open(`/member-export?filter=${filterString.value}`);
MemberPayments, }
MemberCourses,
Tags,
Actions,
},
methods: { async function remove(member) {
exportMembers() { new Promise((resolve, reject) => {
window.open(`/member-export?filter=${this.filterString}`); deleting.value = {resolve, reject, member};
}, })
async remove(member) { .then(() => {
new Promise((resolve, reject) => { router.delete(`/member/${member.id}`);
this.deleting = {resolve, reject, member}; deleting.value = null;
}) })
.then(() => { .catch(() => (deleting.value = null));
this.$inertia.delete(`/member/${member.id}`); }
this.deleting = null;
})
.catch(() => {
this.deleting = null;
});
},
openSidebar(index, name) {
this.single = index;
this.sidebar = name;
},
closeSidebar() {
this.single = null;
this.sidebar = null;
},
},
props: { function openSidebar(index, name) {
query: {}, single.value = index;
}, sidebar.value = name;
}; }
function closeSidebar() {
single.value = null;
sidebar.value = null;
}
</script> </script>

View File

@ -4,7 +4,7 @@ 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\Member\DeleteJob; use App\Member\DeleteAction;
use App\Member\Member; use App\Member\Member;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -26,7 +26,7 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member'); $response->assertRedirect('/member');
Queue::assertPushed(DeleteJob::class, fn ($job) => 123 === $job->namiId); DeleteAction::assertPushed();
$this->assertDatabaseMissing('members', [ $this->assertDatabaseMissing('members', [
'id' => $member->id, 'id' => $member->id,
]); ]);
@ -42,7 +42,7 @@ class DeleteTest extends TestCase
$response->assertRedirect('/member'); $response->assertRedirect('/member');
Queue::assertNotPushed(DeleteJob::class); Queue::assertNotPushed(DeleteAction::class);
$this->assertDatabaseMissing('members', [ $this->assertDatabaseMissing('members', [
'id' => $member->id, 'id' => $member->id,
]); ]);
@ -54,7 +54,7 @@ class DeleteTest extends TestCase
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$member = Member::factory()->defaults()->inNami(123)->create(); $member = Member::factory()->defaults()->inNami(123)->create();
dispatch(new DeleteJob(123)); DeleteAction::dispatch(123);
app(MemberFake::class)->assertDeleted(123, Carbon::parse('yesterday')); app(MemberFake::class)->assertDeleted(123, Carbon::parse('yesterday'));
} }