Compare commits

...

3 Commits

Author SHA1 Message Date
Philipp Lang 1e836b2a28 Fix bill table
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is failing Details
2023-11-16 12:10:07 +01:00
Philipp Lang f01c4f9d02 Lint 2023-11-16 12:07:07 +01:00
Philipp Lang 613096220f Add module management 2023-11-16 10:53:17 +01:00
15 changed files with 351 additions and 112 deletions

View File

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

View File

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

View File

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

35
app/Module/Module.php Normal file
View File

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

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

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

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

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

View File

@ -94,18 +94,6 @@ return [
'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

View File

@ -4,29 +4,9 @@ use Spatie\LaravelSettings\Migrations\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
{
$defaults = $this->defaults(config('app.mode'));
$this->migrator->add('general.modules', $defaults['modules']);
$this->migrator->add('general.single_view', $defaults['single_view']);
$this->migrator->add('general.modules', []);
$this->migrator->add('general.single_view', false);
}
}

View File

@ -0,0 +1,14 @@
<?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,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>
<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-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>
@ -56,8 +73,8 @@
<th class="!hidden 2xl:!table-cell">Ort</th>
<th>Tags</th>
<th class="!hidden xl:!table-cell">Alter</th>
<th v-show="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th>
<th v-show="hasModule('bill')">Ausstand</th>
<th v-if="hasModule('bill')" class="!hidden xl:!table-cell">Rechnung</th>
<th v-if="hasModule('bill')">Ausstand</th>
<th></th>
</thead>
@ -70,10 +87,10 @@
<tags :member="member"></tags>
</td>
<td class="!hidden xl:!table-cell" v-text="member.age"></td>
<td v-show="hasModule('bill')" class="!hidden xl:!table-cell">
<td v-if="hasModule('bill')" class="!hidden xl:!table-cell">
<ui-label :value="member.bill_kind_name" fallback="kein"></ui-label>
</td>
<td v-show="hasModule('bill')">
<td v-if="hasModule('bill')">
<ui-label :value="member.pending_payment" fallback="---"></ui-label>
</td>
<td>
@ -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($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">
<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>
</ui-box>
</div>
@ -107,12 +121,9 @@
</div>
<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-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>
<member-payments v-if="single.type === 'payment'" :url="single.model.links.payment_index" @close="closeSidebar"></member-payments>
<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>
</page-layout>
</template>

View File

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

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