Compare commits
61 Commits
50dc714f18
...
8bc9f30459
Author | SHA1 | Date |
---|---|---|
Philipp Lang | 8bc9f30459 | |
Philipp Lang | 6fd16a7dfe | |
Philipp Lang | 277259320a | |
Philipp Lang | 45a1e165fa | |
philipp lang | d38989f302 | |
philipp lang | a434388bcb | |
philipp lang | 48d9d9cc92 | |
philipp lang | c2016af587 | |
philipp lang | 4057f9fc8d | |
philipp lang | 89f5489e86 | |
philipp lang | 7497593bb2 | |
Philipp Lang | 0080a54aaf | |
Philipp Lang | d41aa466b1 | |
Philipp Lang | 188429af55 | |
Philipp Lang | 2cb04ee142 | |
philipp lang | b87b37a673 | |
philipp lang | 8f3cc95300 | |
Philipp Lang | 904ec829c1 | |
Philipp Lang | a74d0936a2 | |
philipp lang | da3197395f | |
philipp lang | 20973b3664 | |
philipp lang | b54472c14e | |
philipp lang | 8b2bdae5d6 | |
philipp lang | ab43d42869 | |
philipp lang | 79f818c0f9 | |
philipp lang | 4a452ef1fe | |
philipp lang | 36dcff9738 | |
philipp lang | a880333f35 | |
philipp lang | 68ecce179e | |
philipp lang | 06d512e8ae | |
philipp lang | 9442714086 | |
philipp lang | c653a4b0c1 | |
Philipp Lang | 0dcb3b7d5b | |
Philipp Lang | a15b54f98d | |
Philipp Lang | f75201dfaa | |
Philipp Lang | 27c61ff8af | |
Philipp Lang | d2a000cb31 | |
Philipp Lang | 62fb792e65 | |
Philipp Lang | b2a7d2a760 | |
Philipp Lang | 9e1fe63c33 | |
Philipp Lang | 1d8b5b4670 | |
Philipp Lang | 526f2d3a6c | |
Philipp Lang | fbb501519a | |
Philipp Lang | 3485832262 | |
Philipp Lang | 4bece4a761 | |
Philipp Lang | b2b53c558e | |
Philipp Lang | 2e5385b565 | |
Philipp Lang | 754e3a0a82 | |
philipp lang | 4e27dbfe67 | |
philipp lang | c68f1e00c4 | |
philipp lang | d78740d508 | |
philipp lang | 641f3a1098 | |
philipp lang | dbbe6f6171 | |
philipp lang | e26b572575 | |
philipp lang | a526683082 | |
philipp lang | 07d309f606 | |
philipp lang | 234380120e | |
Philipp Lang | 72375affee | |
Philipp Lang | d01322b1ad | |
Philipp Lang | f30eec0e80 | |
philipp lang | f1c55bedce |
20
.drone.yml
20
.drone.yml
|
@ -24,7 +24,7 @@ steps:
|
|||
- name: node
|
||||
image: node:17.9.0-slim
|
||||
commands:
|
||||
- npm ci && npm run prod && npm run img && rm -R node_modules
|
||||
- npm ci && npm run img && npm run prod && rm -R node_modules
|
||||
|
||||
- name: phpunit_tests
|
||||
image: php:8.1.6
|
||||
|
@ -144,6 +144,24 @@ steps:
|
|||
branch: master
|
||||
event: push
|
||||
|
||||
- name: deploy dpsgbergischland
|
||||
image: drillster/drone-rsync
|
||||
settings:
|
||||
hosts: ['zoomyboy.de']
|
||||
user: dpsgbergischland
|
||||
source: ./
|
||||
target: ~/adrema
|
||||
exclude: ['.git']
|
||||
key:
|
||||
from_secret: deploy_private_key
|
||||
script:
|
||||
- cd ~/adrema
|
||||
- /usr/bin/php8.1 artisan migrate --force
|
||||
- sudo systemctl restart adremabl-horizon
|
||||
when:
|
||||
branch: master
|
||||
event: push
|
||||
|
||||
- name: github push
|
||||
image: alpine/git
|
||||
environment:
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
/node_modules
|
||||
/public/build
|
||||
/public/sprite.svg
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/js
|
||||
/public/css
|
||||
/public/fonts
|
||||
/public/img
|
||||
/public/images
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
|
@ -17,14 +13,12 @@ Homestead.yaml
|
|||
npm-debug.log
|
||||
yarn-error.log
|
||||
tags
|
||||
/public/vendor
|
||||
/storage/temp
|
||||
|
||||
# Temporary files
|
||||
*.swp
|
||||
*.swo
|
||||
*.swm
|
||||
/public/mix-manifest.json
|
||||
resources/img/sprite.svg
|
||||
/.php-cs-fixer.cache
|
||||
/data
|
||||
|
|
|
@ -6,7 +6,7 @@ Da du diese Seite besuchst, gehörst du sicherlich zu den Leuten, die möglichst
|
|||
|
||||
Die AdReMa (= "AddRessManagement") macht das auch, nur einfacher, schöner und intuitiver als es NaMi tut.
|
||||
|
||||
![Mitglieder-Übersicht](https://git.zoomyboy.de/silva/adrema/raw/branch/master/doc/assets/member.jpg)
|
||||
![Mitglieder-Übersicht](https://git.zoomyboy.de/silva/adrema/raw/branch/master/doc/page/assets/img/member.jpg)
|
||||
|
||||
AdReMa kann von jedem und jeder genutzt werden, die einen NaMi-Account besitzt und Schreibrechte hat (i.d.R. sind das Stammesvorstände, e.V.-Mitglieder und andere, die Mitgliederdaten und deren Abrechungen und Beiträge pflegen müssen).
|
||||
|
||||
|
@ -30,7 +30,6 @@ Ziel dieses Projektes ist es, viele Dinge, die man normalerweise manuell zu tun
|
|||
|
||||
Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles Design")
|
||||
|
||||
![Mobile Ansicht](https://git.zoomyboy.de/silva/adrema/raw/branch/master/doc/assets/member-mobile.jpg)
|
||||
# Installation
|
||||
|
||||
Submodules updaten:
|
||||
|
|
|
@ -11,7 +11,7 @@ class RedirectIfNotInitializedMiddleware
|
|||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public array $dontRedirect = ['initialize.form', 'initialize.store', 'nami.login-check', 'nami.search'];
|
||||
public array $dontRedirect = ['initialize.form', 'initialize.store', 'nami.login-check', 'nami.search', 'nami.get-search-layer'];
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Initialize\Actions;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\LaravelNami\Data\SearchLayerOption;
|
||||
use Zoomyboy\LaravelNami\Enum\SearchLayer;
|
||||
use Zoomyboy\LaravelNami\Nami;
|
||||
|
||||
class NamiGetSearchLayerAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*
|
||||
* @return Collection<int, SearchLayerOption>
|
||||
*/
|
||||
public function handle(array $input): Collection
|
||||
{
|
||||
return Nami::login((int) $input['mglnr'], $input['password'])->searchLayerOptions(
|
||||
SearchLayer::from($input['layer'] ?: 0),
|
||||
$input['parent'] ?: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'mglnr' => 'required|numeric|min:0',
|
||||
'password' => 'required|string',
|
||||
'parent' => 'present',
|
||||
'layer' => 'required|numeric',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
$response = $this->handle($request->validated());
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Setting\Contracts\Indexable;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use App\Setting\LocalSettings;
|
||||
|
||||
class InvoiceSettings extends LocalSettings
|
||||
class InvoiceSettings extends LocalSettings implements Indexable, Storeable
|
||||
{
|
||||
public string $from_long;
|
||||
|
||||
|
@ -41,7 +43,7 @@ class InvoiceSettings extends LocalSettings
|
|||
return SettingIndexAction::class;
|
||||
}
|
||||
|
||||
public static function saveAction(): string
|
||||
public static function storeAction(): string
|
||||
{
|
||||
return SettingSaveAction::class;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Resources\MaildispatcherResource;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class CreateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(): Response
|
||||
{
|
||||
session()->put('menu', 'maildispatcher');
|
||||
session()->put('title', 'Mail-Verteiler erstellen');
|
||||
|
||||
return Inertia::render('maildispatcher/MaildispatcherForm', [
|
||||
'meta' => MaildispatcherResource::meta(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DestroyAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Maildispatcher $maildispatcher): void
|
||||
{
|
||||
$maildispatcher->delete();
|
||||
}
|
||||
|
||||
public function asController(Maildispatcher $maildispatcher): RedirectResponse
|
||||
{
|
||||
$this->handle($maildispatcher);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Maildispatcher\Resources\MaildispatcherResource;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class EditAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(Maildispatcher $maildispatcher): Response
|
||||
{
|
||||
session()->put('menu', 'maildispatcher');
|
||||
session()->put('title', 'Mail-Verteiler bearbeiten');
|
||||
|
||||
return Inertia::render('maildispatcher/MaildispatcherForm', [
|
||||
'data' => new MaildispatcherResource($maildispatcher),
|
||||
'meta' => MaildispatcherResource::meta(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Maildispatcher\Resources\MaildispatcherResource;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(ActionRequest $request): Response
|
||||
{
|
||||
session()->put('menu', 'maildispatcher');
|
||||
session()->put('title', 'Mail-Verteiler');
|
||||
|
||||
return Inertia::render('maildispatcher/MaildispatcherIndex', [
|
||||
'data' => MaildispatcherResource::collection(Maildispatcher::with('gateway')->paginate(10)),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Data\MailEntry;
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Member\FilterScope;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Support\Collection;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ResyncAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
foreach (Maildispatcher::get() as $dispatcher) {
|
||||
$dispatcher->gateway->type->sync($dispatcher->name, $dispatcher->gateway->domain, $this->getResults($dispatcher));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, MailEntry>
|
||||
*/
|
||||
public function getResults(Maildispatcher $dispatcher): Collection
|
||||
{
|
||||
return Member::search(data_get($dispatcher->filter, 'search', ''))->query(
|
||||
fn ($q) => $q->select('*')->withFilter(FilterScope::fromPost($dispatcher->filter))
|
||||
)->get()->filter(fn ($member) => $member->email || $member->email_parents)->map(fn ($member) => MailEntry::from(['email' => $member->email ?: $member->email_parents]));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StoreAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(array $input): void
|
||||
{
|
||||
Maildispatcher::create([
|
||||
...$input,
|
||||
'filter' => (object) $input['filter'],
|
||||
]);
|
||||
ResyncAction::dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'gateway_id' => 'Verbindung',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'gateway_id' => 'required|exists:mailgateways,id',
|
||||
'name' => 'required|max:50',
|
||||
'filter' => 'present|array',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($request->validated());
|
||||
|
||||
return response()->json('', 201);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Actions;
|
||||
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class UpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(Maildispatcher $maildispatcher, array $input): void
|
||||
{
|
||||
$maildispatcher->update([
|
||||
...$input,
|
||||
'filter' => (object) $input['filter'],
|
||||
]);
|
||||
ResyncAction::dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'gateway_id' => 'Verbindung',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'gateway_id' => 'required|exists:mailgateways,id',
|
||||
'name' => 'required|max:50',
|
||||
'filter' => 'present|array',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(Maildispatcher $maildispatcher, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($maildispatcher, $request->validated());
|
||||
|
||||
return response()->json('', 201);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Data;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class MailEntry extends Data
|
||||
{
|
||||
public function __construct(public string $email)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Localmaildispatcher extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $timestamps = false;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Models;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Maildispatcher extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
|
||||
public $guarded = [];
|
||||
public $timestamps = false;
|
||||
|
||||
public $casts = [
|
||||
'filter' => 'json',
|
||||
];
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::deleting(function ($dispatcher) {
|
||||
foreach ($dispatcher->gateway->type->list($dispatcher->name, $dispatcher->gateway->domain) as $email) {
|
||||
$dispatcher->gateway->type->remove($dispatcher->name, $dispatcher->gateway->domain, $email->email);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Mailgateway, self>
|
||||
*/
|
||||
public function gateway(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mailgateway::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Maildispatcher\Resources;
|
||||
|
||||
use App\Lib\HasMeta;
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Resources\MailgatewayResource;
|
||||
use App\Member\FilterScope;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Maildispatcher
|
||||
*/
|
||||
class MaildispatcherResource extends JsonResource
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'gateway' => new MailgatewayResource($this->whenLoaded('gateway')),
|
||||
'gateway_id' => $this->gateway_id,
|
||||
'filter' => $this->filter,
|
||||
'id' => $this->id,
|
||||
'links' => [
|
||||
'edit' => route('maildispatcher.edit', ['maildispatcher' => $this->getModel()]),
|
||||
'update' => route('maildispatcher.update', ['maildispatcher' => $this->getModel()]),
|
||||
'delete' => route('maildispatcher.destroy', ['maildispatcher' => $this->getModel()]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'links' => [
|
||||
'create' => route('maildispatcher.create'),
|
||||
'index' => route('maildispatcher.index'),
|
||||
],
|
||||
'default_model' => [
|
||||
'name' => '',
|
||||
'gateway_id' => null,
|
||||
'filter' => FilterScope::from([])->toArray(),
|
||||
],
|
||||
'members' => Member::ordered()->get()->map(fn ($member) => ['id' => $member->id, 'name' => $member->fullname]),
|
||||
'gateways' => Mailgateway::pluck('name', 'id'),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?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)),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Actions;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use ValidatesRequests;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(array $input): void
|
||||
{
|
||||
$this->checkIfWorks($input);
|
||||
Mailgateway::create($input);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): void
|
||||
{
|
||||
$this->handle($request->validated());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Actions;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class UpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
use ValidatesRequests;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(Mailgateway $mailgateway, array $input): void
|
||||
{
|
||||
$this->checkIfWorks($input);
|
||||
|
||||
$mailgateway->update($input);
|
||||
}
|
||||
|
||||
public function asController(Mailgateway $mailgateway, ActionRequest $request): void
|
||||
{
|
||||
$this->handle($mailgateway, $request->validated());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Actions;
|
||||
|
||||
use App\Mailgateway\Types\Type;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
|
||||
trait ValidatesRequests
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function checkIfWorks(array $input): void
|
||||
{
|
||||
if (!app(data_get($input, 'type.cls'))->setParams($input['type']['params'])->works()) {
|
||||
throw ValidationException::withMessages(['connection' => 'Verbindung fehlgeschlagen.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'domain' => 'required|string|max:255',
|
||||
...$this->typeValidation(),
|
||||
'type.params' => 'present|array',
|
||||
...collect(request()->input('type.cls')::rules('storeValidator'))->mapWithKeys(fn ($rules, $key) => ["type.params.{$key}" => $rules]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'type.cls' => 'Typ',
|
||||
'name' => 'Beschreibung',
|
||||
'domain' => 'Domain',
|
||||
...collect(request()->input('type.cls')::fieldNames())->mapWithKeys(fn ($attribute, $key) => ["type.params.{$key}" => $attribute]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function typeValidation(): array
|
||||
{
|
||||
return [
|
||||
'type.cls' => ['required', 'string', 'max:255', Rule::in(app('mail-gateways'))],
|
||||
];
|
||||
}
|
||||
|
||||
public function prepareForValidation(ActionRequest $request): void
|
||||
{
|
||||
if (!is_subclass_of(request()->input('type.cls'), Type::class)) {
|
||||
throw ValidationException::withMessages(['type.cls' => 'Typ ist nicht valide.']);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Casts;
|
||||
|
||||
use App\Mailgateway\Types\Type;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
/**
|
||||
* @implements CastsAttributes<Type, Type>
|
||||
*/
|
||||
class TypeCast implements CastsAttributes
|
||||
{
|
||||
/**
|
||||
* Cast the given value.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param mixed $value
|
||||
* @param array<string, mixed> $attributes
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
$value = json_decode($value, true);
|
||||
|
||||
return app($value['cls'])->setParams($value['params']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the given value for storage.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @param mixed $value
|
||||
* @param array<string, mixed> $attributes
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return json_encode($value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway;
|
||||
|
||||
use App\Mailgateway\Actions\IndexAction;
|
||||
use App\Setting\Contracts\Indexable;
|
||||
use App\Setting\LocalSettings;
|
||||
|
||||
class MailgatewaySettings extends LocalSettings implements Indexable
|
||||
{
|
||||
public static function group(): string
|
||||
{
|
||||
return 'mailgateway';
|
||||
}
|
||||
|
||||
public static function slug(): string
|
||||
{
|
||||
return 'mailgateway';
|
||||
}
|
||||
|
||||
public static function indexAction(): string
|
||||
{
|
||||
return IndexAction::class;
|
||||
}
|
||||
|
||||
public static function title(): string
|
||||
{
|
||||
return 'E-Mail-Verbindungen';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Models;
|
||||
|
||||
use App\Mailgateway\Casts\TypeCast;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Mailgateway extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
|
||||
public $casts = ['type' => TypeCast::class];
|
||||
public $guarded = [];
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Resources;
|
||||
|
||||
use App\Lib\HasMeta;
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Mailgateway
|
||||
*/
|
||||
class MailgatewayResource extends JsonResource
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'domain' => $this->domain,
|
||||
'type_human' => $this->type::name(),
|
||||
'works' => $this->type->works(),
|
||||
'type' => $this->type->toResource(),
|
||||
'id' => $this->id,
|
||||
'links' => [
|
||||
'update' => route('mailgateway.update', ['mailgateway' => $this->getModel()]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'links' => [
|
||||
'store' => route('mailgateway.store'),
|
||||
],
|
||||
'types' => app('mail-gateways')->map(fn ($gateway) => [
|
||||
'id' => $gateway,
|
||||
'name' => $gateway::name(),
|
||||
'fields' => $gateway::presentFields('storeValidator'),
|
||||
'defaults' => (object) $gateway::defaults(),
|
||||
])->prepend([
|
||||
'id' => null,
|
||||
'name' => '-- kein --',
|
||||
'fields' => [],
|
||||
'defaults' => (object) [],
|
||||
]),
|
||||
'default' => [
|
||||
'domain' => '',
|
||||
'name' => '',
|
||||
'type' => [
|
||||
'params' => [],
|
||||
'cls' => null,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Types;
|
||||
|
||||
use App\Maildispatcher\Data\MailEntry;
|
||||
use App\Maildispatcher\Models\Localmaildispatcher;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LocalType extends Type
|
||||
{
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Lokal';
|
||||
}
|
||||
|
||||
public function works(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function fields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function setParams(array $params): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function list(string $name, string $domain): Collection
|
||||
{
|
||||
return Localmaildispatcher::where('from', "{$name}@{$domain}")->get()->map(fn ($mail) => MailEntry::from(['email' => $mail->to]));
|
||||
}
|
||||
|
||||
public function search(string $name, string $domain, string $email): ?MailEntry
|
||||
{
|
||||
$result = Localmaildispatcher::where('from', "{$name}@{$domain}")->where('to', $email)->first();
|
||||
|
||||
return $result ? MailEntry::from([
|
||||
'email' => $result->to,
|
||||
]) : null;
|
||||
}
|
||||
|
||||
public function add(string $name, string $domain, string $email): void
|
||||
{
|
||||
Localmaildispatcher::create([
|
||||
'from' => "{$name}@{$domain}",
|
||||
'to' => $email,
|
||||
]);
|
||||
}
|
||||
|
||||
public function remove(string $name, string $domain, string $email): void
|
||||
{
|
||||
Localmaildispatcher::where('from', "{$name}@{$domain}")->where('to', $email)->delete();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Types;
|
||||
|
||||
use App\Maildispatcher\Data\MailEntry;
|
||||
use App\Mailman\Exceptions\MailmanServiceException;
|
||||
use App\Mailman\Support\MailmanService;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class MailmanType extends Type
|
||||
{
|
||||
public string $url;
|
||||
public string $user;
|
||||
public string $password;
|
||||
|
||||
public function setParams(array $params): static
|
||||
{
|
||||
$this->url = data_get($params, 'url');
|
||||
$this->user = data_get($params, 'user');
|
||||
$this->password = data_get($params, 'password');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Mailman';
|
||||
}
|
||||
|
||||
public function works(): bool
|
||||
{
|
||||
return $this->service()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fields(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'url',
|
||||
'label' => 'URL',
|
||||
'type' => 'text',
|
||||
'storeValidator' => 'required|max:255',
|
||||
'updateValidator' => 'required|max:255',
|
||||
'default' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'user',
|
||||
'label' => 'Benutzer',
|
||||
'type' => 'text',
|
||||
'storeValidator' => 'required|max:255',
|
||||
'updateValidator' => 'required|max:255',
|
||||
'default' => '',
|
||||
],
|
||||
[
|
||||
'name' => 'password',
|
||||
'label' => 'Passwort',
|
||||
'type' => 'password',
|
||||
'storeValidator' => 'required|max:255',
|
||||
'updateValidator' => 'nullable|max:255',
|
||||
'default' => '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function search(string $name, string $domain, string $email): ?MailEntry
|
||||
{
|
||||
$list = $this->service()->getLists()->first(fn ($list) => $list->fqdnListname === "{$name}@{$domain}");
|
||||
throw_if(!$list, MailmanServiceException::class, "List for {$name}@{$domain} not found");
|
||||
$member = $this->service()->members($list)->first(fn ($member) => $member->email === $email);
|
||||
|
||||
return $member ? MailEntry::from(['email' => $member->email]) : null;
|
||||
}
|
||||
|
||||
public function add(string $name, string $domain, string $email): void
|
||||
{
|
||||
$list = $this->service()->getLists()->first(fn ($list) => $list->fqdnListname === "{$name}@{$domain}");
|
||||
throw_if(!$list, MailmanServiceException::class, "List for {$name}@{$domain} not found");
|
||||
$this->service()->addMember($list, $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function list(string $name, string $domain): Collection
|
||||
{
|
||||
$list = $this->service()->getLists()->first(fn ($list) => $list->fqdnListname === "{$name}@{$domain}");
|
||||
throw_if(!$list, MailmanServiceException::class, "List for {$name}@{$domain} not found");
|
||||
|
||||
return collect($this->service()->members($list)->map(fn ($member) => MailEntry::from(['email' => $member->email]))->all());
|
||||
}
|
||||
|
||||
public function remove(string $name, string $domain, string $email): void
|
||||
{
|
||||
$list = $this->service()->getLists()->first(fn ($list) => $list->fqdnListname === "{$name}@{$domain}");
|
||||
throw_if(!$list, MailmanServiceException::class, "List for {$name}@{$domain} not found");
|
||||
$member = $this->service()->members($list)->first(fn ($member) => $member->email === $email);
|
||||
throw_if(!$member, MailmanServiceException::class, 'Member for removing not found');
|
||||
$this->service()->removeMember($member);
|
||||
}
|
||||
|
||||
private function service(): MailmanService
|
||||
{
|
||||
return app(MailmanService::class)->setCredentials($this->url, $this->user, $this->password);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailgateway\Types;
|
||||
|
||||
use App\Maildispatcher\Data\MailEntry;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class Type
|
||||
{
|
||||
abstract public static function name(): string;
|
||||
|
||||
/**
|
||||
* @return array<int, MailgatewayCustomField>
|
||||
*/
|
||||
abstract public static function fields(): array;
|
||||
|
||||
abstract public function works(): bool;
|
||||
|
||||
abstract public function search(string $name, string $domain, string $email): ?MailEntry;
|
||||
|
||||
abstract public function add(string $name, string $domain, string $email): void;
|
||||
|
||||
abstract public function remove(string $name, string $domain, string $email): void;
|
||||
|
||||
/**
|
||||
* @return Collection<int, MailEntry>
|
||||
*/
|
||||
abstract public function list(string $name, string $domain): Collection;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
abstract public function setParams(array $params): static;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function defaults(): array
|
||||
{
|
||||
return collect(static::fields())->mapWithKeys(fn ($field) => [
|
||||
$field['name'] => $field['default'],
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, MailgatewayParsedCustomField>
|
||||
*/
|
||||
public static function presentFields(string $validator): array
|
||||
{
|
||||
return array_map(fn ($field) => [
|
||||
...$field,
|
||||
'is_required' => str_contains($field[$validator], 'required'),
|
||||
], static::fields());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(string $validator): array
|
||||
{
|
||||
return collect(static::fields())->mapWithKeys(fn ($field) => [
|
||||
$field['name'] => $field[$validator],
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function toResource(): array
|
||||
{
|
||||
return [
|
||||
'cls' => get_class($this),
|
||||
'params' => get_object_vars($this),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, MailEntry> $results
|
||||
*/
|
||||
public function sync(string $name, string $domain, Collection $results): void
|
||||
{
|
||||
foreach ($results as $result) {
|
||||
if ($this->search($name, $domain, $result->email)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->add($name, $domain, $result->email);
|
||||
}
|
||||
|
||||
$this->list($name, $domain)
|
||||
->filter(fn ($listEntry) => null === $results->first(fn ($r) => $r->email === $listEntry->email))
|
||||
->each(fn ($listEntry) => $this->remove($name, $domain, $listEntry->email));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function fieldNames(): array
|
||||
{
|
||||
return collect(static::fields())->mapWithKeys(fn ($field) => [$field['name'] => $field['label']])->toArray();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ use Spatie\LaravelData\Attributes\MapName;
|
|||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
use Tests\RequestFactories\MailinglistFactory;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class MailingList extends Data
|
||||
|
@ -24,4 +25,9 @@ class MailingList extends Data
|
|||
public int $volume,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function factory(): MailinglistFactory
|
||||
{
|
||||
return MailinglistFactory::new();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mailman\Data;
|
||||
|
||||
use Spatie\LaravelData\Attributes\MapName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapName(SnakeCaseMapper::class)]
|
||||
class Member extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $email,
|
||||
public string $memberId,
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -4,9 +4,11 @@ namespace App\Mailman;
|
|||
|
||||
use App\Mailman\Actions\SettingIndexAction;
|
||||
use App\Mailman\Actions\SettingSaveAction;
|
||||
use App\Setting\Contracts\Indexable;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use App\Setting\LocalSettings;
|
||||
|
||||
class MailmanSettings extends LocalSettings
|
||||
class MailmanSettings extends LocalSettings implements Storeable, Indexable
|
||||
{
|
||||
public ?string $base_url;
|
||||
|
||||
|
@ -39,7 +41,7 @@ class MailmanSettings extends LocalSettings
|
|||
return SettingIndexAction::class;
|
||||
}
|
||||
|
||||
public static function saveAction(): string
|
||||
public static function storeAction(): string
|
||||
{
|
||||
return SettingSaveAction::class;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Mailman\Support;
|
||||
|
||||
use App\Mailman\Data\MailingList;
|
||||
use App\Mailman\Data\Member;
|
||||
use App\Mailman\Exceptions\MailmanServiceException;
|
||||
use App\Mailman\MailmanSettings;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
@ -43,26 +44,50 @@ class MailmanService
|
|||
}
|
||||
|
||||
/**
|
||||
* @return LazyCollection<int, string>
|
||||
* @return LazyCollection<int, Member>
|
||||
*/
|
||||
public function members(string $listId): LazyCollection
|
||||
public function members(MailingList $list): LazyCollection
|
||||
{
|
||||
return app(Paginator::class)->result(
|
||||
fn ($page) => $this->http()->get("/lists/{$listId}/roster/member?page={$page}&count=10"),
|
||||
function ($response) use ($listId) {
|
||||
throw_unless($response->ok(), MailmanServiceException::class, 'Fetching members for listId '.$listId.' failed.');
|
||||
/** @var array<int, array{email: string}>|null */
|
||||
$entries = data_get($response->json(), 'entries');
|
||||
fn ($page) => $this->http()->get("/lists/{$list->listId}/roster/member?page={$page}&count=10"),
|
||||
function ($response) use ($list) {
|
||||
throw_unless($response->ok(), MailmanServiceException::class, 'Fetching members for listId '.$list->listId.' failed.');
|
||||
/** @var array<int, array{email: string, self_link: string}>|null */
|
||||
$entries = data_get($response->json(), 'entries', []);
|
||||
throw_if(is_null($entries), MailmanServiceException::class, 'Failed getting member list from response');
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
yield $entry['email'];
|
||||
yield Member::from([
|
||||
...$entry,
|
||||
'member_id' => strrev(preg_split('/\//', strrev($entry['self_link']))[0]),
|
||||
]);
|
||||
}
|
||||
},
|
||||
fn ($response) => data_get($response->json(), 'total_size')
|
||||
);
|
||||
}
|
||||
|
||||
public function addMember(MailingList $list, string $email): void
|
||||
{
|
||||
$response = $this->http()->post('members', [
|
||||
'list_id' => $list->listId,
|
||||
'subscriber' => $email,
|
||||
'pre_verified' => 'true',
|
||||
'pre_approved' => 'true',
|
||||
'send_welcome_message' => 'false',
|
||||
'pre_confirmed' => 'true',
|
||||
]);
|
||||
|
||||
throw_unless(201 === $response->status(), MailmanServiceException::class, 'Adding member '.$email.' to '.$list->listId.' failed');
|
||||
}
|
||||
|
||||
public function removeMember(Member $member): void
|
||||
{
|
||||
$response = $this->http()->delete("members/{$member->memberId}");
|
||||
|
||||
throw_unless(204 === $response->status(), MailmanServiceException::class, 'Removing member failed');
|
||||
}
|
||||
|
||||
private function http(): PendingRequest
|
||||
{
|
||||
return Http::withBasicAuth($this->username, $this->password)->withOptions(['base_uri' => $this->baseUrl]);
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace App\Member\Actions;
|
||||
|
||||
use App\Member\FilterScope;
|
||||
use App\Member\Member;
|
||||
use App\Member\MemberResource;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
|
@ -14,27 +15,18 @@ class SearchAction
|
|||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return Collection<int, Member>
|
||||
* @return LengthAwarePaginator<int, Member>
|
||||
*/
|
||||
public function handle(string $search): Collection
|
||||
public function handle(FilterScope $filter): LengthAwarePaginator
|
||||
{
|
||||
return Member::search($search)->query(fn ($query) => $query->ordered())->get();
|
||||
return Member::search($filter->search)->query(fn ($q) => $q->select('*')
|
||||
->withFilter($filter)
|
||||
->ordered()
|
||||
)->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): AnonymousResourceCollection
|
||||
{
|
||||
if (null !== $request->input('minLength') && strlen($request->input('search', '')) < $request->input('minLength')) {
|
||||
return MemberResource::collection($this->empty());
|
||||
}
|
||||
|
||||
return MemberResource::collection($this->handle($request->input('search', '')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Member>
|
||||
*/
|
||||
private function empty(): Collection
|
||||
{
|
||||
return Member::where('id', -1)->get();
|
||||
return MemberResource::collection($this->handle(FilterScope::fromRequest($request->input('filter', ''))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,20 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
|||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class FilterScope extends Filter
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $activityIds
|
||||
* @param array<int, int> $subactivityIds
|
||||
* @param array<int, int> $groupIds
|
||||
* @param array<int, int> $additional
|
||||
*/
|
||||
public function __construct(
|
||||
public bool $ausstand = false,
|
||||
public ?string $billKind = null,
|
||||
public ?int $activityId = null,
|
||||
public ?int $subactivityId = null,
|
||||
public string $search = '',
|
||||
public ?int $groupId = null,
|
||||
public array $activityIds = [],
|
||||
public array $subactivityIds = [],
|
||||
public ?string $search = '',
|
||||
public array $groupIds = [],
|
||||
public array $additional = [],
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -41,29 +48,35 @@ class FilterScope extends Filter
|
|||
*/
|
||||
public function apply(Builder $query): Builder
|
||||
{
|
||||
if ($this->ausstand) {
|
||||
$query->whereAusstand();
|
||||
}
|
||||
$query->orWhere(function ($query) {
|
||||
if ($this->ausstand) {
|
||||
$query->whereAusstand();
|
||||
}
|
||||
|
||||
if ($this->billKind) {
|
||||
$query->where('bill_kind', BillKind::fromValue($this->billKind));
|
||||
}
|
||||
if ($this->billKind) {
|
||||
$query->where('bill_kind', BillKind::fromValue($this->billKind));
|
||||
}
|
||||
|
||||
if ($this->groupId) {
|
||||
$query->where('group_id', $this->groupId);
|
||||
}
|
||||
if (count($this->groupIds)) {
|
||||
$query->whereIn('group_id', $this->groupIds);
|
||||
}
|
||||
|
||||
if ($this->subactivityId || $this->activityId) {
|
||||
$query->whereHas('memberships', function ($q) {
|
||||
$q->active();
|
||||
if ($this->subactivityId) {
|
||||
$q->where('subactivity_id', $this->subactivityId);
|
||||
}
|
||||
if ($this->activityId) {
|
||||
$q->where('activity_id', $this->activityId);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (count($this->subactivityIds) + count($this->activityIds) > 0) {
|
||||
$query->whereHas('memberships', function ($q) {
|
||||
$q->active();
|
||||
if (count($this->subactivityIds)) {
|
||||
$q->whereIn('subactivity_id', $this->subactivityIds);
|
||||
}
|
||||
if (count($this->activityIds)) {
|
||||
$q->whereIn('activity_id', $this->activityIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
})->orWhere(function ($query) {
|
||||
if (count($this->additional)) {
|
||||
$query->whereIn('id', $this->additional);
|
||||
}
|
||||
});
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
|
|
@ -282,7 +282,7 @@ class Member extends Model implements Geolocatable
|
|||
*/
|
||||
public function ageGroupMemberships(): HasMany
|
||||
{
|
||||
return $this->memberships()->isAgeGroup();
|
||||
return $this->memberships()->isAgeGroup()->active();
|
||||
}
|
||||
|
||||
public static function booted()
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Member;
|
|||
|
||||
use App\Country;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Maildispatcher\Actions\ResyncAction;
|
||||
use App\Setting\GeneralSettings;
|
||||
use App\Setting\NamiSettings;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
@ -86,6 +87,7 @@ class MemberController extends Controller
|
|||
}
|
||||
|
||||
$member->delete();
|
||||
ResyncAction::dispatch();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace App\Member;
|
|||
use App\Activity;
|
||||
use App\Group;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Maildispatcher\Actions\ResyncAction;
|
||||
use App\Member\Actions\NamiPutMemberAction;
|
||||
use App\Setting\NamiSettings;
|
||||
use App\Subactivity;
|
||||
|
@ -99,6 +100,7 @@ class MemberRequest extends FormRequest
|
|||
Subactivity::find($this->input('first_subactivity_id')),
|
||||
);
|
||||
}
|
||||
ResyncAction::dispatch();
|
||||
}
|
||||
|
||||
public function persistUpdate(Member $member): void
|
||||
|
@ -118,5 +120,6 @@ class MemberRequest extends FormRequest
|
|||
if (!$this->input('has_nami') && null !== $member->nami_id) {
|
||||
DeleteJob::dispatch($member->nami_id);
|
||||
}
|
||||
ResyncAction::dispatch();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,7 @@ class MemberResource extends JsonResource
|
|||
'genders' => Gender::pluck('name', 'id'),
|
||||
'billKinds' => BillKind::forSelect(),
|
||||
'nationalities' => Nationality::pluck('name', 'id'),
|
||||
'members' => Member::ordered()->get()->map(fn ($member) => ['id' => $member->id, 'name' => $member->fullname]),
|
||||
'links' => [
|
||||
'index' => route('member.index'),
|
||||
'create' => route('member.create'),
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
@ -26,6 +28,11 @@ class AppServiceProvider extends ServiceProvider
|
|||
|
||||
return $this;
|
||||
});
|
||||
|
||||
app()->bind('mail-gateways', fn () => collect([
|
||||
LocalType::class,
|
||||
MailmanType::class,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Setting\Contracts;
|
||||
|
||||
interface Indexable
|
||||
{
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
public static function indexAction(): string;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Setting\Contracts;
|
||||
|
||||
interface Storeable
|
||||
{
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
public static function storeAction(): string;
|
||||
}
|
|
@ -14,16 +14,6 @@ class GeneralSettings extends Settings
|
|||
/** @var array<int, int> */
|
||||
public array $allowed_nami_accounts;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function moduleOptions(): array
|
||||
{
|
||||
return [
|
||||
'bill',
|
||||
];
|
||||
}
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
return 'general';
|
||||
|
|
|
@ -8,16 +8,6 @@ abstract class LocalSettings extends Settings
|
|||
{
|
||||
abstract public static function slug(): string;
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
abstract public static function indexAction(): string;
|
||||
|
||||
/**
|
||||
* @return class-string
|
||||
*/
|
||||
abstract public static function saveAction(): string;
|
||||
|
||||
abstract public static function title(): string;
|
||||
|
||||
public static function url(): string
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Setting;
|
||||
|
||||
use App\Setting\Contracts\Indexable;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use Illuminate\Routing\Router;
|
||||
|
||||
class SettingFactory
|
||||
|
@ -12,14 +14,19 @@ class SettingFactory
|
|||
private array $settings = [];
|
||||
|
||||
/**
|
||||
* @param class-string<LocalSettings> $setting
|
||||
* @param class-string $setting
|
||||
*/
|
||||
public function register(string $setting): void
|
||||
{
|
||||
$this->settings[] = $setting;
|
||||
|
||||
app(Router::class)->middleware(['web', 'auth:web', SettingMiddleware::class])->get($setting::url(), $setting::indexAction());
|
||||
app(Router::class)->middleware(['web', 'auth:web', SettingMiddleware::class])->post($setting::url(), $setting::saveAction());
|
||||
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)) {
|
||||
app(Router::class)->redirect('/setting', '/setting/'.$setting::slug());
|
||||
|
@ -33,7 +40,7 @@ class SettingFactory
|
|||
{
|
||||
return collect($this->settings)->map(fn ($setting) => [
|
||||
'url' => $setting::url(),
|
||||
'is_active' => request()->fullUrlIs(url($setting::url())),
|
||||
'is_active' => '/'.request()->path() === $setting::url(),
|
||||
'title' => $setting::title(),
|
||||
])
|
||||
->toArray();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Setting;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Mailgateway\MailgatewaySettings;
|
||||
use App\Mailman\MailmanSettings;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
@ -27,5 +28,6 @@ class SettingServiceProvider extends ServiceProvider
|
|||
{
|
||||
app(SettingFactory::class)->register(InvoiceSettings::class);
|
||||
app(SettingFactory::class)->register(MailmanSettings::class);
|
||||
app(SettingFactory::class)->register(MailgatewaySettings::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Maildispatcher\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Maildispatcher\Models\Maildispatcher>
|
||||
*/
|
||||
class MaildispatcherFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Mailgateway\Models;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Types\Type;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Mailgateway\Models\Mailgateway>
|
||||
*/
|
||||
class MailgatewayFactory extends Factory
|
||||
{
|
||||
protected $model = Mailgateway::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(5, true),
|
||||
'type' => [
|
||||
'cls' => app('mail-gateways')->random(),
|
||||
'params' => [],
|
||||
],
|
||||
'domain' => $this->faker->safeEmailDomain(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<Type> $type
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public function type(string $type, array $params): self
|
||||
{
|
||||
return $this->state(['type' => [
|
||||
'cls' => $type,
|
||||
'params' => $params,
|
||||
]]);
|
||||
}
|
||||
|
||||
public function name(string $name): self
|
||||
{
|
||||
return $this->state(['name' => $name]);
|
||||
}
|
||||
|
||||
public function domain(string $domain): self
|
||||
{
|
||||
return $this->state(['domain' => $domain]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('mailgateways', function (Blueprint $table) {
|
||||
$table->uuid('id');
|
||||
$table->string('name');
|
||||
$table->string('domain');
|
||||
$table->json('type');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('mailgateways');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('maildispatchers', function (Blueprint $table) {
|
||||
$table->uuid('id');
|
||||
$table->string('name');
|
||||
$table->uuid('gateway_id');
|
||||
$table->json('filter');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('maildispatchers');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class() extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('localmaildispatchers', function (Blueprint $table) {
|
||||
$table->uuid('id');
|
||||
$table->string('from');
|
||||
$table->string('to');
|
||||
$table->unique(['from', 'to']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('localmaildispatchers');
|
||||
}
|
||||
};
|
|
@ -38,7 +38,7 @@ Für einige Prozesse benötigt Adrema die Standard-Gruppierungsnummer. Dies ist
|
|||
|
||||
{% include imgcap.html img='init-default-groupid' caption="Standard-Gruppierungsnummer" %}
|
||||
|
||||
## 4. Initialisierung starten
|
||||
## Initialisierung starten
|
||||
|
||||
Danach führt Adrema im Hintergrund selbstständig einen ersten Abgleich durch. Dies kann je nach Datenmenge einige Minuten bis Stunden dauern.
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
layout: page
|
||||
title: Mailverteiler
|
||||
has_children: true
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Mailverteiler
|
||||
|
||||
Mit der Adrema kannst du E-Mail-Verteiler (also E-Mail-Adressen, die an sämtliche andere E-Mail-Adressen weiterleiten, sowas wie "eltern@stamm-bipi.de") verwalten.
|
||||
|
||||
Dabei werden E-Mail-Adressen anhand von Filter-Regeln automatisch in Verteiler aufgenommen oder entfernt bzw aktuell gehalten. Die Verteiler-Verwaltung ist damit Adrema-intern möglich.
|
||||
|
||||
Auf den folgenden Seiten erkläre ich dir, wie du eine Verbindung mit deinem bestehenden E-Mail-System herstellst, danach Verteiler anlegst und diese aktuell hälst.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
layout: page
|
||||
title: Verteiler verwalten
|
||||
parent: Mailverteiler
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Verteiler verwalten
|
||||
|
||||
Wähle als Name den Namen der E-Mail-Adresse des Verteilers (das ist alles was vor dem "@" Zeichen steht). Es wird empfohlen, hier nur Kleinbuchstaben und "-" zu verwenden.
|
||||
|
||||
Wähle danach die Verbindung aus. Es wird für den Verteiler die Domain dieser Verbindung verwendet.
|
||||
|
||||
Wenn du also eine Verbindung mit der Domain "bipi.de" angelegt hast, einen Verteiler mit dieser Verbindung erstellst und dabei als Namen "leute" vergibst, so würde sich daraus die E-Mail-Adresse
|
||||
|
||||
leute@bipi.de
|
||||
|
||||
ergeben.
|
||||
|
||||
Wähle als nächstes die Kriterien aus, nach denen Mitglieder in den Verteiler aufgenommen werden sollen. Du erhälst darunter eine Vorschau der Mitglieder, so wie sie im Verteiler stehen würden. Dies ist natürlich nur eine Momentaufnahme.
|
||||
|
||||
Mitglieder ohne E-Mail-Adressen werden nicht hinzugefügt.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
layout: page
|
||||
title: Verbindungen verwalten
|
||||
parent: Mailverteiler
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
|
||||
# E-Mail-Verbindung anlegen
|
||||
|
||||
Die Adrema selbst leitet keine E-Mails weiter, sondern regelt nur, wer mit welcher E-Mail-Adresse Mitglied in einem bereits bestehendem Verteiler ist.
|
||||
|
||||
Um also die E-Mail-Verteiler-Funktion nutzen zu können, musst du vorher eine Verbindung zu einem bestehenden E-Mail-System herstellen.
|
||||
|
||||
## Unterstützte Systeme
|
||||
|
||||
### Lokal
|
||||
|
||||
Dies ist genau genommen kein Verteiler-System, sondern lediglich eine Datenbank, die intern gepflegt wird.
|
||||
|
||||
Wähle dies, wenn du einen eigene E-Mail-Server betreibst (Postfix, Qmail, o.ä.).
|
||||
|
||||
### Mailman (3.0)
|
||||
|
||||
Mailman ist ein Programm, mit dem man Mailinglisten verwalten kann. Es unterstützt eine Vielzahl von Typen von Mailinglisten, wie z.B. allgemeine Diskussionslisten aber auch reine Informationslisten für Veröffentlichungen.
|
||||
|
||||
{: .info }
|
||||
Es werden von AdReMa keine neuen Verteiler erstellt oder entfernt. Es werden nur die Mitglieder eines bestehenden Verteilers verwaltet. Du musst die E-Mail-Verteiler also vorher in Mailman angelegt haben, damit die AdReMa diese verwalten kann.
|
||||
|
||||
[Zur Dokumentation vom Mailman](https://list.org/)
|
||||
|
||||
Wenn bei deiner Stammes-Domain Mailman zum Einsatz kommt, kannst du dich mit deinem bestehenden Mailman-System verbinden.
|
||||
|
||||
## Mailingliste erstellen
|
||||
|
||||
Gehe hierzu auf "Einstellungen -> E-Mail-Verbindungen" und wähle "Neue Verbindung".
|
||||
|
||||
Fülle nun die folgenden Felder aus:
|
||||
|
||||
* Bezeichnung: frei wählbarer Name - technisch keine Bedeutung
|
||||
* Domain: Domain der E-Mail-Adressen. Alle angelegten Verteiler, die diese Verbindung nutzen haben eine E-Mail-Adresse mit dieser Domain
|
||||
* Typ: Wähle hier das System aus (siehe oben)
|
||||
|
||||
### Zusatzeinstellungen bei Lokal
|
||||
|
||||
Bei lokalen Verteilern sind keine weiteren Einstellungen erforderlich. Diese werden hingegen in deinem existierenden Mailserver (Postfix, Qmail, etc) konfiguriert.
|
||||
|
||||
### Zusatzeinstellungen bei Mailman
|
||||
|
||||
Trage hier die URL, Benutzer und Passwort für die Mailman-Schnittstelle ein. Diese hast du entweder beim Einrichten von Mailman selbst konfiguriert, oder dein Hoster wird diese Daten bereitstellen.
|
||||
|
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
|
@ -2,12 +2,12 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
"development": "npx mix build",
|
||||
"watch": "npx mix watch",
|
||||
"hot": "npx mix watch --hot",
|
||||
"prod": "npx mix build --production",
|
||||
"development": "npx vite",
|
||||
"watch": "npx vite",
|
||||
"hot": "npx vite",
|
||||
"prod": "npx vite build",
|
||||
"production": "npm run prod",
|
||||
"img": "rm -R public/img && cd resources/img/svg && npx svg-sprite -s --symbol-dest=sprite *.svg && mv sprite/svg/sprite.css.svg ../sprite.svg && rm -R sprite && cd ../../../ && cp -R resources/img public/img",
|
||||
"img": "cd resources/img/svg && npx svg-sprite -s --symbol-dest=sprite *.svg && mv sprite/svg/sprite.css.svg ../../../public/sprite.svg && rm -R sprite",
|
||||
"lint": "eslint \"resources/js/**/*.{js,vue}\"",
|
||||
"fix": "eslint \"resources/js/**/*.{js,vue}\" --fix"
|
||||
},
|
||||
|
@ -16,9 +16,8 @@
|
|||
"axios": "^1.3.4",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-plugin-vue": "^8.4.1",
|
||||
"laravel-mix": "^6.0.1",
|
||||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.2",
|
||||
"tailwindcss": "^3.3",
|
||||
"vue": "2.7",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-loader": "^15.9.8",
|
||||
|
@ -28,6 +27,9 @@
|
|||
"@inertiajs/inertia": "^0.11.0",
|
||||
"@inertiajs/inertia-vue": "^0.8.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@vitejs/plugin-vue2": "^2.2.0",
|
||||
"change-case": "^4.1.2",
|
||||
"laravel-vite-plugin": "^0.7.7",
|
||||
"leaflet": "^1.9.3",
|
||||
"lodash": "^4.17.21",
|
||||
"merge": "^2.1.1",
|
||||
|
@ -36,6 +38,7 @@
|
|||
"postcss-import": "^14.0.1",
|
||||
"svg-sprite": "^2.0.2",
|
||||
"v-tooltip": "^2.1.3",
|
||||
"vite": "^4.3.8",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"vue2-leaflet": "^2.7.1",
|
||||
"wnumb": "^1.2.0"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 33875d36fa5bd6fab4147e95f4aa705092f42d93
|
||||
Subproject commit c5ea29af1bb1591238bb037da93739f5bd874334
|
42
phpstan.neon
42
phpstan.neon
|
@ -18,6 +18,8 @@ parameters:
|
|||
ContributionMemberData: 'array<string, mixed>'
|
||||
ContributionRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, members: array<int, int>}'
|
||||
ContributionApiRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, member_data: array<int, ContributionMemberData>}'
|
||||
MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}'
|
||||
MailgatewayParsedCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string, is_required: bool}'
|
||||
|
||||
ignoreErrors:
|
||||
-
|
||||
|
@ -633,3 +635,43 @@ parameters:
|
|||
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<int,class\\-string\\<App\\\\Setting\\\\LocalSettings\\>\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
|
||||
count: 1
|
||||
path: app/Setting/SettingFactory.php
|
||||
|
||||
-
|
||||
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email\\.$#"
|
||||
count: 2
|
||||
path: app/Maildispatcher/Actions/ResyncAction.php
|
||||
|
||||
-
|
||||
message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$email_parents\\.$#"
|
||||
count: 2
|
||||
path: app/Maildispatcher/Actions/ResyncAction.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TKey in call to function collect$#"
|
||||
count: 2
|
||||
path: app/Mailgateway/Actions/StoreAction.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TValue in call to function collect$#"
|
||||
count: 2
|
||||
path: app/Mailgateway/Actions/StoreAction.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TKey in call to function collect$#"
|
||||
count: 2
|
||||
path: app/Mailgateway/Actions/UpdateAction.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TValue in call to function collect$#"
|
||||
count: 2
|
||||
path: app/Mailgateway/Actions/UpdateAction.php
|
||||
|
||||
-
|
||||
message: "#^Return type of call to method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),mixed\\>\\:\\:map\\(\\) contains unresolvable type\\.$#"
|
||||
count: 1
|
||||
path: app/Mailgateway/Resources/MailgatewayResource.php
|
||||
|
||||
-
|
||||
message: "#^Generic type Illuminate\\\\Pagination\\\\LengthAwarePaginator\\<int, App\\\\Member\\\\Member\\> in PHPDoc tag @return specifies 2 template types, but class Illuminate\\\\Pagination\\\\LengthAwarePaginator supports only 1\\: TValue$#"
|
||||
count: 1
|
||||
path: app/Member/Actions/SearchAction.php
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
'tailwindcss': {},
|
||||
'autoprefixer': {},
|
||||
},
|
||||
};
|
|
@ -2,15 +2,13 @@
|
|||
@import 'tailwindcss/components';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@layer components {
|
||||
@import 'base';
|
||||
@import 'switch';
|
||||
@import 'layout';
|
||||
@import 'buttons';
|
||||
@import 'table';
|
||||
@import 'sidebar';
|
||||
@import 'bool';
|
||||
@import 'form';
|
||||
@import 'tooltip';
|
||||
@import 'leaflet';
|
||||
}
|
||||
@import 'base.css';
|
||||
@import 'switch';
|
||||
@import 'layout';
|
||||
@import 'buttons';
|
||||
@import 'table';
|
||||
@import 'sidebar';
|
||||
@import 'bool';
|
||||
@import 'form';
|
||||
@import 'tooltip';
|
||||
@import 'leaflet';
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
@apply mb-4;
|
||||
}
|
||||
|
||||
|
||||
.form-control-font {
|
||||
@apply leading-tight text-gray-600 text-sm;
|
||||
}
|
||||
|
@ -18,7 +17,8 @@
|
|||
top: 34px;
|
||||
transition: color 02s, padding-bottom 02s, font-size 02s, top 02s;
|
||||
}
|
||||
.field-wrapperfocused label-placeholder, label-placeholder-focused {
|
||||
.field-wrapperfocused label-placeholder,
|
||||
label-placeholder-focused {
|
||||
top: 0;
|
||||
@apply text-sm text-gray-600;
|
||||
padding-bottom: 10px;
|
||||
|
@ -42,24 +42,25 @@
|
|||
@apply py-2;
|
||||
}
|
||||
|
||||
input, select {
|
||||
input,
|
||||
select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="datetime-local"],
|
||||
input[type="time"],
|
||||
input[type="month"],
|
||||
input[type='date'],
|
||||
input[type='datetime-local'],
|
||||
input[type='time'],
|
||||
input[type='month'],
|
||||
select,
|
||||
input[type="week"] {
|
||||
input[type='week'] {
|
||||
height: 37px;
|
||||
&::-webkit-inner-spin-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-inner-spin-button,
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
input[type='date']::-webkit-inner-spin-button,
|
||||
input[type='date']::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
}
|
||||
&.btn-danger {
|
||||
@apply bg-red-400 text-red-100 hover:bg-red-300;
|
||||
&:not(.disabled):hover {
|
||||
@apply bg-red-500 text-red-100;
|
||||
}
|
||||
}
|
||||
|
||||
&.label {
|
||||
|
|
|
@ -82,10 +82,8 @@
|
|||
}
|
||||
|
||||
&.popover {
|
||||
$color: #f9f9f9;
|
||||
|
||||
.popover-inner {
|
||||
background: $color;
|
||||
background: #f9f9f9;
|
||||
color: black;
|
||||
padding: 24px;
|
||||
border-radius: 5px;
|
||||
|
@ -93,7 +91,7 @@
|
|||
}
|
||||
|
||||
.popover-arrow {
|
||||
border-color: $color;
|
||||
border-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M257,210c-24.814,0-45,20.186-45,45c0,24.814,20.186,45,45,45c24.814,0,45-20.186,45-45C302,230.186,281.814,210,257,210z
|
||||
"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M255,0C114.39,0,0,114.39,0,255s114.39,257,255,257s257-116.39,257-257S395.61,0,255,0z M362,330
|
||||
c-20.273,0-38.152-10.161-49.017-25.596C299.23,319.971,279.354,330,257,330c-41.353,0-75-33.647-75-75c0-41.353,33.647-75,75-75
|
||||
c16.948,0,32.426,5.865,45,15.383V195c0-8.291,6.709-15,15-15c8.291,0,15,6.709,15,15c0,33.36,0,41.625,0,75
|
||||
c0,16.538,13.462,30,30,30c16.538,0,30-13.462,30-30c0-100.391-66.432-150-135-150c-74.443,0-135,60.557-135,135
|
||||
s60.557,135,135,135c30,0,58.374-9.609,82.061-27.803c15.822-12.078,33.94,11.765,18.281,23.789
|
||||
C328.353,408.237,293.665,420,257,420c-90.981,0-165-74.019-165-165S166.019,90,257,90c82.897,0,165,61.135,165,180
|
||||
C422,303.091,395.091,330,362,330z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -1,56 +1,50 @@
|
|||
import Vue from 'vue';
|
||||
import {App as InertiaApp, plugin, Link as ILink} from '@inertiajs/inertia-vue';
|
||||
import {Inertia} from '@inertiajs/inertia';
|
||||
|
||||
import SvgSprite from './components/SvgSprite.js';
|
||||
import VPages from './components/VPages.vue';
|
||||
import VLabel from './components/VLabel.vue';
|
||||
import VBool from './components/VBool.vue';
|
||||
import Box from './components/Box.vue';
|
||||
import Heading from './components/Heading.vue';
|
||||
import IconButton from './components/Ui/IconButton.vue';
|
||||
import ToolbarButton from './components/Ui/ToolbarButton.vue';
|
||||
import PageLayout from './components/Page/Layout.vue';
|
||||
import AppLayout from './layouts/AppLayout.vue';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import hasModule from './mixins/hasModule.js';
|
||||
import hasFlash from './mixins/hasFlash.js';
|
||||
import PortalVue from 'portal-vue';
|
||||
import axios from 'axios';
|
||||
import VueAxios from 'vue-axios';
|
||||
import Toasted from 'vue-toasted';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import {createPinia, PiniaVuePlugin} from 'pinia';
|
||||
import requireModules from './lib/requireModules.js';
|
||||
|
||||
import AppLayout from './layouts/AppLayout.vue';
|
||||
import hasModule from './mixins/hasModule.js';
|
||||
import hasFlash from './mixins/hasFlash.js';
|
||||
|
||||
import '../css/app.css';
|
||||
|
||||
// ---------------------------------- Assets -----------------------------------
|
||||
import.meta.glob(['../img/**']);
|
||||
|
||||
// ---------------------------------- Plugins ----------------------------------
|
||||
Vue.use(plugin);
|
||||
Vue.use(PortalVue);
|
||||
Vue.use(VTooltip);
|
||||
Vue.use(Toasted);
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.use(PiniaVuePlugin);
|
||||
Vue.component('f-text', () => import(/* webpackChunkName: "form" */ './components/FText'));
|
||||
Vue.component('f-switch', () => import(/* webpackChunkName: "form" */ './components/FSwitch'));
|
||||
Vue.component('f-select', () => import(/* webpackChunkName: "form" */ './components/FSelect'));
|
||||
Vue.component('f-textarea', () => import(/* webpackChunkName: "form" */ './components/FTextarea'));
|
||||
Vue.component('SvgSprite', SvgSprite);
|
||||
Vue.component('VPages', VPages);
|
||||
Vue.component('v-bool', VBool);
|
||||
Vue.component('v-label', VLabel);
|
||||
Vue.component('box', Box);
|
||||
Vue.component('heading', Heading);
|
||||
Vue.component('icon-button', IconButton);
|
||||
Vue.component('toolbar-button', ToolbarButton);
|
||||
Vue.component('page-layout', PageLayout);
|
||||
Vue.component('save-button', () => import(/* webpackChunkName: "form" */ './components/SaveButton'));
|
||||
|
||||
Vue.component('SvgSprite', () => import('./components/SvgSprite.js'));
|
||||
Vue.component('ILink', ILink);
|
||||
|
||||
// -------------------------------- Components ---------------------------------
|
||||
requireModules(import.meta.glob('./components/form/*.vue'), Vue, 'f');
|
||||
requireModules(import.meta.glob('./components/ui/*.vue'), Vue, 'ui');
|
||||
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), Vue, 'page');
|
||||
|
||||
// ---------------------------------- mixins -----------------------------------
|
||||
Vue.mixin(hasModule);
|
||||
Vue.mixin(hasFlash);
|
||||
|
||||
// ----------------------------------- init ------------------------------------
|
||||
const el = document.getElementById('app');
|
||||
const pinia = createPinia();
|
||||
|
||||
Vue.mixin(hasModule);
|
||||
Vue.mixin(hasFlash);
|
||||
Vue.component('ILink', ILink);
|
||||
|
||||
Inertia.on('start', (event) => window.dispatchEvent(new Event('inertiaStart')));
|
||||
|
||||
let views = import.meta.glob('./views/**/*.vue');
|
||||
new Vue({
|
||||
pinia,
|
||||
render: (h) =>
|
||||
|
@ -58,7 +52,7 @@ new Vue({
|
|||
props: {
|
||||
initialPage: JSON.parse(el.dataset.page),
|
||||
resolveComponent: async (name) => {
|
||||
var page = (await import(`./views/${name}`)).default;
|
||||
var page = (await views[`./views/${name}.vue`]()).default;
|
||||
|
||||
if (page.layout === undefined) {
|
||||
page.layout = AppLayout;
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
<template>
|
||||
<label class="flex flex-col relative field-checkbox cursor-pointer" :for="id" :class="{[`size-${size}`]: true}">
|
||||
<span
|
||||
v-if="label && inset"
|
||||
class="z-10 absolute top-0 left-0 -mt-2 px-1 ml-3 inset-bg font-semibold text-gray-700"
|
||||
>{{ label }}</span
|
||||
>
|
||||
<div class="relative flex items-start">
|
||||
<input :id="id" type="checkbox" v-model="v" :disabled="disabled" class="invisible absolute" />
|
||||
<span class="display-wrapper flex items-center">
|
||||
<span
|
||||
class="relative cursor-pointer flex flex-none justify-center items-center display"
|
||||
:class="{'bg-terminoto-2': v === true, 'bg-white': v === false}"
|
||||
>
|
||||
<svg-sprite src="check" class="w-4 h-4 check-icon text-white"></svg-sprite>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="label && !inset"
|
||||
class="text-sm leading-tight ml-3 text-gray-700 checkbox-label flex items-center"
|
||||
>
|
||||
<span>
|
||||
<span v-text="label" v-if="!html"></span>
|
||||
<span v-html="label" v-if="html"></span>
|
||||
<span v-show="required" class="font-semibold text-red-700">*</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'items',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
html: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
v: {
|
||||
set(v) {
|
||||
if (this.disabled === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.items === 'boolean') {
|
||||
this.$emit('input', v);
|
||||
return;
|
||||
}
|
||||
|
||||
var a = this.items.filter((i) => i !== this.value);
|
||||
if (v) {
|
||||
a.push(this.value);
|
||||
}
|
||||
|
||||
this.$emit('input', a);
|
||||
},
|
||||
get() {
|
||||
if (typeof this.items === 'boolean') {
|
||||
return this.items;
|
||||
}
|
||||
|
||||
if (typeof this.items === 'undefined') {
|
||||
return this.$emit('input', false);
|
||||
}
|
||||
|
||||
return this.items.indexOf(this.value) !== -1;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if (typeof this.items === 'undefined') {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
:root {
|
||||
--checkbox-width: 30px;
|
||||
--margin: 0.2rem;
|
||||
}
|
||||
|
||||
.field-checkbox {
|
||||
input:checked + span {
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.display-wrapper,
|
||||
.checkbox-label {
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.display {
|
||||
width: var(--checkbox-width);
|
||||
height: var(--checkbox-width);
|
||||
border-radius: 0.3rem;
|
||||
border: solid 2px hsl(60, 1.8%, 10.8%);
|
||||
.check-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
input:checked + .display-wrapper .display .check-icon {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<div class="font-semibold text-gray-300">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
|
@ -1,48 +0,0 @@
|
|||
<template>
|
||||
<div class="h-16 px-6 flex justify-between items-center border-b border-solid border-gray-500">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-1 text-xl font-semibold leading-none text-white" v-html="title"></span>
|
||||
<a
|
||||
v-for="(link, index) in links.filter((link) => link.icon === undefined)"
|
||||
:key="index"
|
||||
@click.prevent="$emit(link.event)"
|
||||
href="#"
|
||||
class="btn label btn-primary-light"
|
||||
>
|
||||
<span v-if="link.label" v-text="link.label"></span>
|
||||
<svg-sprite v-if="link.icon" :src="link.icon"></svg-sprite>
|
||||
</a>
|
||||
<a
|
||||
v-for="(link, index) in links.filter((link) => link.icon !== undefined)"
|
||||
:key="index"
|
||||
:href="link.href"
|
||||
class="btn label icon btn-primary-light ml-1"
|
||||
>
|
||||
<span v-if="link.label" v-text="link.label"></span>
|
||||
<svg-sprite v-if="link.icon" :src="link.icon"></svg-sprite>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex ml-4">
|
||||
<a href="#" @click.prevent="$emit('close')" class="btn label btn-primary-light icon">
|
||||
<svg-sprite class="w-3 h-3" src="close"></svg-sprite>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
links: {
|
||||
default: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
title: {
|
||||
default: function () {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,17 +1,25 @@
|
|||
export default {
|
||||
props: {
|
||||
src: { required: true, type: String }
|
||||
src: {required: true, type: String},
|
||||
},
|
||||
render: function(createElement) {
|
||||
render: function (createElement) {
|
||||
var attr = this.$attrs.class ? this.$attrs.class : '';
|
||||
return createElement('svg', {
|
||||
class: attr + ' fill-current'
|
||||
}, [
|
||||
createElement('use', {
|
||||
'attrs': {
|
||||
'xlink:href': `/img/sprite.svg#${this.$props.src}`
|
||||
}
|
||||
}, '')
|
||||
] );
|
||||
}
|
||||
return createElement(
|
||||
'svg',
|
||||
{
|
||||
class: attr + ' fill-current',
|
||||
},
|
||||
[
|
||||
createElement(
|
||||
'use',
|
||||
{
|
||||
attrs: {
|
||||
'xlink:href': `/sprite.svg#${this.$props.src}`,
|
||||
},
|
||||
},
|
||||
''
|
||||
),
|
||||
]
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<label class="field-wrap" :for="id" :class="`field-wrap-${size}`">
|
||||
<span v-if="label" class="field-label">
|
||||
{{ label }}
|
||||
<span v-show="required" class="text-red-800"> *</span>
|
||||
</span>
|
||||
<div class="relative real-field-wrap" :class="`size-${size}`">
|
||||
<div
|
||||
@click="visible = !visible"
|
||||
class="flex items-center border-gray-600 text-gray-300 leading-none border-solid bg-gray-700 w-full appearance-none outline-none rounded-lg size-sm text-xs px-1 border pr-6"
|
||||
v-text="`${value.length} Einträge ausgewählt`"
|
||||
></div>
|
||||
<div v-show="visible" class="absolute w-[max-content] z-30 max-h-[31rem] overflow-auto shadow-lg bg-gray-600 border border-gray-500 rounded-lg p-2 top-7">
|
||||
<div v-for="(option, index) in parsedOptions" class="flex items-center space-x-2" :key="index">
|
||||
<f-switch :id="`${id}-${index}`" size="sm" :items="value.includes(option.id)" :value="option.id" @input="trigger(option, $event)"></f-switch>
|
||||
<div class="text-sm text-gray-200" v-text="option.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-wrap">
|
||||
<div v-if="hint" v-tooltip="hint">
|
||||
<svg-sprite src="info-button" class="info-button"></svg-sprite>
|
||||
</div>
|
||||
<div class="px-1 relative" v-if="size != 'xs'">
|
||||
<svg-sprite class="chevron w-3 h-3 fill-current" src="chevron-down"></svg-sprite>
|
||||
</div>
|
||||
<div class="px-1 relative" v-if="size == 'xs'">
|
||||
<svg-sprite class="chevron w-2 h-2 fill-current" src="chevron-down"></svg-sprite>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import map from 'lodash/map';
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
id: {},
|
||||
inset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: function () {
|
||||
return 'base';
|
||||
},
|
||||
},
|
||||
emptyLabel: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
value: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: null,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
hint: {},
|
||||
options: {
|
||||
default: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
parsedOptions() {
|
||||
return Array.isArray(this.options)
|
||||
? this.options
|
||||
: map(this.options, (value, key) => {
|
||||
return {name: value, id: key};
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trigger(option, v) {
|
||||
var value = [...this.value];
|
||||
|
||||
this.$emit('input', value.includes(option.id) ? value.filter((cv) => cv !== option.id) : [...value, option.id]);
|
||||
},
|
||||
clear() {
|
||||
this.$emit('input', null);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
.inset-bg {
|
||||
background: linear-gradient(to bottom, hsl(247.5, 66.7%, 97.6%) 0%, hsl(247.5, 66.7%, 97.6%) 41%, hsl(0deg 0% 100%) 41%, hsl(180deg 0% 100%) 100%);
|
||||
}
|
||||
</style>
|
|
@ -8,12 +8,7 @@
|
|||
<select :disabled="disabled" :name="name" :value="value" @change="trigger">
|
||||
<option v-if="placeholder" v-html="placeholder" :value="null"></option>
|
||||
|
||||
<option
|
||||
v-for="option in parsedOptions"
|
||||
:key="option.id"
|
||||
v-html="option.name"
|
||||
:value="option.id"
|
||||
></option>
|
||||
<option v-for="option in parsedOptions" :key="option.id" v-html="option.name" :value="option.id"></option>
|
||||
</select>
|
||||
<div class="info-wrap">
|
||||
<div v-if="hint" v-tooltip="hint">
|
||||
|
@ -95,10 +90,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
trigger(v) {
|
||||
this.$emit(
|
||||
'input',
|
||||
isNaN(parseInt(v.target.value)) ? (v.target.value ? v.target.value : null) : parseInt(v.target.value)
|
||||
);
|
||||
this.$emit('input', /^[0-9]+$/.test(v.target.value) ? parseInt(v.target.value) : v.target.value ? v.target.value : null);
|
||||
},
|
||||
clear() {
|
||||
this.$emit('input', null);
|
||||
|
@ -119,12 +111,6 @@ export default {
|
|||
|
||||
<style scope>
|
||||
.inset-bg {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
hsl(247.5, 66.7%, 97.6%) 0%,
|
||||
hsl(247.5, 66.7%, 97.6%) 41%,
|
||||
hsl(0deg 0% 100%) 41%,
|
||||
hsl(180deg 0% 100%) 100%
|
||||
);
|
||||
background: linear-gradient(to bottom, hsl(247.5, 66.7%, 97.6%) 0%, hsl(247.5, 66.7%, 97.6%) 41%, hsl(0deg 0% 100%) 41%, hsl(180deg 0% 100%) 100%);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<h1 class="text-xl border-b-2 pb-1 mb-4 text-primary-100 text-center border-primary-800"><slot></slot></h1>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="h-16 px-6 flex items-center justify-between border-b border-solid border-gray-600 group-[.is-bright]:border-gray-500">
|
||||
<div class="flex items-center space-x-2">
|
||||
<slot name="before-title"></slot>
|
||||
<page-title>{{ title }}</page-title>
|
||||
<slot name="toolbar"></slot>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 ml-2">
|
||||
<a href="#" v-if="$listeners.close" @click.prevent="$emit('close')" class="btn label btn-primary-light icon">
|
||||
<svg-sprite class="w-3 h-3" src="close"></svg-sprite>
|
||||
</a>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
default: function () {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,16 +1,18 @@
|
|||
<template>
|
||||
<div class="grow bg-gray-900 flex flex-col transition-all" :class="{'ml-56': menuStore.visible, 'ml-0': !menuStore.visible}">
|
||||
<div class="h-16 px-6 flex items-center space-x-3 border-b border-gray-600">
|
||||
<a href="#" @click.prevent="menuStore.toggle()" class="lg:hidden">
|
||||
<svg-sprite src="menu" class="text-gray-100 w-5 h-5"></svg-sprite>
|
||||
</a>
|
||||
<span class="text-sm md:text-xl font-semibold text-white leading-none" v-html="$page.props.title"></span>
|
||||
<slot name="toolbar"></slot>
|
||||
<div class="flex grow justify-between">
|
||||
<portal-target name="toolbar-left"> </portal-target>
|
||||
<page-header :title="$page.props.title">
|
||||
<template #before-title>
|
||||
<a href="#" @click.prevent="menuStore.toggle()" class="mr-2 lg:hidden">
|
||||
<svg-sprite src="menu" class="text-gray-100 w-5 h-5"></svg-sprite>
|
||||
</a>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<slot name="toolbar"></slot>
|
||||
</template>
|
||||
<template #right>
|
||||
<portal-target name="toolbar-right"> </portal-target>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</page-header>
|
||||
|
||||
<div :class="pageClass" class="grow flex flex-col">
|
||||
<slot></slot>
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<span class="text-sm md:text-xl font-semibold leading-none text-white"><slot></slot></span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<i-link :href="href" class="btn label mr-2" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? $slots.default[0].text : ''">
|
||||
<i-link :href="href" v-on="$listeners" class="btn label" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? $slots.default[0].text : ''">
|
||||
<svg-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></svg-sprite>
|
||||
<span class="hidden xl:inline"><slot></slot></span>
|
||||
</i-link>
|
|
@ -1,17 +1,7 @@
|
|||
<template>
|
||||
<div class="flex gap-1 justify-center items-center">
|
||||
<svg-sprite
|
||||
class="flex-none"
|
||||
v-if="member.is_leader"
|
||||
:class="[ageColors.leiter, iconClass]"
|
||||
src="lilie"
|
||||
></svg-sprite>
|
||||
<svg-sprite
|
||||
class="flex-none"
|
||||
v-if="member.age_group_icon"
|
||||
:class="[ageColors[member.age_group_icon], iconClass]"
|
||||
src="lilie"
|
||||
></svg-sprite>
|
||||
<svg-sprite class="flex-none" v-if="member.is_leader" :class="[ageColors.leiter, iconClass]" src="lilie"></svg-sprite>
|
||||
<svg-sprite class="flex-none" v-if="member.age_group_icon" :class="[ageColors[member.age_group_icon], iconClass]" src="lilie"></svg-sprite>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,14 +1,7 @@
|
|||
<template>
|
||||
<div v-tooltip="longLabel" class="flex space-x-2 items-center">
|
||||
<div
|
||||
class="border-2 rounded-full w-4 h-4 flex items-center justify-center"
|
||||
:class="value ? 'border-green-700' : 'border-red-700'"
|
||||
>
|
||||
<svg-sprite
|
||||
:src="value ? 'check' : 'close'"
|
||||
:class="value ? 'text-green-800' : 'text-red-800'"
|
||||
class="w-3 h-3 flex-none"
|
||||
></svg-sprite>
|
||||
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? 'border-green-700' : 'border-red-700'">
|
||||
<svg-sprite :src="value ? 'check' : 'close'" :class="value ? 'text-green-800' : 'text-red-800'" class="w-3 h-3 flex-none"></svg-sprite>
|
||||
</div>
|
||||
<div class="text-gray-400 text-xs" v-text="label"></div>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<section class="p-3 rounded-lg flex flex-col" :class="{'bg-gray-800': second === false, 'bg-gray-700': second === true}">
|
||||
<div class="flex items-center">
|
||||
<heading class="col-span-full" v-if="heading">{{ heading }}</heading>
|
||||
<div class="col-span-full font-semibold text-gray-300" v-if="heading" v-text="heading"></div>
|
||||
<slot name="in-title"></slot>
|
||||
</div>
|
||||
<main :class="{'mt-2': heading, [containerClass]: true}">
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<button v-on="$listeners" class="btn btn-primary relative group">
|
||||
<div :class="{hidden: !isLoading, flex: isLoading}" class="absolute items-center top-0 h-full left-0 ml-2">
|
||||
<ui-spinner class="border-primary-400 w-6 h-6 group-hover:border-primary-200"></ui-spinner>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {menuStore} from '../../stores/menuStore.js';
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div :class="`spin-${type}`">
|
||||
<div v-if="type === 'ring'"></div>
|
||||
<div v-if="type === 'ring'"></div>
|
||||
<div v-if="type === 'ring'"></div>
|
||||
<div v-if="type === 'ring'"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: () => 'ring',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.spin-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.spin-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-top-color: inherit;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
.spin-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.spin-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.spin-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -16,6 +16,7 @@
|
|||
<v-link href="/subscription" v-show="hasModule('bill')" menu="subscription" icon="money">Beiträge</v-link>
|
||||
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
|
||||
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
|
||||
<v-link href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</v-link>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div id="app" class="bg-gray-900 font-sans flex flex-col grow items-center justify-center">
|
||||
<div id="app" class="bg-gray-900 font-sans flex flex-col grow items-center justify-center p-6">
|
||||
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
|
||||
<div class="bg-gray-800 rounded-xl overflow-hidden shadow-lg p-6">
|
||||
<slot></slot>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import {paramCase} from 'change-case';
|
||||
|
||||
export default function (context, Vue, prefix) {
|
||||
for (const file in context) {
|
||||
let componentName = paramCase(`${prefix}${file.replace(/^.*\/(.*?)\.vue$/g, '$1')}`);
|
||||
|
||||
Vue.component(componentName, typeof context[file] === 'function' ? context[file] : context[file].default);
|
||||
}
|
||||
}
|
|
@ -9,10 +9,13 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
filterString() {
|
||||
return btoa(encodeURIComponent(JSON.stringify(this.inner.meta.filter)));
|
||||
return this.toFilterString(this.inner.meta.filter);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toFilterString(data) {
|
||||
return btoa(encodeURIComponent(JSON.stringify(data)));
|
||||
},
|
||||
reload(resetPage = true) {
|
||||
var _self = this;
|
||||
var data = {
|
||||
|
|
|
@ -1,63 +1,57 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="step === 0">
|
||||
<page-full-heading>Willkommen im Adrema-Setup.<br /></page-full-heading>
|
||||
<div class="prose prose-invert">
|
||||
<p>Willkommen im Adrema-Setup.<br /></p>
|
||||
<p>
|
||||
Bitte gib deine NaMi-Zugangsdaten ein,<br />
|
||||
um eine erste Synchronisation durchzuführen.
|
||||
</p>
|
||||
<p>Bitte gib deine NaMi-Zugangsdaten ein,<br />um eine erste Synchronisation durchzuführen.</p>
|
||||
</div>
|
||||
<form @submit.prevent="check" class="grid gap-3 mt-5">
|
||||
<f-text v-model="values.mglnr" label="Mitgliedsnummer" name="mglnr" id="mglnr" type="tel" required></f-text>
|
||||
<f-text v-model="values.password" type="password" label="Passwort" name="password" id="password" required></f-text>
|
||||
<button type="submit" class="btn w-full btn-primary mt-6 inline-block">Weiter</button>
|
||||
<ui-button class="mt-6" :is-loading="loading" type="submit">Weiter</ui-button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="step === 1" class="flex flex-col" style="width: 90vw; height: 90vh">
|
||||
<form @submit.prevent="storeSearch" class="border-2 border-primary-700 border-solid p-3 rounded-lg grid grid-cols-3 gap-3 flex-none">
|
||||
<div v-if="step === 1" class="grid grid-cols-5 w-full gap-3">
|
||||
<page-full-heading class="col-span-full !mb-0">Suchkriterien festlegen</page-full-heading>
|
||||
<form @submit.prevent="storeSearch" class="border-2 border-primary-800 border-solid p-3 rounded-lg grid gap-3 col-span-2">
|
||||
<div class="prose prose-invert max-w-none col-span-full">
|
||||
<p>
|
||||
Lege hier die Suchkriterien für den Abruf der Mitglieder-Daten fest. Mit diesen Suchkriterien wird im Anschluss eine Mitgliedersuche in NaMi durchgeführt. Alle Mitglieder, die
|
||||
dann dort auftauchen werden in die Adrema übernommen. Dir wird hier eine Vorschau eingeblendet, damit du sicherstellen kannst, dass die Suchkriterien die richtigen sind.
|
||||
</p>
|
||||
<p>
|
||||
Außerdem werden diese Suchkriterien bei jedem neuen Abgleich (der automatisch täglich erfolgt) angewendet. Du kannst die Suchkriterien in den globalen Einstellungen jederzeit
|
||||
ändern.
|
||||
</p>
|
||||
</div>
|
||||
<f-text
|
||||
<f-select
|
||||
v-model="values.params.gruppierung1Id"
|
||||
label="Diözesan-Gruppierung"
|
||||
name="gruppierung1Id"
|
||||
id="gruppierung1Id"
|
||||
type="tel"
|
||||
size="sm"
|
||||
@input="search"
|
||||
:options="searchLayerOptions[0]"
|
||||
@input="loadSearchLayer(1, $event, search)"
|
||||
hint="Gruppierungs-Nummer einer Diözese, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deiner Diözese. Entspricht dem Feld '1. Ebene' in der NaMi Suche."
|
||||
></f-text>
|
||||
<f-text
|
||||
></f-select>
|
||||
<f-select
|
||||
v-model="values.params.gruppierung2Id"
|
||||
label="Bezirks-Gruppierung"
|
||||
name="gruppierung2Id"
|
||||
id="gruppierung2Id"
|
||||
type="tel"
|
||||
hint="Gruppierungs-Nummer eines Bezirks, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Bezirks. Entspricht dem Feld '2. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Bezirk zu begrenzen."
|
||||
:disabled="!values.params.gruppierung1Id"
|
||||
@input="search"
|
||||
@input="loadSearchLayer(2, $event, search)"
|
||||
size="sm"
|
||||
></f-text>
|
||||
<f-text
|
||||
:options="searchLayerOptions[1]"
|
||||
></f-select>
|
||||
<f-select
|
||||
v-model="values.params.gruppierung3Id"
|
||||
label="Stammes-Gruppierung"
|
||||
name="gruppierung3Id"
|
||||
id="gruppierung3Id"
|
||||
type="tel"
|
||||
size="sm"
|
||||
@input="search"
|
||||
hint="Gruppierungs-Nummer deines Stammes, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Stammes. Entspricht dem Feld '3. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Stamm zu beschränken."
|
||||
:disabled="!values.params.gruppierung1Id || !values.params.gruppierung2Id"
|
||||
></f-text>
|
||||
:options="searchLayerOptions[2]"
|
||||
></f-select>
|
||||
<f-select
|
||||
v-model="values.params.mglStatusId"
|
||||
label="Mitglieds-Status"
|
||||
|
@ -86,12 +80,12 @@
|
|||
hint="Mitglieder finden, die direktes Mitglied in einer Untergruppe der kleinsten befüllten Gruppierung sind."
|
||||
size="sm"
|
||||
></f-switch>
|
||||
<div class="col-span-full">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Weiter</button>
|
||||
<div class="col-span-full flex justify-center">
|
||||
<ui-button :is-loading="loading" class="px-10" type="submit">Weiter</ui-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="grow border-2 border-primary-700 border-solid p-3 rounded-lg mt-4" v-if="preview !== null && preview.data.length">
|
||||
<section class="col-span-3 text-sm col-span-3" v-if="preview !== null && preview.data.length">
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
|
||||
<thead>
|
||||
<th>GruppierungsNr</th>
|
||||
|
@ -111,12 +105,13 @@
|
|||
</table>
|
||||
|
||||
<div v-if="preview !== null" class="px-6">
|
||||
<v-pages class="mt-4" :value="preview" @reload="reloadPage"></v-pages>
|
||||
<ui-pagination class="mt-4" :value="preview" @reload="reloadPage"></ui-pagination>
|
||||
</div>
|
||||
</section>
|
||||
<section class="grow items-center justify-center flex text-xl text-gray-200 border-2 border-primary-700 border-solid p-3 rounded-lg mt-4" v-else>Keine Mitglieder gefunden</section>
|
||||
<section class="col-span-3 items-center justify-center flex text-xl text-gray-200 border-2 border-primary-800 border-solid p-3 rounded-lg mt-4" v-else>Keine Mitglieder gefunden</section>
|
||||
</div>
|
||||
<div v-if="step === 2">
|
||||
<page-full-heading>Standard-Gruppierung</page-full-heading>
|
||||
<div class="prose prose-invert">
|
||||
<p>Bitte gib hier deine Standard-Gruppierungsnummer ein.</p>
|
||||
<p>Dieser Gruppierung werden Mitglieder automatisch zugeordnet,<br />falls nichts anderes angegeben wurde.</p>
|
||||
|
@ -128,6 +123,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<div v-if="step === 3">
|
||||
<page-full-heading>Einrichtung abgeschlossen</page-full-heading>
|
||||
<div class="prose prose-invert">
|
||||
<p>Wir werden nun die Mitgliederdaten anhand deiner festgelegten Kriterien abrufen.</p>
|
||||
<p>Per Klick auf "Abschließen" gelangst du zum Dashboard</p>
|
||||
|
@ -150,6 +146,8 @@ export default {
|
|||
|
||||
data: function () {
|
||||
return {
|
||||
searchLayerOptions: [[], [], []],
|
||||
loading: false,
|
||||
preview: null,
|
||||
states: [
|
||||
{id: 'INAKTIV', name: 'Inaktiv'},
|
||||
|
@ -188,23 +186,57 @@ export default {
|
|||
await this.loadSearchResult(page);
|
||||
},
|
||||
async check() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.axios.post('/nami/login-check', this.values);
|
||||
this.step = 1;
|
||||
await this.loadSearchResult(1);
|
||||
await this.loadSearchLayer(0, null, () => '');
|
||||
this.step = 1;
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
search: debounce(async function () {
|
||||
await this.loadSearchResult(1);
|
||||
}, 500),
|
||||
async loadSearchLayer(parentLayer, parent, after) {
|
||||
this.loading = true;
|
||||
try {
|
||||
var result = await this.axios.post('/nami/get-search-layer', {...this.values, layer: parentLayer, parent});
|
||||
|
||||
this.searchLayerOptions = this.searchLayerOptions.map((layers, index) => {
|
||||
if (index < parentLayer) {
|
||||
return layers;
|
||||
}
|
||||
|
||||
var groupIndex = index + 1;
|
||||
this.values.params[`gruppierung${groupIndex}Id`] = null;
|
||||
|
||||
if (index === parentLayer) {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
after();
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadSearchResult(page) {
|
||||
this.loading = true;
|
||||
try {
|
||||
var result = await this.axios.post('/nami/search', {...this.values, page: page});
|
||||
this.preview = result.data;
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<form @submit.prevent="submit">
|
||||
<div class="h-24 p-6 md:px-10 bg-primary-800 flex justify-between items-center w-full">
|
||||
<span class="text-primary-500 text-xl">Login</span>
|
||||
<img src="/img/dpsg.gif" class="w-24" />
|
||||
<img src="../../img/dpsg.gif" class="w-24" />
|
||||
</div>
|
||||
<div class="p-6 md:p-10 grid gap-5">
|
||||
<f-text id="email" label="E-Mail-Adresse" v-model="values.email"></f-text>
|
||||
|
@ -13,7 +13,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import FullLayout from '../layouts/FullLayout';
|
||||
import FullLayout from '../layouts/FullLayout.vue';
|
||||
|
||||
export default {
|
||||
layout: FullLayout,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue