Compare commits

...

10 Commits

Author SHA1 Message Date
philipp lang 14925f6cb5 --wip-- [skip ci] 2024-08-01 20:28:59 +02:00
philipp lang 5f31489bab --wip-- [skip ci] 2024-08-01 18:25:25 +02:00
philipp lang be57152bea Clear settings view 2024-08-01 17:30:55 +02:00
philipp lang d9adb526ce Remove Setting store actions
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is failing Details
2024-08-01 16:26:38 +02:00
philipp lang daf6e87814 Remove Middleware 2024-08-01 16:26:38 +02:00
philipp lang 2056e1ad6b Fixed tests 2024-08-01 16:26:38 +02:00
philipp lang a4eba23bff Remove viewable interface 2024-08-01 16:26:38 +02:00
philipp lang ad8cd58b4f Add SettingView action 2024-08-01 16:26:38 +02:00
philipp lang 3a4908f505 Move Indexable to Viewable 2024-08-01 16:26:38 +02:00
philipp lang 291eec3849 Update CHANGELOG
continuous-integration/drone/push Build is passing Details
2024-08-01 12:15:28 +02:00
55 changed files with 578 additions and 965 deletions

View File

@ -4,6 +4,7 @@
- Rechnungen und Erinnerungen werden nun automatisch täglich um 10 Uhr verschickt - Rechnungen und Erinnerungen werden nun automatisch täglich um 10 Uhr verschickt
- Es kann eingestellt werden, nach wie vielen Wochen an Rechnungen erinnert werden soll (Standard: 12) - Es kann eingestellt werden, nach wie vielen Wochen an Rechnungen erinnert werden soll (Standard: 12)
- Name und Profilbild des angemeldeten Benutzers wird nun oben rechts angezeigt
### 1.10.15 ### 1.10.15

View File

@ -1,21 +0,0 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Fileshare\Resources\FileshareResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareApiIndexAction
{
use AsAction;
public function handle(): AnonymousResourceCollection
{
session()->put('menu', 'setting');
session()->put('title', 'Datei-Verbindungen');
return FileshareResource::collection(Fileshare::paginate(15));
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Fileshare\Resources\FileshareResource;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareIndexAction
{
use AsAction;
public function handle(): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Datei-Verbindungen');
return Inertia::render('fileshare/Index', [
'data' => FileshareResource::collection(Fileshare::paginate(15)),
]);
}
}

View File

@ -2,29 +2,27 @@
namespace App\Fileshare; namespace App\Fileshare;
use App\Fileshare\Actions\FileshareIndexAction; use App\Fileshare\Models\Fileshare;
use App\Setting\Contracts\Indexable; use App\Fileshare\Resources\FileshareResource;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
class FileshareSettings extends LocalSettings implements Indexable class FileshareSettings extends LocalSettings
{ {
public static function group(): string public static function group(): string
{ {
return 'fileshare'; return 'fileshare';
} }
public static function slug(): string
{
return 'fileshare';
}
public static function indexAction(): string
{
return FileshareIndexAction::class;
}
public static function title(): string public static function title(): string
{ {
return 'Datei-Verbindungen'; return 'Datei-Verbindungen';
} }
/**
* @inheritdoc
*/
public function data()
{
return FileshareResource::collection(Fileshare::paginate(15));
}
} }

View File

@ -1,36 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\FormSettings;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingIndexAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function handle(FormSettings $settings): array
{
return [
'data' => [
'register_url' => $settings->registerUrl,
'clear_cache_url' => $settings->clearCacheUrl,
],
];
}
public function asController(FormSettings $settings): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Module');
return Inertia::render('setting/Form', [
'data' => $this->handle($settings),
]);
}
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\FormSettings;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingStoreAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*/
public function handle(array $input): void
{
$settings = app(FormSettings::class);
$settings->fill([
'registerUrl' => $input['register_url'],
'clearCacheUrl' => $input['clear_cache_url'],
]);
$settings->save();
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'register_url' => 'present|string',
'clear_cache_url' => 'present|string',
];
}
public function asController(ActionRequest $request): RedirectResponse
{
$this->handle($request->validated());
return redirect()->back();
}
}

View File

@ -2,13 +2,10 @@
namespace App\Form; namespace App\Form;
use App\Form\Actions\SettingIndexAction;
use App\Form\Actions\SettingStoreAction;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
class FormSettings extends LocalSettings implements Indexable, Storeable class FormSettings extends LocalSettings implements Storeable
{ {
public string $registerUrl; public string $registerUrl;
public string $clearCacheUrl; public string $clearCacheUrl;
@ -18,23 +15,19 @@ class FormSettings extends LocalSettings implements Indexable, Storeable
return 'form'; return 'form';
} }
public static function slug(): string
{
return 'form';
}
public static function title(): string public static function title(): string
{ {
return 'Formulare'; return 'Formulare';
} }
public static function indexAction(): string /**
* @inheritdoc
*/
public function rules(): array
{ {
return SettingIndexAction::class; return [
} 'registerUrl' => 'present|string',
'clearCacheUrl' => 'present|string',
public static function storeAction(): string ];
{
return SettingStoreAction::class;
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use App\Lib\HasMeta;
use App\User; use App\User;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Storage; use Storage;
@ -11,6 +12,8 @@ use Storage;
*/ */
class UserResource extends JsonResource class UserResource extends JsonResource
{ {
use HasMeta;
/** /**
* Transform the resource into an array. * Transform the resource into an array.
* *
@ -25,9 +28,21 @@ class UserResource extends JsonResource
'lastname' => $this->lastname, 'lastname' => $this->lastname,
'avatar_url' => $this->getGravatarUrl(), 'avatar_url' => $this->getGravatarUrl(),
'email' => $this->email, 'email' => $this->email,
'id' => $this->id,
'avatar' => [ 'avatar' => [
'src' => Storage::url('avatar.png'), 'src' => Storage::url('avatar.png'),
], ],
]; ];
} }
public static function meta(): array
{
return [
'default' => [
'firstname' => '',
'lastname' => '',
],
'links' => []
];
}
} }

View File

@ -2,11 +2,10 @@
namespace App\Invoice; namespace App\Invoice;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
class InvoiceSettings extends LocalSettings implements Indexable, Storeable class InvoiceSettings extends LocalSettings implements Storeable
{ {
public string $from_long; public string $from_long;
@ -35,19 +34,24 @@ class InvoiceSettings extends LocalSettings implements Indexable, Storeable
return 'bill'; return 'bill';
} }
public static function slug(): string /**
* @inheritdoc
*/
public function rules(): array
{ {
return 'bill'; return [
} 'from_long' => '',
'from' => '',
public static function indexAction(): string 'mobile' => '',
{ 'email' => '',
return SettingIndexAction::class; 'website' => '',
} 'address' => '',
'place' => '',
public static function storeAction(): string 'zip' => '',
{ 'iban' => '',
return SettingSaveAction::class; 'bic' => '',
'rememberWeeks' => '',
];
} }
public static function title(): string public static function title(): string

View File

@ -1,42 +0,0 @@
<?php
namespace App\Invoice;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingIndexAction
{
use AsAction;
/**
* @return array<string, string>
*/
public function handle(InvoiceSettings $settings): array
{
return [
'from_long' => $settings->from_long,
'from' => $settings->from,
'mobile' => $settings->mobile,
'email' => $settings->email,
'website' => $settings->website,
'address' => $settings->address,
'place' => $settings->place,
'zip' => $settings->zip,
'iban' => $settings->iban,
'bic' => $settings->bic,
'remember_weeks' => $settings->rememberWeeks,
];
}
public function asController(InvoiceSettings $settings): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Rechnungs-Einstellungen');
return Inertia::render('setting/Bill', [
'data' => $this->handle($settings),
]);
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Invoice;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingSaveAction
{
use AsAction;
/**
* @param array<string, string> $input
*/
public function handle(array $input): void
{
$settings = app(InvoiceSettings::class);
$settings->fill([
'from_long' => $input['from_long'] ?? '',
'from' => $input['from'] ?? '',
'mobile' => $input['mobile'] ?? '',
'email' => $input['email'] ?? '',
'website' => $input['website'] ?? '',
'address' => $input['address'] ?? '',
'place' => $input['place'] ?? '',
'zip' => $input['zip'] ?? '',
'iban' => $input['iban'] ?? '',
'bic' => $input['bic'] ?? '',
'rememberWeeks' => $input['remember_weeks'] ?? 1,
]);
$settings->save();
}
public function asController(ActionRequest $request): RedirectResponse
{
$this->handle($request->all());
return redirect()->back();
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Mailgateway\Actions;
use App\Mailgateway\Models\Mailgateway;
use App\Mailgateway\Resources\MailgatewayResource;
use Illuminate\Database\Eloquent\Builder;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class IndexAction
{
use AsAction;
/**
* @return Builder<Mailgateway>
*/
public function handle(): Builder
{
return (new Mailgateway())->newQuery();
}
public function asController(): Response
{
session()->put('menu', 'setting');
session()->put('title', 'E-Mail-Verbindungen');
return Inertia::render('mailgateway/Index', [
'data' => MailgatewayResource::collection($this->handle()->paginate(10)),
]);
}
}

View File

@ -2,29 +2,27 @@
namespace App\Mailgateway; namespace App\Mailgateway;
use App\Mailgateway\Actions\IndexAction; use App\Mailgateway\Models\Mailgateway;
use App\Setting\Contracts\Indexable; use App\Mailgateway\Resources\MailgatewayResource;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
class MailgatewaySettings extends LocalSettings implements Indexable class MailgatewaySettings extends LocalSettings
{ {
public static function group(): string public static function group(): string
{ {
return 'mailgateway'; return 'mailgateway';
} }
public static function slug(): string
{
return 'mailgateway';
}
public static function indexAction(): string
{
return IndexAction::class;
}
public static function title(): string public static function title(): string
{ {
return 'E-Mail-Verbindungen'; return 'E-Mail-Verbindungen';
} }
/**
* @inheritdoc
*/
public function data()
{
return MailgatewayResource::collection(Mailgateway::paginate(10));
}
} }

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

@ -2,11 +2,12 @@
namespace App\Module; namespace App\Module;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
use Illuminate\Validation\Rule;
use Lorisleiva\Actions\ActionRequest;
class ModuleSettings extends LocalSettings implements Indexable, Storeable class ModuleSettings extends LocalSettings implements Storeable
{ {
/** @var array<int, string> */ /** @var array<int, string> */
public array $modules; public array $modules;
@ -16,28 +17,38 @@ class ModuleSettings extends LocalSettings implements Indexable, Storeable
return 'module'; return 'module';
} }
public static function slug(): string
{
return 'module';
}
public static function title(): string public static function title(): string
{ {
return 'Module'; return 'Module';
} }
public static function indexAction(): string
{
return ModuleIndexAction::class;
}
public static function storeAction(): string
{
return ModuleStoreAction::class;
}
public function hasModule(string $module): bool public function hasModule(string $module): bool
{ {
return in_array($module, $this->modules); return in_array($module, $this->modules);
} }
/**
* @inheritdoc
*/
public function rules(): array
{
return [
'modules' => 'present|array',
'modules.*' => ['string', Rule::in(Module::values())],
];
}
/**
* @inheritdoc
*/
public function data(): array
{
return [
...parent::data(),
'meta' => [
...parent::data()['meta'],
'modules' => Module::forSelect(),
]
];
}
} }

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

@ -1,35 +0,0 @@
<?php
namespace App\Nami\Actions;
use App\Setting\NamiSettings;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingIndexAction
{
use AsAction;
/**
* @return array<string, string>
*/
public function handle(NamiSettings $settings): array
{
return [
'mglnr' => $settings->mglnr,
'password' => '',
'default_group_id' => $settings->default_group_id,
];
}
public function asController(NamiSettings $settings): Response
{
session()->put('menu', 'setting');
session()->put('title', 'NaMi-Settings');
return Inertia::render('setting/Nami', [
'data' => $this->handle($settings),
]);
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Nami\Actions;
use App\Initialize\Actions\NamiLoginCheckAction;
use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingSaveAction
{
use AsAction;
/**
* @param array<string, string> $input
*/
public function handle(array $input): void
{
$settings = app(NamiSettings::class);
$settings->fill([
'mglnr' => $input['mglnr'] ?? '',
'password' => $input['password'] ?? '',
'default_group_id' => $input['default_group_id'] ?? '',
]);
$settings->save();
}
public function asController(ActionRequest $request): RedirectResponse
{
NamiLoginCheckAction::run([
'mglnr' => $request->mglnr,
'password' => $request->password,
]);
$this->handle($request->all());
return redirect()->back();
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Prevention\Actions;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class PreventionIndexAction
{
use AsAction;
public function handle(): Response
{
session()->put('menu', 'setting');
session()->put('title', 'Prävention');
return Inertia::render('setting/Prevention');
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Prevention\Actions;
use App\Prevention\PreventionSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingApiAction
{
use AsAction;
public function handle(): JsonResponse
{
return response()->json([
'data' => app(PreventionSettings::class)->toArray(),
]);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Prevention\Actions;
use App\Lib\Editor\EditorData;
use App\Lib\Events\Succeeded;
use App\Prevention\PreventionSettings;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SettingStoreAction
{
use AsAction;
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'formmail' => 'array',
];
}
public function handle(ActionRequest $request): void
{
$settings = app(PreventionSettings::class);
$settings->formmail = EditorData::from($request->formmail);
$settings->save();
Succeeded::message('Einstellungen gespeichert.')->dispatch();
}
}

View File

@ -3,11 +3,11 @@
namespace App\Prevention; namespace App\Prevention;
use App\Lib\Editor\EditorData; use App\Lib\Editor\EditorData;
use App\Prevention\Actions\PreventionIndexAction; use App\Setting\Contracts\Storeable;
use App\Setting\Contracts\Indexable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest;
class PreventionSettings extends LocalSettings implements Indexable class PreventionSettings extends LocalSettings implements Storeable
{ {
public EditorData $formmail; public EditorData $formmail;
@ -17,18 +17,25 @@ class PreventionSettings extends LocalSettings implements Indexable
return 'prevention'; return 'prevention';
} }
public static function slug(): string
{
return 'prevention';
}
public static function indexAction(): string
{
return PreventionIndexAction::class;
}
public static function title(): string public static function title(): string
{ {
return 'Prävention'; return 'Prävention';
} }
/**
* @inheritdoc
*/
public function rules(): array
{
return [
'formmail' => 'required',
];
}
public function saveAttributes(ActionRequest $request): array
{
return [
'formmail' => EditorData::from($request->formmail),
];
}
} }

View File

@ -0,0 +1,44 @@
<?php
namespace App\Setting\Actions;
use App\Lib\Events\Succeeded;
use App\Setting\Contracts\Storeable;
use GrahamCampbell\ResultType\Success;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class StoreAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*/
public function handle(Storeable $settings, array $input): void
{
$settings->fill($input)->save();
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Storeable */
$group = request()->route('settingGroup');
return $group->rules();
}
public function asController(ActionRequest $request, Storeable $settingGroup): RedirectResponse
{
$settingGroup->beforeSave($request);
$this->handle($settingGroup, $settingGroup->saveAttributes($request));
Succeeded::message('Einstellungen gespeichert')->dispatch();
return redirect()->back();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Setting\Actions;
use App\Setting\LocalSettings;
use App\Setting\SettingFactory;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class ViewAction
{
use AsAction;
public function handle(LocalSettings $settingGroup): Response
{
session()->put('menu', 'setting');
session()->put('title', $settingGroup::title());
return Inertia::render('setting/' . ucfirst($settingGroup::group()), [
'data' => $settingGroup->data(),
'settingMenu' => app(SettingFactory::class)->getShare(),
]);
}
}

View File

@ -1,11 +0,0 @@
<?php
namespace App\Setting\Contracts;
interface Indexable
{
/**
* @return class-string
*/
public static function indexAction(): string;
}

View File

@ -2,10 +2,28 @@
namespace App\Setting\Contracts; namespace App\Setting\Contracts;
use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest;
use Spatie\LaravelSettings\Settings;
/**
* @mixin LocalSettings
*/
interface Storeable interface Storeable
{ {
public function url(): string;
/** /**
* @return class-string * @param array<string, mixed> $input
*/ */
public static function storeAction(): string; public function fill(array $input): Settings;
/**
* @return array<string, mixed>
*/
public function rules(): array;
public function beforeSave(ActionRequest $request): void;
public function saveAttributes(ActionRequest $request): array;
} }

View File

@ -2,16 +2,48 @@
namespace App\Setting; namespace App\Setting;
use Lorisleiva\Actions\ActionRequest;
use Spatie\LaravelSettings\Settings; use Spatie\LaravelSettings\Settings;
abstract class LocalSettings extends Settings abstract class LocalSettings extends Settings
{ {
abstract public static function slug(): string;
abstract public static function title(): string; abstract public static function title(): string;
public static function url(): string public function url(): string
{ {
return '/setting/'.static::slug(); return route('setting.view', ['settingGroup' => $this->group()]);
}
public function storeUrl(): string
{
return $this->url();
}
/**
* @return mixed
*/
public function data()
{
return [
'data' => $this->toArray(),
'meta' => [
'links' => [
'store' => $this->storeUrl(),
]
]
];
}
public function beforeSave(ActionRequest $request): void
{
return;
}
/**
* @return array<string, mixed>
*/
public function saveAttributes(ActionRequest $request): array
{
return $request->validated();
} }
} }

View File

@ -3,14 +3,13 @@
namespace App\Setting; namespace App\Setting;
use App\Group; use App\Group;
use App\Nami\Actions\SettingIndexAction; use App\Initialize\Actions\NamiLoginCheckAction;
use App\Nami\Actions\SettingSaveAction;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use Lorisleiva\Actions\ActionRequest;
use Zoomyboy\LaravelNami\Api; use Zoomyboy\LaravelNami\Api;
use Zoomyboy\LaravelNami\Nami; use Zoomyboy\LaravelNami\Nami;
class NamiSettings extends LocalSettings implements Indexable, Storeable class NamiSettings extends LocalSettings implements Storeable
{ {
public int $mglnr; public int $mglnr;
@ -31,28 +30,48 @@ class NamiSettings extends LocalSettings implements Indexable, Storeable
return Nami::login($this->mglnr, $this->password); return Nami::login($this->mglnr, $this->password);
} }
/**
* @inheritdoc
*/
public function rules(): array
{
return [
'mglnr' => 'required',
'password' => 'required',
'default_group_id' => 'required',
];
}
public function beforeSave(ActionRequest $request): void
{
NamiLoginCheckAction::run([
'mglnr' => $request->mglnr,
'password' => $request->password,
]);
}
public function localGroup(): ?Group public function localGroup(): ?Group
{ {
return Group::firstWhere('nami_id', $this->default_group_id); return Group::firstWhere('nami_id', $this->default_group_id);
} }
public static function slug(): string
{
return 'nami';
}
public static function indexAction(): string
{
return SettingIndexAction::class;
}
public static function storeAction(): string
{
return SettingSaveAction::class;
}
public static function title(): string public static function title(): string
{ {
return 'NaMi-Login'; return 'NaMi-Login';
} }
/**
* @inheritdoc
*/
public function data(): array
{
return [
...parent::data(),
'data' => [
'mglnr' => $this->mglnr,
'password' => '',
'default_group_id' => $this->default_group_id,
],
];
}
} }

View File

@ -2,8 +2,6 @@
namespace App\Setting; namespace App\Setting;
use App\Setting\Contracts\Indexable;
use App\Setting\Contracts\Storeable;
use Illuminate\Routing\Router; use Illuminate\Routing\Router;
class SettingFactory class SettingFactory
@ -14,22 +12,14 @@ class SettingFactory
private array $settings = []; private array $settings = [];
/** /**
* @param class-string $setting * @param class-string<LocalSettings> $setting
*/ */
public function register(string $setting): void public function register(string $setting): void
{ {
$this->settings[] = $setting; $this->settings[] = $setting;
if (new $setting() instanceof Indexable) {
app(Router::class)->middleware(['web', 'auth:web', SettingMiddleware::class])->get($setting::url(), $setting::indexAction());
}
if (new $setting() instanceof Storeable) {
app(Router::class)->middleware(['web', 'auth:web', SettingMiddleware::class])->post($setting::url(), $setting::storeAction());
}
if (1 === count($this->settings)) { if (1 === count($this->settings)) {
app(Router::class)->redirect('/setting', '/setting/'.$setting::slug()); app(Router::class)->redirect('/setting', '/setting/' . $setting::group());
} }
} }
@ -39,10 +29,17 @@ class SettingFactory
public function getShare(): array public function getShare(): array
{ {
return collect($this->settings)->map(fn ($setting) => [ return collect($this->settings)->map(fn ($setting) => [
'url' => $setting::url(), 'url' => (new $setting)->url(),
'is_active' => '/'.request()->path() === $setting::url(), 'is_active' => url(request()->path()) === (new $setting)->url(),
'title' => $setting::title(), 'title' => $setting::title(),
]) ])
->toArray(); ->toArray();
} }
public function resolveGroupName(string $name): LocalSettings
{
$settingClass = collect($this->settings)->first(fn ($setting) => $setting::group() === $name);
return app($settingClass);
}
} }

View File

@ -1,26 +0,0 @@
<?php
namespace App\Setting;
use Closure;
use Illuminate\Http\Request;
use Inertia;
class SettingMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
*
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
Inertia::share([
'setting_menu' => app(SettingFactory::class)->getShare(),
]);
return $next($request);
}
}

View File

@ -8,6 +8,10 @@ use App\Invoice\InvoiceSettings;
use App\Mailgateway\MailgatewaySettings; use App\Mailgateway\MailgatewaySettings;
use App\Module\ModuleSettings; use App\Module\ModuleSettings;
use App\Prevention\PreventionSettings; use App\Prevention\PreventionSettings;
use App\Setting\Actions\StoreAction;
use App\Setting\Actions\ViewAction;
use App\User\UserSettings;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class SettingServiceProvider extends ServiceProvider class SettingServiceProvider extends ServiceProvider
@ -20,6 +24,10 @@ class SettingServiceProvider extends ServiceProvider
public function register() public function register()
{ {
app()->singleton(SettingFactory::class, fn () => new SettingFactory()); app()->singleton(SettingFactory::class, fn () => new SettingFactory());
app(Router::class)->bind('settingGroup', fn ($param) => app(SettingFactory::class)->resolveGroupName($param));
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.view')->get('/setting/{settingGroup}', ViewAction::class);
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.data')->get('/setting/{settingGroup}/data', ViewAction::class);
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.store')->post('/setting/{settingGroup}', StoreAction::class);
} }
/** /**
@ -36,5 +44,6 @@ class SettingServiceProvider extends ServiceProvider
app(SettingFactory::class)->register(FormSettings::class); app(SettingFactory::class)->register(FormSettings::class);
app(SettingFactory::class)->register(FileshareSettings::class); app(SettingFactory::class)->register(FileshareSettings::class);
app(SettingFactory::class)->register(PreventionSettings::class); app(SettingFactory::class)->register(PreventionSettings::class);
app(SettingFactory::class)->register(UserSettings::class);
} }
} }

28
app/User/UserSettings.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\User;
use App\Http\Resources\UserResource;
use App\Setting\LocalSettings;
use App\User;
class UserSettings extends LocalSettings
{
public static function group(): string
{
return 'user';
}
public static function title(): string
{
return 'Benutzer';
}
/**
* @inheritdoc
*/
public function data()
{
return UserResource::collection(User::orderByRaw('lastname, firstname')->get());
}
}

5
bin/copydbdk Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
ssh dkd 'cd $ADREMA_PATH && docker compose exec db mysqldump -q -udb -p'$ADREMA_DB_PASSWORD' db --ignore-table=db.telescope_entries --ignore-table=db.failed_jobs' > db.tmp
sudo mysql scoutrobot < db.tmp

7
bin/testmigration Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/zsh
php artisan migrate:fresh --env=testing
ssh dkd 'cd $ADREMA_PATH && docker compose exec db mysqldump -q -udb -p'$ADREMA_DB_PASSWORD' db --ignore-table=db.telescope_entries --ignore-table=db.failed_jobs' > dddd.sql
sudo mysql scoutrobottest < dddd.sql
php artisan migrate --env=testing

View File

@ -92,6 +92,8 @@ services:
socketi: socketi:
image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian
ports:
- '6001:6001'
environment: environment:
SOKETI_DEFAULT_APP_ID: adremaid SOKETI_DEFAULT_APP_ID: adremaid
SOKETI_DEFAULT_APP_KEY: adremakey SOKETI_DEFAULT_APP_KEY: adremakey
@ -104,6 +106,8 @@ services:
meilisearch: meilisearch:
image: getmeili/meilisearch:v1.6 image: getmeili/meilisearch:v1.6
ports:
- '7700:7700'
volumes: volumes:
- ./data/meilisearch:/meili_data - ./data/meilisearch:/meili_data
env_file: env_file:

View File

@ -2,9 +2,11 @@ import {ref, inject, computed, onBeforeUnmount} from 'vue';
import {router} from '@inertiajs/vue3'; import {router} from '@inertiajs/vue3';
import useQueueEvents from './useQueueEvents.js'; import useQueueEvents from './useQueueEvents.js';
export function useIndex(props, siteName) { export function useIndex(props, siteName = null) {
const axios = inject('axios'); const axios = inject('axios');
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false)); if (siteName !== null) {
var {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
}
const rawProps = JSON.parse(JSON.stringify(props)); const rawProps = JSON.parse(JSON.stringify(props));
const inner = { const inner = {
data: ref(rawProps.data), data: ref(rawProps.data),
@ -56,8 +58,10 @@ export function useIndex(props, siteName) {
reload(true); reload(true);
} }
if (siteName !== null) {
startListener(); startListener();
onBeforeUnmount(() => stopListener()); onBeforeUnmount(() => stopListener());
}
return { return {
data: inner.data, data: inner.data,

View File

@ -2,9 +2,11 @@ import {computed, ref, inject, onBeforeUnmount} from 'vue';
import {router} from '@inertiajs/vue3'; import {router} from '@inertiajs/vue3';
import useQueueEvents from './useQueueEvents.js'; import useQueueEvents from './useQueueEvents.js';
export function useIndex(props, siteName) { export function useIndex(props, siteName = null) {
const axios = inject('axios'); const axios = inject('axios');
const {startListener, stopListener} = useQueueEvents(siteName, () => reload(false)); if (siteName !== null) {
var {startListener, stopListener} = useQueueEvents(siteName, () => reload(false));
}
const single = ref(null); const single = ref(null);
const rawProps = JSON.parse(JSON.stringify(props)); const rawProps = JSON.parse(JSON.stringify(props));
const inner = { const inner = {
@ -86,8 +88,10 @@ export function useIndex(props, siteName) {
reload(true); reload(true);
} }
if (siteName !== null) {
startListener(); startListener();
onBeforeUnmount(() => stopListener()); onBeforeUnmount(() => stopListener());
}
return { return {
data: inner.data, data: inner.data,

View File

@ -5,45 +5,26 @@
</template> </template>
<setting-layout> <setting-layout>
<form id="billsettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit"> <form id="billsettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
<f-text id="from" v-model="inner.from" label="Absender" hint="Absender-Name in Kurzform, i.d.R. der kurze Stammesname"></f-text> <f-text id="from" v-model="data.from" label="Absender" hint="Absender-Name in Kurzform, i.d.R. der kurze Stammesname"></f-text>
<f-text id="from_long" v-model="inner.from_long" label="Absender (lang)" hint="Absender-Name in Langform, i.d.R. der Stammesname"></f-text> <f-text id="from_long" v-model="data.from_long" label="Absender (lang)" hint="Absender-Name in Langform, i.d.R. der Stammesname"></f-text>
<h2 class="text-lg font-semibold text-gray-300 col-span-2 mt-5">Kontaktdaten</h2> <h2 class="text-lg font-semibold text-gray-300 col-span-2 mt-5">Kontaktdaten</h2>
<div class="col-span-2 text-gray-300 text-sm">Diese Kontaktdaten stehen im Absender-Bereich auf der Rechnung.</div> <div class="col-span-2 text-gray-300 text-sm">Diese Kontaktdaten stehen im Absender-Bereich auf der Rechnung.</div>
<f-text id="address" v-model="inner.address" label="Straße"></f-text> <f-text id="address" v-model="data.address" label="Straße"></f-text>
<f-text id="zip" v-model="inner.zip" label="PLZ"></f-text> <f-text id="zip" v-model="data.zip" label="PLZ"></f-text>
<f-text id="place" v-model="inner.place" label="Ort"></f-text> <f-text id="place" v-model="data.place" label="Ort"></f-text>
<f-text id="email" v-model="inner.email" label="E-Mail-Adresse"></f-text> <f-text id="email" v-model="data.email" label="E-Mail-Adresse"></f-text>
<f-text id="mobile" v-model="inner.mobile" label="Telefonnummer"></f-text> <f-text id="mobile" v-model="data.mobile" label="Telefonnummer"></f-text>
<f-text id="website" v-model="inner.website" label="Webseite"></f-text> <f-text id="website" v-model="data.website" label="Webseite"></f-text>
<f-text id="iban" v-model="inner.iban" label="IBAN"></f-text> <f-text id="iban" v-model="data.iban" label="IBAN"></f-text>
<f-text id="bic" v-model="inner.bic" label="BIC"></f-text> <f-text id="bic" v-model="data.bic" label="BIC"></f-text>
<f-text id="remember_weeks" v-model="inner.remember_weeks" type="number" label="Erinnerung alle X Wochen versenden"></f-text> <f-text id="remember_weeks" v-model="data.rememberWeeks" type="number" label="Erinnerung alle X Wochen versenden"></f-text>
</form> </form>
</setting-layout> </setting-layout>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import SettingLayout from './Layout.vue'; import {props, useSettings} from './useSettings.js';
const innerProps = defineProps(props);
export default { const {submit, data, meta, SettingLayout} = useSettings(innerProps);
components: {
SettingLayout,
},
props: {
data: {},
},
data: function () {
return {
inner: {...this.data},
};
},
methods: {
submit() {
this.$inertia.post('/setting/bill', this.inner, {
onSuccess: () => this.$success('Einstellungen gespeichert.'),
});
},
},
};
</script> </script>

View File

@ -76,10 +76,12 @@
</template> </template>
<script lang="js" setup> <script lang="js" setup>
import { useApiIndex } from '../../composables/useApiIndex.js'; import { useIndex, indexProps } from '../../composables/useInertiaApiIndex.js';
import SettingLayout from '../setting/Layout.vue'; import SettingLayout from '../setting/Layout.vue';
const { meta, data, reload, create, edit, cancel, single, submit } = useApiIndex('/api/fileshare', 'fileshare'); const props = defineProps(indexProps);
const { meta, data, reload, create, edit, cancel, single, submit } = useIndex(props.data, 'fileshare');
function getType(type) { function getType(type) {
if (!type) { if (!type) {
@ -89,6 +91,4 @@ function getType(type) {
} }
return meta.value.types.find((t) => t.id === type); return meta.value.types.find((t) => t.id === type);
} }
reload();
</script> </script>

View File

@ -9,40 +9,16 @@
<p class="text-sm">Hier kannst du Einstellungen für Anmeldeformulare setzen.</p> <p class="text-sm">Hier kannst du Einstellungen für Anmeldeformulare setzen.</p>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<f-text id="register_url" v-model="inner.register_url" label="Formular-Link"></f-text> <f-text id="register_url" v-model="data.registerUrl" label="Formular-Link"></f-text>
<f-text id="clear_cache_url" v-model="inner.clear_cache_url" label="Frontend-Cache-Url"></f-text> <f-text id="clear_cache_url" v-model="data.clearCacheUrl" label="Frontend-Cache-Url"></f-text>
</div> </div>
</form> </form>
</setting-layout> </setting-layout>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import SettingLayout from './Layout.vue'; import {props, useSettings} from './useSettings.js';
const innerProps = defineProps(props);
export default { const {submit, data, meta, SettingLayout} = useSettings(innerProps);
components: {
SettingLayout,
},
props: {
data: {
type: Object,
default: () => {
return {};
},
},
},
data: function () {
return {
inner: {...this.data.data},
};
},
methods: {
submit() {
this.$inertia.post('/setting/form', this.inner, {
onSuccess: () => this.$success('Einstellungen gespeichert.'),
});
},
},
};
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="flex grow relative"> <div class="flex grow relative">
<ui-menulist v-model="active" :entries="$page.props.setting_menu"></ui-menulist> <ui-menulist v-model="active" :entries="$page.props.settingMenu"></ui-menulist>
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
@ -9,7 +9,7 @@
export default { export default {
data: function () { data: function () {
return { return {
innerActive: this.$page.props.setting_menu.findIndex((menu) => menu.is_active), innerActive: this.$page.props.settingMenu.findIndex((menu) => menu.is_active),
}; };
}, },
computed: { computed: {
@ -19,7 +19,7 @@ export default {
}, },
set(v) { set(v) {
var _self = this; var _self = this;
this.$inertia.visit(this.$page.props.setting_menu[v].url, { this.$inertia.visit(this.$page.props.settingMenu[v].url, {
onSuccess() { onSuccess() {
_self.innerActive = v; _self.innerActive = v;
}, },

View File

@ -1,87 +0,0 @@
<template>
<page-layout>
<template #right>
<f-save-button form="mailmansettingform"></f-save-button>
</template>
<setting-layout>
<form id="mailmansettingform" 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">
Scoutrobot kann automatisch Mailinglisten erstellen, wenn es mit einem existierenden
<a href="https://docs.mailman3.org/en/latest/">Mailman Server</a> verbunden wird. Mailman ist ein OpenSource-Mailinglisten-System, um E-Mails an mehrere Leute zu senden.
</p>
<p class="text-sm mt-1">Scoutrobot wird nach der Ersteinrichtung deine Mitglieder zu bestehenden E-Mail-Verteilern hinzufügen.</p>
</div>
<div>
<f-switch id="is_active" v-model="inner.is_active" label="Mailman-Synchronisation aktiv"></f-switch>
</div>
<div class="flex h-full items-center">
<ui-sprite :src="stateDisplay.icon" :class="stateDisplay.text" class="w-5 h-5"></ui-sprite>
<span class="ml-3" :class="stateDisplay.text" v-text="stateDisplay.label"></span>
</div>
<f-text id="base_url" v-model="inner.base_url" label="URL" hint="URL der Mailman Api"></f-text>
<f-text id="username" v-model="inner.username" label="Benutzername"></f-text>
<f-text id="password" v-model="inner.password" type="password" label="Passwort"></f-text>
<f-select id="all_list" v-model="inner.all_list" label="Liste für alle Mitglieder" name="all_list" :options="lists"></f-select>
<f-select id="all_parents_list" v-model="inner.all_parents_list" label="Liste für Eltern" name="all_parents_list" :options="lists"></f-select>
<f-select id="active_leaders_list" v-model="inner.active_leaders_list" label="Liste für aktive Leiter" name="active_leaders_list" :options="lists"></f-select>
<f-select id="passive_leaders_list" v-model="inner.passive_leaders_list" label="Liste für passive Leiter" name="passive_leaders_list" :options="lists"></f-select>
<div></div>
</form>
</setting-layout>
</page-layout>
</template>
<script>
import SettingLayout from './Layout.vue';
export default {
components: {
SettingLayout,
},
props: {
data: {},
state: {},
lists: {},
},
data: function () {
return {
inner: {...this.data},
};
},
computed: {
stateDisplay() {
if (this.state === null) {
return {
text: 'text-gray-500',
icon: 'disabled',
label: 'Deaktiviert',
};
}
return this.state
? {
text: 'text-green-500',
icon: 'check',
label: 'Verbindung erfolgreich.',
}
: {
text: 'text-red-500',
icon: 'close',
label: 'Verbindung fehlgeschlagen.',
};
},
},
methods: {
submit() {
this.$inertia.post('/setting/mailman', this.inner, {
onSuccess: (page) => {
this.$success('Einstellungen gespeichert.');
this.inner = page.props.data;
},
});
},
},
};
</script>

View File

@ -4,48 +4,20 @@
<f-save-button form="modulesettingform"></f-save-button> <f-save-button form="modulesettingform"></f-save-button>
</template> </template>
<setting-layout> <setting-layout>
<form id="modulesettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" <form id="modulesettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
@submit.prevent="submit">
<div class="col-span-full text-gray-100 mb-3"> <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 <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>
und so den Funktionsumfang auf deine Bedürfnisse anpassen.</p>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <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" <f-switch v-for="module in meta.modules" :id="module.id" v-model="data.modules" :value="module.id" size="sm" name="modules" :label="module.name"></f-switch>
size="sm" name="modules" :label="module.name"></f-switch>
</div> </div>
</form> </form>
</setting-layout> </setting-layout>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import SettingLayout from './Layout.vue'; import {props, useSettings} from './useSettings.js';
const innerProps = defineProps(props);
export default { const {submit, data, meta, SettingLayout} = useSettings(innerProps);
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> </script>

View File

@ -8,40 +8,16 @@
<div class="col-span-full text-gray-100 mb-3"> <div class="col-span-full text-gray-100 mb-3">
<p class="text-sm">Hier kannst du deine Zugangsdaten zu NaMi anpassen, falls sich z.B. dein Passwort geändert hat.</p> <p class="text-sm">Hier kannst du deine Zugangsdaten zu NaMi anpassen, falls sich z.B. dein Passwort geändert hat.</p>
</div> </div>
<f-text id="mglnr" v-model="inner.mglnr" label="Mitgliedsnummer"></f-text> <f-text id="mglnr" v-model="data.mglnr" label="Mitgliedsnummer"></f-text>
<f-text id="default_group_id" v-model="inner.default_group_id" label="Standard-Gruppierung"></f-text> <f-text id="default_group_id" v-model="data.default_group_id" label="Standard-Gruppierung"></f-text>
<f-text id="password" v-model="inner.password" label="Passwort" name="password" type="password"></f-text> <f-text id="password" v-model="data.password" label="Passwort" name="password" type="password"></f-text>
</form> </form>
</setting-layout> </setting-layout>
</page-layout> </page-layout>
</template> </template>
<script> <script setup>
import SettingLayout from './Layout.vue'; import {props, useSettings} from './useSettings.js';
const innerProps = defineProps(props);
export default { const {submit, data, meta, SettingLayout} = useSettings(innerProps);
components: {
SettingLayout,
},
props: {
data: {
type: Object,
default: () => {
return {};
},
},
},
data: function () {
return {
inner: {...this.data},
};
},
methods: {
submit() {
this.$inertia.post('/setting/nami', this.inner, {
onSuccess: () => this.$success('Einstellungen gespeichert.'),
});
},
},
};
</script> </script>

View File

@ -3,7 +3,7 @@
<template #right> <template #right>
<f-save-button form="preventionform"></f-save-button> <f-save-button form="preventionform"></f-save-button>
</template> </template>
<setting-layout v-if="loaded"> <setting-layout>
<form id="preventionform" class="grow p-6" @submit.prevent="submit"> <form id="preventionform" class="grow p-6" @submit.prevent="submit">
<div class="col-span-full text-gray-100 mb-3"> <div class="col-span-full text-gray-100 mb-3">
<p class="text-sm">Hier kannst du Einstellungen zu Prävention setzen.</p> <p class="text-sm">Hier kannst du Einstellungen zu Prävention setzen.</p>
@ -16,22 +16,8 @@
</page-layout> </page-layout>
</template> </template>
<script lang="js" setup> <script setup>
import { ref } from 'vue'; import {props, useSettings} from './useSettings.js';
import { useApiIndex } from '../../composables/useApiIndex.js'; const innerProps = defineProps(props);
import SettingLayout from '../setting/Layout.vue'; const {submit, data, meta, SettingLayout} = useSettings(innerProps);
const { axios, data, reload } = useApiIndex('/api/prevention', 'prevention');
const loaded = ref(false);
async function load() {
await reload();
loaded.value = true;
}
async function submit() {
await axios.post('/api/prevention', { ...data.value });
}
load();
</script> </script>

View File

@ -0,0 +1,53 @@
<template>
<page-layout>
<template #toolbar>
<page-toolbar-button color="primary" icon="plus" @click.prevent="create">Neuer Benutzer</page-toolbar-button>
</template>
<ui-popup v-if="single !== null" :heading="single.id ? 'Benutzer bearbeiten' : 'Neuer Benutzer'" @close="cancel">
<form @submit.prevent="submit">
<section class="grid grid-cols-2 gap-3 mt-6">
<f-text id="firstname" v-model="single.firstname" name="firstname" label="Vorname" required></f-text>
<f-text id="lastname" v-model="single.lastname" name="lastname" label="Nachname" required></f-text>
</section>
<section class="flex mt-4 space-x-2">
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
<ui-button class="btn-primary" @click.prevent="single = null">Abbrechen</ui-button>
</section>
</form>
</ui-popup>
<setting-layout>
<div class="w-full h-full pb-6">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th>Nachname</th>
<th>Vorname</th>
<th>Aktion</th>
</thead>
<tr v-for="(user, index) in data" :key="index">
<td v-text="user.lastname"></td>
<td v-text="user.firstname"></td>
<td>
<a v-tooltip="`Bearbeiten`" href="#" class="inline-flex btn btn-warning btn-sm" @click.prevent="edit(user)"><ui-sprite src="pencil"></ui-sprite></a>
<a v-tooltip="`Löschen`" href="#" class="inline-flex btn btn-danger btn-sm" @click.prevent="remove(user)"><ui-sprite src="pencil"></ui-sprite></a>
</td>
</tr>
</table>
<div class="px-6">
<ui-pagination class="mt-4" :value="meta" :only="['data']"></ui-pagination>
</div>
</div>
</setting-layout>
</page-layout>
</template>
<script lang="js" setup>
import SettingLayout from '../setting/Layout.vue';
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
const props = defineProps(indexProps);
const {data, cancel, meta, single, create, edit} = useIndex(props.data);
</script>

View File

@ -0,0 +1,29 @@
import {useIndex} from '../../composables/useInertiaApiIndex.js';
import SettingLayout from './Layout.vue';
export function useSettings(props) {
const {data, meta, router} = useIndex(props.data);
function submit() {
router.post(meta.value.links.store, {...data.value});
}
return {
submit,
data,
meta,
props,
SettingLayout,
};
}
const props = {
data: {
default: () => {
return {data: [], meta: {}};
},
type: Object,
},
};
export {props};

View File

@ -3,13 +3,9 @@
use App\Contribution\Actions\GenerateApiAction as ContributionGenerateApiAction; use App\Contribution\Actions\GenerateApiAction as ContributionGenerateApiAction;
use App\Form\Actions\FormApiListAction; use App\Form\Actions\FormApiListAction;
use App\Form\Actions\RegisterAction; use App\Form\Actions\RegisterAction;
use App\Prevention\Actions\SettingStoreAction as PreventionStoreAction;
use App\Group\Actions\GroupApiIndexAction; use App\Group\Actions\GroupApiIndexAction;
use App\Prevention\Actions\SettingApiAction;
Route::post('/contribution-generate', ContributionGenerateApiAction::class)->name('api.contribution.generate')->middleware('client:contribution-generate'); Route::post('/contribution-generate', ContributionGenerateApiAction::class)->name('api.contribution.generate')->middleware('client:contribution-generate');
Route::post('/form/{form}/register', RegisterAction::class)->name('form.register'); Route::post('/form/{form}/register', RegisterAction::class)->name('form.register');
Route::get('/group/{group?}', GroupApiIndexAction::class)->name('api.group'); Route::get('/group/{group?}', GroupApiIndexAction::class)->name('api.group');
Route::get('/form', FormApiListAction::class)->name('api.form.index'); Route::get('/form', FormApiListAction::class)->name('api.form.index');
Route::get('/prevention', SettingApiAction::class)->name('api.prevention.index');
Route::post('/prevention', PreventionStoreAction::class)->name('api.prevention.store');

View File

@ -19,7 +19,6 @@ use App\Invoice\Actions\InvoiceStoreAction;
use App\Course\Actions\CourseUpdateAction; use App\Course\Actions\CourseUpdateAction;
use App\Dashboard\Actions\IndexAction as DashboardIndexAction; use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
use App\Efz\ShowEfzDocumentAction; use App\Efz\ShowEfzDocumentAction;
use App\Fileshare\Actions\FileshareApiIndexAction;
use App\Fileshare\Actions\FileshareStoreAction; use App\Fileshare\Actions\FileshareStoreAction;
use App\Fileshare\Actions\FileshareUpdateAction; use App\Fileshare\Actions\FileshareUpdateAction;
use App\Fileshare\Actions\ListFilesAction; use App\Fileshare\Actions\ListFilesAction;
@ -182,6 +181,5 @@ Route::group(['middleware' => 'auth:web'], function (): void {
// ------------------------------------ fileshare ----------------------------------- // ------------------------------------ fileshare -----------------------------------
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store'); Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');
Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update'); Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update');
Route::get('/api/fileshare', FileshareApiIndexAction::class)->name('api.fileshare.index');
Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files'); Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files');
}); });

View File

@ -11,10 +11,8 @@ use App\Membership\Actions\MassStoreAction;
use App\Membership\Actions\MembershipDestroyAction; use App\Membership\Actions\MembershipDestroyAction;
use App\Membership\Actions\MembershipStoreAction; use App\Membership\Actions\MembershipStoreAction;
use App\Subactivity; use App\Subactivity;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Tests\EndToEndTestCase; use Tests\EndToEndTestCase;
use Tests\TestCase;
use Throwable; use Throwable;
use Zoomyboy\LaravelNami\Fakes\MembershipFake; use Zoomyboy\LaravelNami\Fakes\MembershipFake;

View File

@ -10,10 +10,19 @@ class SettingTest extends TestCase
{ {
use DatabaseTransactions; use DatabaseTransactions;
public function testSettingIndex(): void public function testItDisplaysView(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
InvoiceSettings::fake([
$this->get(route('setting.view', ['settingGroup' => 'bill']))
->assertOk()
->assertComponent('setting/Bill');
}
public function testDisplaySettings(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
app(InvoiceSettings::class)->fill([
'from_long' => 'DPSG Stamm Muster', 'from_long' => 'DPSG Stamm Muster',
'from' => 'Stamm Muster', 'from' => 'Stamm Muster',
'mobile' => '+49 176 55555', 'mobile' => '+49 176 55555',
@ -25,43 +34,33 @@ class SettingTest extends TestCase
'iban' => 'DE05', 'iban' => 'DE05',
'bic' => 'SOLSDE', 'bic' => 'SOLSDE',
'rememberWeeks' => 6 'rememberWeeks' => 6
]); ])->save();
$response = $this->get('/setting/bill'); $this->get(route('setting.data', ['settingGroup' => 'bill']))
->assertOk()
$response->assertOk(); ->assertComponent('setting/Bill')
$this->assertInertiaHas([ ->assertInertiaPath('data.from_long', 'DPSG Stamm Muster')
'from_long' => 'DPSG Stamm Muster', ->assertInertiaPath('data.from', 'Stamm Muster')
'from' => 'Stamm Muster', ->assertInertiaPath('data.mobile', '+49 176 55555')
'mobile' => '+49 176 55555', ->assertInertiaPath('data.email', 'max@muster.de')
'email' => 'max@muster.de', ->assertInertiaPath('data.website', 'https://example.com')
'website' => 'https://example.com', ->assertInertiaPath('data.address', 'Musterstr 4')
'address' => 'Musterstr 4', ->assertInertiaPath('data.place', 'Solingen')
'place' => 'Solingen', ->assertInertiaPath('data.zip', '12345')
'zip' => '12345', ->assertInertiaPath('data.iban', 'DE05')
'iban' => 'DE05', ->assertInertiaPath('data.bic', 'SOLSDE')
'bic' => 'SOLSDE', ->assertInertiaPath('data.rememberWeeks', 6);
'remember_weeks' => 6
], $response, 'data');
} }
public function testItReturnsTabs(): void public function testItReturnsTabs(): void
{ {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$response = $this->get('/setting/bill'); $this->get(route('setting.view', ['settingGroup' => 'bill']))
->assertInertiaPath('setting_menu.1.title', 'Rechnung')
/** @var array<int, array{url: string, title: string, is_active: bool}> */ ->assertInertiaPath('setting_menu.1.url', url('/setting/bill'))
$menus = $this->inertia($response, 'setting_menu'); ->assertInertiaPath('setting_menu.1.is_active', true)
$this->assertTrue( ->assertInertiaPath('setting_menu.0.is_active', false);
collect($menus)
->pluck('url')
->contains('/setting/bill')
);
$settingMenu = collect($menus)->first(fn ($menu) => '/setting/bill' === $menu['url']);
$this->assertTrue($settingMenu['is_active']);
$this->assertEquals('Rechnung', $settingMenu['title']);
} }
public function testItCanChangeSettings(): void public function testItCanChangeSettings(): void
@ -79,7 +78,7 @@ class SettingTest extends TestCase
'zip' => '12345', 'zip' => '12345',
'iban' => 'DE05', 'iban' => 'DE05',
'bic' => 'SOLSDE', 'bic' => 'SOLSDE',
'remember_weeks' => 10 'rememberWeeks' => 10
]); ]);
$response->assertRedirect('/setting/bill'); $response->assertRedirect('/setting/bill');

View File

@ -0,0 +1,30 @@
<?php
namespace Tests\Feature\Permission;
use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
class UserIndexTest extends TestCase
{
use DatabaseTransactions;
public function testItOpensSettingsPage(): void
{
$this->login()->loginNami();
auth()->user()->update(['firstname' => 'Jane', 'lastname' => 'Doe']);
User::factory()->create(['firstname' => 'John', 'lastname' => 'Doe']);
$anna = User::factory()->create(['firstname' => 'Anna', 'lastname' => 'Doe']);
$this->get(route('setting.view', ['settingGroup' => 'user']))
->assertOk()
->assertComponent('setting/User')
->assertInertiaPath('data.data.0.firstname', 'Anna')
->assertInertiaPath('data.data.0.lastname', 'Doe')
->assertInertiaPath('data.data.0.id', $anna->id)
->assertInertiaPath('data.data.1.firstname', 'Jane')
->assertInertiaPath('data.data.2.firstname', 'John')
->assertInertiaPath('data.meta.default.firstname', '');
}
}

View File

@ -16,25 +16,25 @@ class SettingTest extends TestCase
{ {
$this->login()->loginNami(); $this->login()->loginNami();
$this->get('/setting/prevention')->assertComponent('setting/Prevention')->assertOk();
}
public function testItReceivesSettings(): void
{
$this->login()->loginNami();
$text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData(); $text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData();
app(PreventionSettings::class)->fill(['formmail' => $text])->save(); app(PreventionSettings::class)->fill(['formmail' => $text])->save();
$this->get('/api/prevention') $this->get(route('setting.view', ['settingGroup' => 'prevention']))
->assertJsonPath('data.formmail.blocks.0.data.text', 'lorem ipsum'); ->assertOk()
->assertComponent('setting/Prevention')
->assertInertiaPath('data.formmail.blocks.0.data.text', 'lorem ipsum')
->assertInertiaPath('store_url', route('setting.store', ['settingGroup' => 'prevention']));
} }
public function testItStoresSettings(): void public function testItStoresSettings(): void
{ {
$this->login()->loginNami(); $this->login()->loginNami();
$this->post('/api/prevention', ['formmail' => EditorRequestFactory::new()->text(50, 'new lorem')->create()])->assertOk(); $route = route('setting.store', ['settingGroup' => 'prevention']);
$this
->from($route)
->post($route, ['formmail' => EditorRequestFactory::new()->text(50, 'new lorem')->create()])
->assertRedirect($route);
$this->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem'])); $this->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
} }
} }

View File

@ -17,31 +17,25 @@ class FileshareIndexActionTest extends FileshareTestCase
->name('lokaler Server') ->name('lokaler Server')
->create(); ->create();
$this->get('/api/fileshare') $this->get(route('setting.view', ['settingGroup' => 'fileshare']))
->assertJsonPath('data.0.name', 'lokaler Server') ->assertComponent('setting/Fileshare')
->assertJsonPath('data.0.type', OwncloudConnection::class) ->assertInertiaPath('data.data.0.name', 'lokaler Server')
->assertJsonPath('data.0.config.user', 'badenpowell') ->assertInertiaPath('data.data.0.type', OwncloudConnection::class)
->assertJsonPath('data.0.config.password', 'secret') ->assertInertiaPath('data.data.0.config.user', 'badenpowell')
->assertJsonPath('data.0.config.base_url', env('TEST_OWNCLOUD_DOMAIN')) ->assertInertiaPath('data.data.0.config.password', 'secret')
->assertJsonPath('data.0.id', $connection->id) ->assertInertiaPath('data.data.0.config.base_url', env('TEST_OWNCLOUD_DOMAIN'))
->assertJsonPath('data.0.is_active', true) ->assertInertiaPath('data.data.0.id', $connection->id)
->assertJsonPath('data.0.type_human', 'Owncloud') ->assertInertiaPath('data.data.0.is_active', true)
->assertJsonPath('data.0.links.update', route('fileshare.update', ['fileshare' => $connection])) ->assertInertiaPath('data.data.0.type_human', 'Owncloud')
->assertJsonPath('meta.default.name', '') ->assertInertiaPath('data.data.0.links.update', route('fileshare.update', ['fileshare' => $connection]))
->assertJsonPath('meta.links.store', route('fileshare.store')) ->assertInertiaPath('data.meta.default.name', '')
->assertJsonPath('meta.types.0.id', NextcloudConnection::class) ->assertInertiaPath('data.meta.links.store', route('fileshare.store'))
->assertJsonPath('meta.types.0.name', 'Nextcloud') ->assertInertiaPath('data.meta.types.0.id', NextcloudConnection::class)
->assertJsonPath('meta.types.0.defaults.base_url', '') ->assertInertiaPath('data.meta.types.0.name', 'Nextcloud')
->assertJsonPath('meta.types.1.id', OwncloudConnection::class) ->assertInertiaPath('data.meta.types.0.defaults.base_url', '')
->assertJsonPath('meta.types.1.name', 'Owncloud') ->assertInertiaPath('data.meta.types.1.id', OwncloudConnection::class)
->assertJsonPath('meta.types.1.defaults.base_url', '') ->assertInertiaPath('data.meta.types.1.name', 'Owncloud')
->assertJsonPath('meta.types.0.fields.1', ['label' => 'Benutzer', 'key' => 'user', 'type' => 'text']); ->assertInertiaPath('data.meta.types.1.defaults.base_url', '')
} ->assertInertiaPath('data.meta.types.0.fields.1', ['label' => 'Benutzer', 'key' => 'user', 'type' => 'text']);
public function testItRendersComponent(): void
{
$this->withoutExceptionHandling()->login()->loginNami();
$this->get('/setting/fileshare')->assertComponent('fileshare/Index');
} }
} }