Compare commits

..

No commits in common. "1e836b2a28169114d351ec6a20f00ef5309bc02f" and "fc368bcd1a5009ef719f2bc9a8ab7be979a4e8da" have entirely different histories.

15 changed files with 112 additions and 351 deletions

View File

@ -3,6 +3,7 @@ APP_ENV=production
APP_KEY=YOUR_APP_KEY APP_KEY=YOUR_APP_KEY
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://localhost:8000 APP_URL=http://localhost:8000
APP_MODE=stamm
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io MAIL_HOST=smtp.mailtrap.io

View File

@ -41,6 +41,7 @@ steps:
APP_ENV: local APP_ENV: local
APP_DEBUG: true APP_DEBUG: true
APP_URL: http://scoutrobot.test APP_URL: http://scoutrobot.test
APP_MODE: stamm
LOG_CHANNEL: stack LOG_CHANNEL: stack
DB_CONNECTION: mysql DB_CONNECTION: mysql
DB_HOST: db DB_HOST: db

View File

@ -3,7 +3,7 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Http\Resources\UserResource; use App\Http\Resources\UserResource;
use App\Module\ModuleSettings; use App\Setting\GeneralSettings;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Inertia\Middleware; use Inertia\Middleware;
@ -53,7 +53,7 @@ class HandleInertiaRequests extends Middleware
return session()->get('title', ''); return session()->get('title', '');
}, },
'settings' => [ 'settings' => [
'modules' => app(ModuleSettings::class)->modules, 'modules' => app(GeneralSettings::class)->modules,
], ],
]; ];
} }

View File

