Compare commits
33 Commits
62fb792e65
...
d38989f302
Author | SHA1 | Date |
---|---|---|
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 |
|
@ -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', ''))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,17 @@ 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 array $activityIds = [],
|
||||
public array $subactivityIds = [],
|
||||
public string $search = '',
|
||||
public ?int $groupId = null,
|
||||
public ?string $search = '',
|
||||
public array $groupIds = [],
|
||||
public array $additional = [],
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -45,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 (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);
|
||||
}
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 |
|
@ -10,7 +10,7 @@
|
|||
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 shadow-lg bg-gray-600 border border-gray-500 rounded-lg p-2 top-7">
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,9 +1,9 @@
|
|||
<template>
|
||||
<button class="btn btn-primary relative group">
|
||||
<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>
|
||||
Weiter
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -83,7 +83,11 @@ export default {
|
|||
set: debounce(async function (event) {
|
||||
this.search.s = event;
|
||||
|
||||
var response = await this.axios.post('/api/member/search', {search: event, minLength: 3});
|
||||
var response = await this.axios.post('/api/member/search', {
|
||||
filter: {
|
||||
search: event,
|
||||
},
|
||||
});
|
||||
|
||||
this.search.results = response.data.data;
|
||||
}, 300),
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<template #toolbar>
|
||||
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">Zurück</page-toolbar-button>
|
||||
</template>
|
||||
<form id="form" class="p-3 grid gap-3" @submit.prevent="submit">
|
||||
<f-save-button form="form"></f-save-button>
|
||||
<ui-box heading="Metadatem">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<f-text id="name" name="name" v-model="model.name" label="Name" size="sm" required></f-text>
|
||||
<f-select id="gateway_id" name="gateway_id" :options="meta.gateways" v-model="model.gateway_id" label="Verbindung" size="sm" required></f-select>
|
||||
</div>
|
||||
</ui-box>
|
||||
<ui-box heading="Filterregeln" v-if="members !== null">
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<f-multipleselect
|
||||
id="activity_ids"
|
||||
name="activity_ids"
|
||||
:options="members.meta.filterActivities"
|
||||
v-model="model.filter.activity_ids"
|
||||
@input="reload(1)"
|
||||
label="Tätigkeit"
|
||||
size="sm"
|
||||
></f-multipleselect>
|
||||
<f-multipleselect
|
||||
id="subactivity_ids"
|
||||
name="subactivity_ids"
|
||||
:options="members.meta.filterSubactivities"
|
||||
v-model="model.filter.subactivity_ids"
|
||||
@input="reload(1)"
|
||||
label="Unterttätigkeit"
|
||||
size="sm"
|
||||
></f-multipleselect>
|
||||
<f-multipleselect
|
||||
id="additional"
|
||||
name="additional"
|
||||
:options="members.meta.members"
|
||||
v-model="model.filter.additional"
|
||||
@input="reload(1)"
|
||||
label="Zusätzliche Mitglieder"
|
||||
size="sm"
|
||||
></f-multipleselect>
|
||||
<f-multipleselect
|
||||
id="groupIds"
|
||||
name="groupIds"
|
||||
:options="members.meta.groups"
|
||||
v-model="model.filter.group_ids"
|
||||
@input="reload(1)"
|
||||
label="Gruppierungen"
|
||||
size="sm"
|
||||
></f-multipleselect>
|
||||
</div>
|
||||
</ui-box>
|
||||
<ui-box heading="Mitglieder" v-if="members !== null">
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Nachname</th>
|
||||
<th>Vorname</th>
|
||||
<th>E-Mail-Adresse</th>
|
||||
<th>E-Mail-Adresse Eltern</th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(member, index) in members.data" :key="index">
|
||||
<td><ui-age-groups :member="member"></ui-age-groups></td>
|
||||
<td v-text="member.lastname"></td>
|
||||
<td v-text="member.firstname"></td>
|
||||
<td v-text="member.email"></td>
|
||||
<td v-text="member.email_parents"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<ui-pagination class="mt-4" @reload="reload" :value="members.meta" :only="['data']"></ui-pagination>
|
||||
</ui-box>
|
||||
</form>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import indexHelpers from '../../mixins/indexHelpers.js';
|
||||
import hasFlash from '../../mixins/hasFlash.js';
|
||||
|
||||
export default {
|
||||
mixins: [indexHelpers, hasFlash],
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
model: this.data === undefined ? {...this.meta.default_model} : {...this.data},
|
||||
members: null,
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
data: {},
|
||||
meta: {},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async reload(page) {
|
||||
this.members = (
|
||||
await this.axios.post('/api/member/search', {
|
||||
page: page || 1,
|
||||
filter: this.toFilterString(this.model.filter),
|
||||
})
|
||||
).data;
|
||||
},
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
this.model.id ? await this.axios.patch(this.model.links.update, this.model) : await this.axios.post('/maildispatcher', this.model);
|
||||
this.$inertia.visit(this.meta.links.index);
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.reload();
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<template #toolbar>
|
||||
<page-toolbar-button :href="data.meta.links.create" color="primary" icon="plus">Verteiler erstellen</page-toolbar-button>
|
||||
</template>
|
||||
<ui-popup heading="Verteiler löschen?" v-if="deleting !== null" @close="deleting.reject()">
|
||||
<div>
|
||||
<p class="mt-4">Den Verteiler "{{ deleting.dispatcher.name }}" löschen?</p>
|
||||
<div class="grid grid-cols-2 gap-3 mt-6">
|
||||
<a href="#" @click.prevent="deleting.resolve()" class="text-center btn btn-danger">Löschen</a>
|
||||
<a href="#" @click.prevent="deleting.reject()" class="text-center btn btn-primary">Abbrechen</a>
|
||||
</div>
|
||||
</div>
|
||||
</ui-popup>
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Domain</th>
|
||||
<th>Verbindung</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(dispatcher, index) in data.data" :key="index">
|
||||
<td>
|
||||
<div v-text="dispatcher.name"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="dispatcher.gateway.domain"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="dispatcher.gateway.name"></div>
|
||||
</td>
|
||||
<td>
|
||||
<i-link :href="dispatcher.links.edit" class="mr-1 inline-flex btn btn-warning btn-sm"><svg-sprite src="pencil"></svg-sprite></i-link>
|
||||
<i-link @click.prevent="remove(dispatcher)" class="inline-flex btn btn-danger btn-sm"><svg-sprite src="trash"></svg-sprite></i-link>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
deleting: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async remove(dispatcher) {
|
||||
new Promise((resolve, reject) => {
|
||||
this.deleting = {resolve, reject, dispatcher};
|
||||
})
|
||||
.then(() => {
|
||||
this.$inertia.delete(dispatcher.links.delete);
|
||||
this.deleting = null;
|
||||
})
|
||||
.catch(() => {
|
||||
this.deleting = null;
|
||||
});
|
||||
},
|
||||
},
|
||||
props: {
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<template #toolbar>
|
||||
<page-toolbar-button @click.prevent="model = {...data.meta.default}" color="primary" icon="plus">Neue Verbindung</page-toolbar-button>
|
||||
</template>
|
||||
<ui-popup :heading="model.id ? 'Verbindung bearbeiten' : 'Neue Verbindung'" v-if="model !== null" @close="model = null">
|
||||
<form @submit.prevent="submit">
|
||||
<section class="grid grid-cols-2 gap-3 mt-6">
|
||||
<f-text v-model="model.name" name="name" id="name" label="Bezeichnung" required></f-text>
|
||||
<f-text v-model="model.domain" name="domain" id="domain" label="Domain" required></f-text>
|
||||
<f-select
|
||||
:value="model.type.cls"
|
||||
@input="
|
||||
model.type = {
|
||||
cls: $event,
|
||||
params: {...getType($event).defaults},
|
||||
}
|
||||
"
|
||||
label="Typ"
|
||||
name="type"
|
||||
id="type"
|
||||
:options="data.meta.types"
|
||||
:placeholder="''"
|
||||
required
|
||||
></f-select>
|
||||
<template v-for="(field, index) in getType(model.type.cls).fields">
|
||||
<f-text
|
||||
:key="index"
|
||||
v-if="field.type === 'text' || field.type === 'password'"
|
||||
:label="field.label"
|
||||
:type="field.type"
|
||||
:name="field.name"
|
||||
:id="field.name"
|
||||
v-model="model.type.params[field.name]"
|
||||
:required="field.is_required"
|
||||
></f-text>
|
||||
</template>
|
||||
</section>
|
||||
<section class="flex mt-4 space-x-2">
|
||||
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
|
||||
<ui-button @click.prevent="model = null" class="btn-primary">Abbrechen</ui-button>
|
||||
</section>
|
||||
</form>
|
||||
</ui-popup>
|
||||
<setting-layout>
|
||||
<div class="w-full h-full pb-6">
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
|
||||
<thead>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Domain</th>
|
||||
<th>Typ</th>
|
||||
<th>Prüfung</th>
|
||||
<th>Aktion</th>
|
||||
</thead>
|
||||
|
||||
<tr v-for="(gateway, index) in inner.data" :key="index">
|
||||
<td v-text="gateway.name"></td>
|
||||
<td v-text="gateway.domain"></td>
|
||||
<td v-text="gateway.type_human"></td>
|
||||
<td>
|
||||
<ui-boolean-display
|
||||
:value="gateway.works"
|
||||
long-label="Verbindungsstatus"
|
||||
:label="gateway.works ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'"
|
||||
></ui-boolean-display>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" v-tooltip="`Bearbeiten`" @click.prevent="model = {...gateway}" class="inline-flex btn btn-warning btn-sm"><svg-sprite src="pencil"></svg-sprite></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="px-6">
|
||||
<ui-pagination class="mt-4" :value="data.meta" :only="['data']"></ui-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</setting-layout>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingLayout from '../setting/Layout.vue';
|
||||
import indexHelpers from '../../mixins/indexHelpers.js';
|
||||
|
||||
export default {
|
||||
mixins: [indexHelpers],
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
model: null,
|
||||
inner: {...this.data},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
data: {},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getType(type) {
|
||||
return this.data.meta.types.find((t) => t.id === type);
|
||||
},
|
||||
async submit() {
|
||||
try {
|
||||
await this.axios[this.model.id ? 'patch' : 'post'](this.model.id ? this.model.links.update : this.data.meta.links.store, this.model);
|
||||
|
||||
this.reload();
|
||||
this.model = null;
|
||||
} catch (e) {
|
||||
this.errorsFromException(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
SettingLayout,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -22,7 +22,15 @@
|
|||
<div class="px-6 py-2 flex border-b border-gray-600 items-center space-x-3">
|
||||
<f-text :value="getFilter('search')" @input="setFilter('search', $event)" id="search" name="search" label="Suchen …" size="sm"></f-text>
|
||||
<f-switch v-show="hasModule('bill')" id="ausstand" @input="setFilter('ausstand', $event)" :items="getFilter('ausstand')" label="Nur Ausstände" size="sm"></f-switch>
|
||||
<f-select id="group_id" @input="setFilter('group_id', $event)" :options="data.meta.groups" :value="getFilter('group_id')" label="Gruppierung" size="sm" name="group_id"></f-select>
|
||||
<f-multipleselect
|
||||
id="group_ids"
|
||||
@input="setFilter('group_ids', $event)"
|
||||
:options="data.meta.groups"
|
||||
:value="getFilter('group_ids')"
|
||||
label="Gruppierungen"
|
||||
size="sm"
|
||||
name="group_ids"
|
||||
></f-multipleselect>
|
||||
<f-select
|
||||
v-show="hasModule('bill')"
|
||||
name="billKinds"
|
||||
|
@ -71,7 +79,7 @@
|
|||
</thead>
|
||||
|
||||
<tr v-for="(member, index) in inner.data" :key="index">
|
||||
<td><age-groups :member="member"></age-groups></td>
|
||||
<td><ui-age-groups :member="member"></ui-age-groups></td>
|
||||
<td v-text="member.lastname"></td>
|
||||
<td v-text="member.firstname"></td>
|
||||
<td class="hidden 2xl:table-cell" v-text="member.full_address"></td>
|
||||
|
@ -92,7 +100,7 @@
|
|||
<div class="md:hidden p-3 grid gap-3">
|
||||
<ui-box class="relative" :heading="member.fullname" v-for="(member, index) in data.data" :key="index">
|
||||
<div slot="in-title">
|
||||
<age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></age-groups>
|
||||
<ui-age-groups class="ml-2" :member="member" icon-class="w-4 h-4"></ui-age-groups>
|
||||
</div>
|
||||
<div class="text-xs text-gray-200" v-text="member.full_address"></div>
|
||||
<div class="flex items-center mt-1 space-x-4">
|
||||
|
@ -153,9 +161,8 @@ export default {
|
|||
MemberMemberships,
|
||||
MemberPayments,
|
||||
MemberCourses,
|
||||
'age-groups': () => import('./AgeGroups.vue'),
|
||||
'tags': () => import('./Tags.vue'),
|
||||
'actions': () => import('./index/Actions.vue'),
|
||||
tags: () => import('./Tags.vue'),
|
||||
actions: () => import('./index/Actions.vue'),
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
<template>
|
||||
<form id="billsettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
|
||||
<f-save-button form="billsettingform"></f-save-button>
|
||||
<f-text label="Absender" hint="Absender-Name in Kurzform, i.d.R. der kurze Stammesname" name="from" id="from" v-model="inner.from"></f-text>
|
||||
<f-text label="Absender (lang)" v-model="inner.from_long" name="from_long" id="from_long" hint="Absender-Name in Langform, i.d.R. der Stammesname"></f-text>
|
||||
<h2 class="text-lg font-semibold text-gray-300 col-span-2 mt-5">Kontaktdaten</h2>
|
||||
<div class="col-span-2 text-gray-300 text-sm">Diese Kontaktdaten stehen im Absender-Bereich auf der Rechnung.</div>
|
||||
<f-text label="Straße" v-model="inner.address" name="address" id="address"></f-text>
|
||||
<f-text label="PLZ" v-model="inner.zip" name="zip" id="zip"></f-text>
|
||||
<f-text label="Ort" v-model="inner.place" name="place" id="place"></f-text>
|
||||
<f-text label="E-Mail-Adresse" v-model="inner.email" name="email" id="email"></f-text>
|
||||
<f-text label="Telefonnummer" v-model="inner.mobile" name="mobile" id="mobile"></f-text>
|
||||
<f-text label="Webseite" v-model="inner.website" name="website" id="website"></f-text>
|
||||
<f-text label="IBAN" v-model="inner.iban" name="iban" id="iban"></f-text>
|
||||
<f-text label="BIC" v-model="inner.bic" name="bic" id="bic"></f-text>
|
||||
</form>
|
||||
<page-layout>
|
||||
<setting-layout>
|
||||
<form id="billsettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
|
||||
<f-save-button form="billsettingform"></f-save-button>
|
||||
<f-text label="Absender" hint="Absender-Name in Kurzform, i.d.R. der kurze Stammesname" name="from" id="from" v-model="inner.from"></f-text>
|
||||
<f-text label="Absender (lang)" v-model="inner.from_long" name="from_long" id="from_long" hint="Absender-Name in Langform, i.d.R. der Stammesname"></f-text>
|
||||
<h2 class="text-lg font-semibold text-gray-300 col-span-2 mt-5">Kontaktdaten</h2>
|
||||
<div class="col-span-2 text-gray-300 text-sm">Diese Kontaktdaten stehen im Absender-Bereich auf der Rechnung.</div>
|
||||
<f-text label="Straße" v-model="inner.address" name="address" id="address"></f-text>
|
||||
<f-text label="PLZ" v-model="inner.zip" name="zip" id="zip"></f-text>
|
||||
<f-text label="Ort" v-model="inner.place" name="place" id="place"></f-text>
|
||||
<f-text label="E-Mail-Adresse" v-model="inner.email" name="email" id="email"></f-text>
|
||||
<f-text label="Telefonnummer" v-model="inner.mobile" name="mobile" id="mobile"></f-text>
|
||||
<f-text label="Webseite" v-model="inner.website" name="website" id="website"></f-text>
|
||||
<f-text label="IBAN" v-model="inner.iban" name="iban" id="iban"></f-text>
|
||||
<f-text label="BIC" v-model="inner.bic" name="bic" id="bic"></f-text>
|
||||
</form>
|
||||
</setting-layout>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppLayout from '../../layouts/AppLayout.vue';
|
||||
import SettingLayout from './Layout.vue';
|
||||
|
||||
export default {
|
||||
layout: [AppLayout, SettingLayout],
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
inner: {},
|
||||
inner: {...this.data},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
@ -36,8 +37,8 @@ export default {
|
|||
this.$inertia.post('/setting/bill', this.inner);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.inner = this.data;
|
||||
components: {
|
||||
SettingLayout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
<template>
|
||||
<page-layout>
|
||||
<div class="flex grow relative">
|
||||
<ui-tabs v-model="active" :entries="$page.props.setting_menu"></ui-tabs>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</page-layout>
|
||||
<div class="flex grow relative">
|
||||
<ui-tabs v-model="active" :entries="$page.props.setting_menu"></ui-tabs>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
innerActive: 0,
|
||||
innerActive: this.$page.props.setting_menu.findIndex((menu) => menu.is_active),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -19,19 +17,15 @@ export default {
|
|||
get() {
|
||||
return this.innerActive;
|
||||
},
|
||||
set(v, old) {
|
||||
set(v) {
|
||||
var _self = this;
|
||||
this.$inertia.visit(this.$page.props.setting_menu[v].url, {
|
||||
onSuccess(page) {
|
||||
console.log('A');
|
||||
onSuccess() {
|
||||
_self.innerActive = v;
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.innerActive = this.$page.props.setting_menu.findIndex((menu) => menu.is_active);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
<template>
|
||||
<form id="mailmansettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
|
||||
<f-save-button form="mailmansettingform"></f-save-button>
|
||||
<div class="col-span-full text-gray-100 mb-3">
|
||||
<p class="text-sm">
|
||||
Scoutrobot kann automatisch Mailinglisten erstellen, wenn es mit einem existierenden
|
||||
<a href="https://docs.mailman3.org/en/latest/">Mailman Server</a> verbunden wird. Mailman ist ein OpenSource-Mailinglisten-System, um E-Mails an mehrere Leute zu senden.
|
||||
</p>
|
||||
<p class="text-sm mt-1">Scoutrobot wird nach der Ersteinrichtung deine Mitglieder zu bestehenden E-Mail-Verteilern hinzufügen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<f-switch id="is_active" v-model="inner.is_active" label="Mailman-Synchronisation aktiv"></f-switch>
|
||||
</div>
|
||||
<div class="flex h-full items-center">
|
||||
<svg-sprite :src="stateDisplay.icon" :class="stateDisplay.text" class="w-5 h-5"></svg-sprite>
|
||||
<span class="ml-3" :class="stateDisplay.text" v-text="stateDisplay.label"></span>
|
||||
</div>
|
||||
<f-text label="URL" hint="URL der Mailman Api" name="base_url" id="base_url" v-model="inner.base_url"></f-text>
|
||||
<f-text label="Benutzername" name="username" id="username" v-model="inner.username"></f-text>
|
||||
<f-text type="password" label="Passwort" name="password" id="password" v-model="inner.password"></f-text>
|
||||
<f-select label="Liste für alle Mitglieder" name="all_list" id="all_list" v-model="inner.all_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für Eltern" name="all_parents_list" id="all_parents_list" v-model="inner.all_parents_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für aktive Leiter" name="active_leaders_list" id="active_leaders_list" v-model="inner.active_leaders_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für passive Leiter" name="passive_leaders_list" id="passive_leaders_list" v-model="inner.passive_leaders_list" :options="lists"></f-select>
|
||||
<div></div>
|
||||
</form>
|
||||
<page-layout>
|
||||
<setting-layout>
|
||||
<form id="mailmansettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
|
||||
<f-save-button form="mailmansettingform"></f-save-button>
|
||||
<div class="col-span-full text-gray-100 mb-3">
|
||||
<p class="text-sm">
|
||||
Scoutrobot kann automatisch Mailinglisten erstellen, wenn es mit einem existierenden
|
||||
<a href="https://docs.mailman3.org/en/latest/">Mailman Server</a> verbunden wird. Mailman ist ein OpenSource-Mailinglisten-System, um E-Mails an mehrere Leute zu senden.
|
||||
</p>
|
||||
<p class="text-sm mt-1">Scoutrobot wird nach der Ersteinrichtung deine Mitglieder zu bestehenden E-Mail-Verteilern hinzufügen.</p>
|
||||
</div>
|
||||
<div>
|
||||
<f-switch id="is_active" v-model="inner.is_active" label="Mailman-Synchronisation aktiv"></f-switch>
|
||||
</div>
|
||||
<div class="flex h-full items-center">
|
||||
<svg-sprite :src="stateDisplay.icon" :class="stateDisplay.text" class="w-5 h-5"></svg-sprite>
|
||||
<span class="ml-3" :class="stateDisplay.text" v-text="stateDisplay.label"></span>
|
||||
</div>
|
||||
<f-text label="URL" hint="URL der Mailman Api" name="base_url" id="base_url" v-model="inner.base_url"></f-text>
|
||||
<f-text label="Benutzername" name="username" id="username" v-model="inner.username"></f-text>
|
||||
<f-text type="password" label="Passwort" name="password" id="password" v-model="inner.password"></f-text>
|
||||
<f-select label="Liste für alle Mitglieder" name="all_list" id="all_list" v-model="inner.all_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für Eltern" name="all_parents_list" id="all_parents_list" v-model="inner.all_parents_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für aktive Leiter" name="active_leaders_list" id="active_leaders_list" v-model="inner.active_leaders_list" :options="lists"></f-select>
|
||||
<f-select label="Liste für passive Leiter" name="passive_leaders_list" id="passive_leaders_list" v-model="inner.passive_leaders_list" :options="lists"></f-select>
|
||||
<div></div>
|
||||
</form>
|
||||
</setting-layout>
|
||||
</page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppLayout from '../../layouts/AppLayout.vue';
|
||||
import SettingLayout from './Layout.vue';
|
||||
|
||||
export default {
|
||||
layout: [AppLayout, SettingLayout],
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
inner: {},
|
||||
inner: {...this.data},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
@ -78,8 +79,8 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.inner = this.data;
|
||||
components: {
|
||||
SettingLayout,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -20,6 +20,14 @@ use App\Initialize\Actions\InitializeFormAction;
|
|||
use App\Initialize\Actions\NamiGetSearchLayerAction;
|
||||
use App\Initialize\Actions\NamiLoginCheckAction;
|
||||
use App\Initialize\Actions\NamiSearchAction;
|
||||
use App\Maildispatcher\Actions\CreateAction;
|
||||
use App\Maildispatcher\Actions\DestroyAction;
|
||||
use App\Maildispatcher\Actions\EditAction;
|
||||
use App\Maildispatcher\Actions\IndexAction;
|
||||
use App\Maildispatcher\Actions\StoreAction as MaildispatcherStoreAction;
|
||||
use App\Maildispatcher\Actions\UpdateAction as MaildispatcherUpdateAction;
|
||||
use App\Mailgateway\Actions\StoreAction;
|
||||
use App\Mailgateway\Actions\UpdateAction;
|
||||
use App\Member\Actions\ExportAction;
|
||||
use App\Member\Actions\MemberResyncAction;
|
||||
use App\Member\Actions\MemberShowAction;
|
||||
|
@ -78,4 +86,14 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::get('/contribution', ContributionFormAction::class)->name('contribution.form');
|
||||
Route::get('/contribution-generate', ContributionGenerateAction::class)->name('contribution.generate');
|
||||
Route::post('/contribution-validate', ContributionValidateAction::class)->name('contribution.validate');
|
||||
|
||||
// ----------------------------------- mail ------------------------------------
|
||||
Route::post('/api/mailgateway', StoreAction::class)->name('mailgateway.store');
|
||||
Route::patch('/api/mailgateway/{mailgateway}', UpdateAction::class)->name('mailgateway.update');
|
||||
Route::get('/maildispatcher', IndexAction::class)->name('maildispatcher.index');
|
||||
Route::get('/maildispatcher/create', CreateAction::class)->name('maildispatcher.create');
|
||||
Route::get('/maildispatcher/{maildispatcher}', EditAction::class)->name('maildispatcher.edit');
|
||||
Route::patch('/maildispatcher/{maildispatcher}', MaildispatcherUpdateAction::class)->name('maildispatcher.update');
|
||||
Route::post('/maildispatcher', MaildispatcherStoreAction::class)->name('maildispatcher.store');
|
||||
Route::delete('/maildispatcher/{maildispatcher}', DestroyAction::class)->name('maildispatcher.destroy');
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Bill;
|
||||
namespace Tests\Feature\Invoice;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Maildispatcher;
|
||||
|
||||
use App\Activity;
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Member\Member;
|
||||
use App\Member\Membership;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->login()->loginNami();
|
||||
}
|
||||
|
||||
public function testItCanStoreAMail(): void
|
||||
{
|
||||
$gateway = Mailgateway::factory()->type(LocalType::class, [])->domain('example.com')->create();
|
||||
Member::factory()->defaults()->create();
|
||||
Member::factory()->defaults()->has(Membership::factory()->inLocal('Leiter*in', 'Wölfling'))->create(['email' => 'jane@example.com']);
|
||||
$activityId = Activity::first()->id;
|
||||
|
||||
$response = $this->postJson('/maildispatcher', [
|
||||
'name' => 'test',
|
||||
'gateway_id' => $gateway->id,
|
||||
'filter' => ['activity_ids' => [$activityId]],
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas('maildispatchers', [
|
||||
'name' => 'test',
|
||||
'gateway_id' => $gateway->id,
|
||||
'filter' => "{\"activity_ids\":[{$activityId}]}",
|
||||
]);
|
||||
$dispatcher = Maildispatcher::first();
|
||||
$this->assertDatabaseCount('localmaildispatchers', 1);
|
||||
$this->assertDatabaseHas('localmaildispatchers', [
|
||||
'from' => 'test@example.com',
|
||||
'to' => 'jane@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Mailgateway;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use App\Mailman\Support\MailmanService;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Phake;
|
||||
use Tests\RequestFactories\MailmanTypeRequest;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IndexTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->login()->loginNami();
|
||||
}
|
||||
|
||||
public function testItCanViewIndexPage(): void
|
||||
{
|
||||
$response = $this->get('/setting/mailgateway');
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function testItDisplaysLocalGateways(): void
|
||||
{
|
||||
$this->withoutExceptionHandling();
|
||||
$mailgateway = Mailgateway::factory()->type(LocalType::class, [])->name('Lore')->domain('example.com')->create();
|
||||
|
||||
$response = $this->get('/setting/mailgateway');
|
||||
|
||||
$this->assertInertiaHas('example.com', $response, 'data.data.0.domain');
|
||||
$this->assertInertiaHas('Lore', $response, 'data.data.0.name');
|
||||
$this->assertInertiaHas('Lokal', $response, 'data.data.0.type_human');
|
||||
$this->assertInertiaHas(true, $response, 'data.data.0.works');
|
||||
$this->assertInertiaHas($mailgateway->id, $response, 'data.data.0.id');
|
||||
$this->assertInertiaHas(route('mailgateway.update', ['mailgateway' => $mailgateway->id]), $response, 'data.data.0.links.update');
|
||||
}
|
||||
|
||||
public function testItDisplaysMailmanGateways(): void
|
||||
{
|
||||
$this->stubIo(MailmanService::class, function ($mock) {
|
||||
Phake::when($mock)->setCredentials()->thenReturn($mock);
|
||||
Phake::when($mock)->check()->thenReturn(true);
|
||||
});
|
||||
$this->withoutExceptionHandling();
|
||||
Mailgateway::factory()->type(MailmanType::class, MailmanTypeRequest::new()->create())->create();
|
||||
|
||||
$this->get('/setting/mailgateway')->assertOk();
|
||||
}
|
||||
|
||||
public function testItHasMeta(): void
|
||||
{
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
$response = $this->get('/setting/mailgateway');
|
||||
|
||||
$this->assertInertiaHas(route('mailgateway.store'), $response, 'data.meta.links.store');
|
||||
$this->assertInertiaHas([
|
||||
'id' => null,
|
||||
'name' => '-- kein --',
|
||||
], $response, 'data.meta.types.0');
|
||||
$this->assertInertiaHas([
|
||||
'id' => LocalType::class,
|
||||
'name' => 'Lokal',
|
||||
], $response, 'data.meta.types.1');
|
||||
$this->assertInertiaHas([
|
||||
'id' => MailmanType::class,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'url',
|
||||
'is_required' => true,
|
||||
],
|
||||
],
|
||||
], $response, 'data.meta.types.2');
|
||||
$this->assertInertiaHas([
|
||||
'domain' => '',
|
||||
'name' => '',
|
||||
'type' => [
|
||||
'params' => [],
|
||||
'cls' => null,
|
||||
],
|
||||
], $response, 'data.meta.default');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Mailgateway;
|
||||
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use App\Mailman\Support\MailmanService;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Phake;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MailmanTypeTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function testItChecksForWorks(): void
|
||||
{
|
||||
$this->withoutExceptionHandling();
|
||||
$this->stubIo(MailmanService::class, function ($mock) {
|
||||
Phake::when($mock)->setCredentials('https://example.com', 'user', 'secret')->thenReturn($mock);
|
||||
Phake::when($mock)->check()->thenReturn(true);
|
||||
});
|
||||
$type = app(MailmanType::class)->setParams([
|
||||
'url' => 'https://example.com',
|
||||
'user' => 'user',
|
||||
'password' => 'secret',
|
||||
]);
|
||||
|
||||
$this->assertTrue($type->works());
|
||||
}
|
||||
|
||||
public function testItCanReturnFalse(): void
|
||||
{
|
||||
$this->withoutExceptionHandling();
|
||||
$this->stubIo(MailmanService::class, function ($mock) {
|
||||
Phake::when($mock)->setCredentials('https://example.com', 'user', 'secret')->thenReturn($mock);
|
||||
Phake::when($mock)->check()->thenReturn(false);
|
||||
});
|
||||
$type = app(MailmanType::class)->setParams([
|
||||
'url' => 'https://example.com',
|
||||
'user' => 'user',
|
||||
'password' => 'secret',
|
||||
]);
|
||||
|
||||
$this->assertFalse($type->works());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Mailgateway;
|
||||
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Phake;
|
||||
use Tests\RequestFactories\MailgatewayRequestFactory;
|
||||
use Tests\RequestFactories\MailmanTypeRequest;
|
||||
use Tests\TestCase;
|
||||
|
||||
class StoreTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->login()->loginNami();
|
||||
}
|
||||
|
||||
public function testItCanStoreALocalGateway(): void
|
||||
{
|
||||
$response = $this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create());
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('mailgateways', [
|
||||
'domain' => 'example.com',
|
||||
'name' => 'lala',
|
||||
'type' => json_encode([
|
||||
'cls' => LocalType::class,
|
||||
'params' => [],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItCanStoreAMailmanGateway(): void
|
||||
{
|
||||
$typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret'];
|
||||
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
|
||||
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
|
||||
Phake::when($mock)->works()->thenReturn(true);
|
||||
});
|
||||
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create());
|
||||
|
||||
$this->assertDatabaseHas('mailgateways', [
|
||||
'type' => json_encode([
|
||||
'cls' => MailmanType::class,
|
||||
'params' => $typeParams,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testItThrowsErrorWhenMailmanConnectionFailed(): void
|
||||
{
|
||||
$typeParams = ['url' => 'https://example.com', 'user' => 'user', 'password' => 'secret'];
|
||||
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
|
||||
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
|
||||
Phake::when($mock)->works()->thenReturn(false);
|
||||
});
|
||||
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create())
|
||||
->assertJsonValidationErrors('connection');
|
||||
}
|
||||
|
||||
public function testItValidatesCustomFields(): void
|
||||
{
|
||||
$typeParams = ['url' => 'https://example.com', 'user' => '', 'password' => 'secret'];
|
||||
$this->stubIo(MailmanType::class, function ($mock) use ($typeParams) {
|
||||
Phake::when($mock)->setParams($typeParams)->thenReturn($mock);
|
||||
Phake::when($mock)->works()->thenReturn(false);
|
||||
});
|
||||
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->type(MailmanType::class, MailmanTypeRequest::new()->create($typeParams))->create())
|
||||
->assertJsonValidationErrors(['type.params.user' => 'Benutzer ist erforderlich.']);
|
||||
}
|
||||
|
||||
public function testItValidatesType(): void
|
||||
{
|
||||
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->missingType()->create())
|
||||
->assertJsonValidationErrors('type.cls');
|
||||
}
|
||||
|
||||
public function testItValidatesNameAndDomain(): void
|
||||
{
|
||||
$this->postJson('/api/mailgateway', MailgatewayRequestFactory::new()->withoutName()->withoutDomain()->create())
|
||||
->assertJsonValidationErrors('domain')
|
||||
->assertJsonValidationErrors('name');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Mailgateway;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\RequestFactories\MailgatewayRequestFactory;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdateTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->login()->loginNami();
|
||||
}
|
||||
|
||||
public function testItCanUpdateALocalGateway(): void
|
||||
{
|
||||
$mailgateway = Mailgateway::factory()->type(LocalType::class, [])->create();
|
||||
$response = $this->patchJson("/api/mailgateway/{$mailgateway->id}", MailgatewayRequestFactory::new()->name('lala')->type(LocalType::class, [])->domain('example.com')->create());
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('mailgateways', 1);
|
||||
}
|
||||
}
|
|
@ -194,12 +194,12 @@ class IndexTest extends TestCase
|
|||
Member::factory()->defaults()->for($group1)->create();
|
||||
Member::factory()->defaults()->for($group1)->create();
|
||||
|
||||
$oneResponse = $this->callFilter('member.index', ['group_id' => $group1->id]);
|
||||
$twoResponse = $this->callFilter('member.index', ['group_id' => $group2->id]);
|
||||
$oneResponse = $this->callFilter('member.index', ['group_ids' => [$group1->id]]);
|
||||
$twoResponse = $this->callFilter('member.index', ['group_ids' => [$group2->id]]);
|
||||
|
||||
$this->assertCount(3, $this->inertia($oneResponse, 'data.data'));
|
||||
$this->assertCount(0, $this->inertia($twoResponse, 'data.data'));
|
||||
$this->assertInertiaHas($group1->id, $oneResponse, 'data.meta.filter.group_id');
|
||||
$this->assertInertiaHas([$group1->id], $oneResponse, 'data.meta.filter.group_ids');
|
||||
}
|
||||
|
||||
public function testItFiltersForAusstand(): void
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\RequestFactories;
|
||||
|
||||
use App\Mailgateway\Types\Type;
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class MailgatewayRequestFactory extends RequestFactory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(5, true),
|
||||
'type' => [
|
||||
'cls' => app('mail-gateways')->random(),
|
||||
'params' => [],
|
||||
],
|
||||
'domain' => $this->faker->safeEmailDomain(),
|
||||
];
|
||||
}
|
||||
|
||||
public function name(string $name): self
|
||||
{
|
||||
return $this->state(['name' => $name]);
|
||||
}
|
||||
|
||||
public function domain(string $domain): self
|
||||
{
|
||||
return $this->state(['domain' => $domain]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 missingType(): self
|
||||
{
|
||||
return $this->state(['type' => [
|
||||
'cls' => null,
|
||||
'params' => [],
|
||||
]]);
|
||||
}
|
||||
|
||||
public function withoutName(): self
|
||||
{
|
||||
return $this->state(['name' => '']);
|
||||
}
|
||||
|
||||
public function withoutDomain(): self
|
||||
{
|
||||
return $this->state(['domain' => '']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\RequestFactories;
|
||||
|
||||
use App\Mailman\Data\MailingList;
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class MailinglistFactory extends RequestFactory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'description' => $this->faker->words(5, true),
|
||||
'displayName' => $this->faker->words(5, true),
|
||||
'fqdnListname' => $this->faker->safeEmail(),
|
||||
'listId' => $this->faker->domainName(),
|
||||
'listName' => $this->faker->words(5, true),
|
||||
'mailHost' => $this->faker->domainName(),
|
||||
'memberCount' => $this->faker->numberBetween(10, 100),
|
||||
'selfLink' => $this->faker->url(),
|
||||
'volume' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function toData(array $attributes = []): MailingList
|
||||
{
|
||||
return MailingList::from($this->create($attributes));
|
||||
}
|
||||
|
||||
public function id(string $id): self
|
||||
{
|
||||
return $this->state([
|
||||
'list_id' => $id,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\RequestFactories;
|
||||
|
||||
use Worksome\RequestFactories\RequestFactory;
|
||||
|
||||
class MailmanTypeRequest extends RequestFactory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'url' => 'https://'.$this->faker->domainName(),
|
||||
'user' => $this->faker->firstName(),
|
||||
'password' => $this->faker->password(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -45,16 +45,16 @@ class ServiceTest extends TestCase
|
|||
Http::fake([
|
||||
'http://mailman.test/api/lists/listid/roster/member?page=1&count=10' => Http::response(json_encode([
|
||||
'entries' => [
|
||||
['email' => 'test@example.com'],
|
||||
['email' => 'test2@example.com'],
|
||||
['email' => 'test@example.com', 'self_link' => 'https://example.com/994'],
|
||||
],
|
||||
'total_size' => 2,
|
||||
]), 200),
|
||||
]);
|
||||
|
||||
$result = app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members('listid');
|
||||
$result = app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members(MailingList::factory()->id('listid')->toData())->first();
|
||||
|
||||
$this->assertEquals(['test@example.com', 'test2@example.com'], $result->toArray());
|
||||
$this->assertEquals(994, $result->memberId);
|
||||
$this->assertEquals('test@example.com', $result->email);
|
||||
Http::assertSentCount(1);
|
||||
Http::assertSent(fn ($request) => 'GET' === $request->method() && 'http://mailman.test/api/lists/listid/roster/member?page=1&count=10' === $request->url() && $request->header('Authorization') === ['Basic '.base64_encode('user:secret')]);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class ServiceTest extends TestCase
|
|||
'http://mailman.test/api/lists/listid/roster/member?page=1&count=10' => Http::response('', 401),
|
||||
]);
|
||||
|
||||
app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members('listid')->first();
|
||||
app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members(MailingList::factory()->id('listid')->toData())->first();
|
||||
}
|
||||
|
||||
public function testItCanGetLists(): void
|
||||
|
@ -94,7 +94,7 @@ class ServiceTest extends TestCase
|
|||
foreach (range(3, 40) as $i) {
|
||||
yield [
|
||||
collect(range(1, $i))
|
||||
->map(fn ($num) => ['email' => 'test'.$num.'@example.com'])
|
||||
->map(fn ($num) => ['email' => 'test'.$num.'@example.com', 'self_link' => 'https://example.com/994'])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ class ServiceTest extends TestCase
|
|||
]);
|
||||
}
|
||||
|
||||
$result = app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members('listid');
|
||||
$result = app(MailmanService::class)->setCredentials('http://mailman.test/api/', 'user', 'secret')->members(MailingList::factory()->id('listid')->toData());
|
||||
|
||||
$this->assertCount($totals->count(), $result->toArray());
|
||||
Http::assertSentCount($totals->chunk(10)->count());
|
||||
|
|
Loading…
Reference in New Issue