@ -1,35 +0,0 @@
<?php
namespace App\Module;
enum Module: string
{
case BILL = 'bill';
case COURSE = 'course';
public function title(): string
{
return match ($this) {
static::BILL => 'Zahlungs-Management',
static::COURSE => 'Ausbildung',
};
}
/**
* @return array<int, array{id: string, name: string}>
*/
public static function forSelect(): array
{
return array_map(fn ($module) => ['id' => $module->value, 'name' => $module->title()], static::cases());
}
/**
* @return array<int, string>
*/
public static function values(): array
{
return array_map(fn ($module) => $module->value, static::cases());
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Module;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class ModuleIndexAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function handle(ModuleSettings $settings): array
{
return [
'data' => [
'modules' => $settings->modules,
],
'meta' => ['modules' => Module::forSelect()],
];
}
public function asController(ModuleSettings $settings): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Module');
return Inertia::render('setting/Module', [
'data' => $this->handle($settings),
]);
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Module;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings;
class ModuleSettings extends LocalSettings implements Indexable, Storeable
{
/** @var array<int, string> */
public array $modules;
public static function group(): string
{
return 'module';
}
public static function slug(): string
{
return 'module';
}
public static function title(): string
{
return 'Module';
}
public static function indexAction(): string
{
return ModuleIndexAction::class;
}
public static function storeAction(): string
{
return ModuleStoreAction::class;
}
public function hasModule(string $module): bool
{
return in_array($module, $this->modules);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace App\Module;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\Rule;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ModuleStoreAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*/
public function handle(array $input): void
{
$settings = app(ModuleSettings::class);
$settings->fill([
'modules' => $input['modules'],
]);
$settings->save();
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'modules' => 'present|array',
'modules.*' => ['string', Rule::in(Module::values())],
];
}
public function asController(ActionRequest $request): RedirectResponse
{
$this->handle($request->validated());
return redirect()->back();
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Setting;
use Spatie\LaravelSettings\Settings;
class GeneralSettings extends Settings
{
/** @var array<int, string> */
public array $modules;
public bool $single_view;
/** @var array<int, int> */
public array $allowed_nami_accounts;
public static function group(): string
{
return 'general';
}
public function hasModule(string $module): bool
{
return in_array($module, $this->modules);
}
}

View File

@ -4,7 +4,6 @@ namespace App\Setting;
use App\Invoice\InvoiceSettings; use App\Invoice\InvoiceSettings;
use App\Mailgateway\MailgatewaySettings; use App\Mailgateway\MailgatewaySettings;
use App\Module\ModuleSettings;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class SettingServiceProvider extends ServiceProvider class SettingServiceProvider extends ServiceProvider
@ -26,7 +25,6 @@ class SettingServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
app(SettingFactory::class)->register(ModuleSettings::class);
app(SettingFactory::class)->register(InvoiceSettings::class); app(SettingFactory::class)->register(InvoiceSettings::class);
app(SettingFactory::class)->register(MailgatewaySettings::class); app(SettingFactory::class)->register(MailgatewaySettings::class);
app(SettingFactory::class)->register(NamiSettings::class); app(SettingFactory::class)->register(NamiSettings::class);

View File

@ -94,6 +94,18 @@ return [
'fallback_locale' => 'en', 'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| App Mode
|--------------------------------------------------------------------------
|
| The mode of the app will set some default settings for you on initial
| database setup. You can change these Settings anytime later, but it
| usually defines a good starting point for you to set up the world.
|
*/
'mode' => env('APP_MODE', 'stamm'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Faker Locale | Faker Locale

View File

@ -4,9 +4,29 @@ use Spatie\LaravelSettings\Migrations\SettingsMigration;
class CreateGeneralSettings extends SettingsMigration class CreateGeneralSettings extends SettingsMigration
{ {
/**
* @return array<string, array<int,string>|bool>
*/
public function defaults(string $mode): array
{
$defaults = [
'diözese' => [
'modules' => ['courses'],
'single_view' => false,
],
'stamm' => [
'modules' => ['bill', 'courses'],
'single_view' => true,
],
];
return $defaults[$mode];
}
public function up(): void public function up(): void
{ {
$this->migrator->add('general.modules', []); $defaults = $this->defaults(config('app.mode'));
$this->migrator->add('general.single_view', false); $this->migrator->add('general.modules', $defaults['modules']);
$this->migrator->add('general.single_view', $defaults['single_view']);
} }
} }

View File

@ -1,14 +0,0 @@
<?php
use App\Module\Module;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->delete('general.modules');
$this->migrator->delete('general.single_view');
$this->migrator->add('module.modules', collect(Module::cases())->map(fn ($module) => $module->value));
}
};

View File

@ -1,17 +1,23 @@
<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 anlegen</page-toolbar-button> <page-toolbar-button :href="meta.links.create" color="primary" icon="plus">Mitglied
<page-toolbar-button v-if="hasModule('bill')" :href="meta.links.allpayment" color="primary" icon="invoice">Rechnungen erstellen</page-toolbar-button> anlegen</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 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> </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 entfernt.</p> <p class="mt-2">Alle Zuordnungen (Ausbildungen, Rechnungen, Zahlungen, Tätigkeiten) werden ebenfalls
<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> 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"> <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="#" 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>
@ -20,45 +26,22 @@
</div> </div>
</ui-popup> </ui-popup>
<page-filter breakpoint="xl"> <page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> <f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
<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> @update:model-value="setFilter('search', $event)"></f-text>
<f-multipleselect <f-switch v-show="hasModule('bill')" id="ausstand" :model-value="getFilter('ausstand')" label="Nur Ausstände"
id="group_ids" size="sm" @update:model-value="setFilter('ausstand', $event)"></f-switch>
:options="meta.groups" <f-multipleselect id="group_ids" :options="meta.groups" :model-value="getFilter('group_ids')"
:model-value="getFilter('group_ids')" label="Gruppierungen" size="sm" name="group_ids"
label="Gruppierungen" @update:model-value="setFilter('group_ids', $event)"></f-multipleselect>
size="sm" <f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds"
name="group_ids" :model-value="getFilter('bill_kind')" label="Rechnung" size="sm"
@update:model-value="setFilter('group_ids', $event)" @update:model-value="setFilter('bill_kind', $event)"></f-select>
></f-multipleselect> <f-multipleselect id="activity_ids" :options="meta.filterActivities" :model-value="getFilter('activity_ids')"
<f-select label="Tätigkeiten" size="sm" name="activity_ids"
v-show="hasModule('bill')" @update:model-value="setFilter('activity_ids', $event)"></f-multipleselect>
id="billKinds" <f-multipleselect id="subactivity_ids" :options="meta.filterSubactivities"
name="billKinds" :model-value="getFilter('subactivity_ids')" label="Untertätigkeiten" size="sm" name="subactivity_ids"
:options="meta.billKinds" @update:model-value="setFilter('subactivity_ids', $event)"></f-multipleselect>
: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>
@ -73,8 +56,8 @@
<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 v-if="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th> <th v-show="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th>
<th v-if="hasModule('bill')">Ausstand</th> <th v-show="hasModule('bill')">Ausstand</th>
<th></th> <th></th>
</thead> </thead>
@ -87,10 +70,10 @@
<tags :member="member"></tags> <tags :member="member"></tags>
</td> </td>
<td class="!hidden xl:!table-cell" v-text="member.age"></td> <td class="!hidden xl:!table-cell" v-text="member.age"></td>
<td v-if="hasModule('bill')" class="!hidden xl:!table-cell"> <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-if="hasModule('bill')"> <td v-show="hasModule('bill')">
<ui-label :value="member.pending_payment" fallback="---"></ui-label> <ui-label :value="member.pending_payment" fallback="---"></ui-label>
</td> </td>
<td> <td>
@ -107,11 +90,14 @@
<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" 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($event, member)" @remove="remove(member)"> </actions> <actions class="mt-2" :member="member" @sidebar="openSidebar($event, member)" @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 v-tooltip="`Details`" :href="member.links.show"><ui-sprite src="chevron" 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"
class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
</div> </div>
</ui-box> </ui-box>
</div> </div>
@ -121,9 +107,12 @@
</div> </div>
<ui-sidebar v-if="single !== null" @close="closeSidebar"> <ui-sidebar v-if="single !== null" @close="closeSidebar">
<member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index" @close="closeSidebar"></member-payments> <member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index"
<member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index" @close="closeSidebar"></member-memberships> @close="closeSidebar"></member-payments>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index" @close="closeSidebar"></member-courses> <member-memberships v-if="single.type === 'membership'" :url="single.model.links.membership_index"
@close="closeSidebar"></member-memberships>
<member-courses v-if="single.type === 'courses'" :url="single.model.links.course_index"
@close="closeSidebar"></member-courses>
</ui-sidebar> </ui-sidebar>
</page-layout> </page-layout>
</template> </template>
@ -134,14 +123,14 @@ 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 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, reloadPage} = useIndex(props.data, 'member'); var { router, data, meta, getFilter, setFilter, filterString, reloadPage } = useIndex(props.data, 'member');
function exportMembers() { function exportMembers() {
window.open(`/member-export?filter=${filterString.value}`); window.open(`/member-export?filter=${filterString.value}`);
@ -149,7 +138,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

@ -1,51 +0,0 @@
<template>
<page-layout>
<template #right>
<f-save-button form="modulesettingform"></f-save-button>
</template>
<setting-layout>
<form id="modulesettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start"
@submit.prevent="submit">
<div class="col-span-full text-gray-100 mb-3">
<p class="text-sm">Hier kannst du Funktionen innerhalb von Adrema (Module) aktivieren oder deaktivieren
und so den Funktionsumfang auf deine Bedürfnisse anpassen.</p>
</div>
<div class="grid grid-cols-2 gap-4">
<f-switch v-for="module in meta.modules" :id="module.id" v-model="inner.modules" :value="module.id"
size="sm" name="modules" :label="module.name"></f-switch>
</div>
</form>
</setting-layout>
</page-layout>
</template>
<script>
import SettingLayout from './Layout.vue';
export default {
components: {
SettingLayout,
},
props: {
data: {
type: Object,
default: () => {
return {};
},
},
},
data: function () {
return {
inner: { ...this.data.data },
meta: { ...this.data.meta },
};
},
methods: {
submit() {
this.$inertia.post('/setting/module', this.inner, {
onSuccess: () => this.$success('Einstellungen gespeichert.'),
});
},
},
};
</script>

View File

@ -1,63 +0,0 @@
<?php
namespace Tests\Feature;
use App\Module\Module;
use App\Module\ModuleSettings;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class ModuleTest extends TestCase
{
use DatabaseTransactions;
public function testItGetsModuleSettings(): void
{
$this->login()->loginNami();
ModuleSettings::fake(['modules' => ['bill']]);
$response = $this->get('/setting/module');
$response->assertOk();
$this->assertCount(count(Module::cases()), $this->inertia($response, 'data.meta.modules'));
$this->assertInertiaHas([
'name' => 'Zahlungs-Management',
'id' => 'bill',
], $response, 'data.meta.modules.0');
$this->assertEquals(['bill'], $this->inertia($response, 'data.data.modules'));
}
public function testItSavesSettings(): void
{
$this->login()->loginNami();
$response = $this->from('/setting/module')->post('/setting/module', [
'modules' => ['bill'],
]);
$response->assertRedirect('/setting/module');
$this->assertEquals(['bill'], app(ModuleSettings::class)->modules);
}
public function testModuleMustExists(): void
{
$this->login()->loginNami();
$response = $this->from('/setting/module')->post('/setting/module', [
'modules' => ['lalala'],
]);
$response->assertSessionHasErrors('modules.0');
}
public function testItReturnsModulesOnEveryPage(): void
{
$this->login()->loginNami();
ModuleSettings::fake(['modules' => ['bill']]);
$response = $this->get('/');
$this->assertEquals(['bill'], $this->inertia($response, 'settings.modules'));
}
}