Compare commits
119 Commits
16c5ab294a
...
20dea4502c
Author | SHA1 | Date |
---|---|---|
|
20dea4502c | |
|
ba3e88b930 | |
|
7cc3d4197b | |
|
c433abbbb4 | |
|
6806e54209 | |
|
d6a1de8fb9 | |
|
2567f28741 | |
|
4e845d29a9 | |
|
a9089f6d01 | |
|
5f07686bc9 | |
|
793429e8e0 | |
|
f4407e41e5 | |
|
c2cd87bc5b | |
|
71538a6d81 | |
|
b33596c6f0 | |
|
3693dcfe54 | |
|
4644f3832b | |
|
d68b166e15 | |
|
5d103b1908 | |
|
195fcc97b2 | |
|
ef4f4e68fb | |
|
7c73b489ce | |
|
74db2f7c6b | |
|
458d68c271 | |
|
471f766638 | |
|
dc3eb68861 | |
|
003e30d423 | |
|
5d40c0b2bb | |
|
e60105e5af | |
|
2fadba3193 | |
|
357a696a53 | |
|
79fed8c002 | |
|
9b02bddea9 | |
|
1b6057673a | |
|
5c9c59718c | |
|
c1e0842457 | |
|
a61fd55b29 | |
|
4156a8744a | |
|
84b17b312f | |
|
6906b7c097 | |
|
30c84f0cfd | |
|
d765bad0ed | |
|
89ce931f9d | |
|
2fac4d5a73 | |
|
56090b0104 | |
|
131061a3ee | |
|
9733c260b2 | |
|
e2e9ddb9fd | |
|
511a58ded3 | |
|
b937a8f4fa | |
|
cb01496028 | |
|
92284b053c | |
|
39f0d476fb | |
|
e6aa5e2c61 | |
|
d225be6137 | |
|
6094a16c36 | |
|
34355d9b32 | |
|
7a39798e8b | |
|
b07ad8d739 | |
|
5cd164f0d8 | |
|
652b83a140 | |
|
4d0923a01f | |
|
1de148eecc | |
|
12f053428c | |
|
0ce797c874 | |
|
8e2487c719 | |
|
2b1fe6ddcd | |
|
fe8a2d69e6 | |
|
76d6c4ef90 | |
|
8bd66010dd | |
|
bb9c219d81 | |
|
b52eedc254 | |
|
a06fd01194 | |
|
4a731f1104 | |
|
278a598078 | |
|
3c72b890fb | |
|
7de12a92dd | |
|
643959ff04 | |
|
5d16d65f8a | |
|
985b7942c4 | |
|
6582f49321 | |
|
5998230569 | |
|
0dbac1da4a | |
|
4b507aa297 | |
|
af45560fe1 | |
|
cecc90f07b | |
|
5b116a54c4 | |
|
0070976e34 | |
|
dd07f6dbd7 | |
|
ab74abdfdf | |
|
8f5448bbbc | |
|
2fd6598ef3 | |
|
0c90f66a66 | |
|
b541d246e0 | |
|
858ae7f8d1 | |
|
abf4158256 | |
|
f2d59e4535 | |
|
a75b4a7808 | |
|
ecf3cc998c | |
|
7202cd60f5 | |
|
9d5c71adcd | |
|
eb00d928fc | |
|
6683990755 | |
|
2a5f4832e1 | |
|
719b1920c9 | |
|
3260202465 | |
|
9b995641b6 | |
|
2d5921fe1d | |
|
efa9eeccbe | |
|
b33072ad20 | |
|
2e77823c3a | |
|
9a5d9ae30e | |
|
d607c8beac | |
|
1a86ea7ea5 | |
|
a157f2b656 | |
|
e3c7dad51e | |
|
749e3dcd7c | |
|
4417008d22 | |
|
b38591b87c |
|
@ -1,5 +1,9 @@
|
|||
# Letzte Änderungen
|
||||
|
||||
### 1.12.7
|
||||
|
||||
- Fix: Synchronisation von allen Mitgliedern bei Mail-Verteilern - nicht nur den ersten 20
|
||||
|
||||
### 1.12.6
|
||||
|
||||
- Fix: Beiträge von Familienmitgliedern splitten
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@servers(['docker' => ['stammsilva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']])
|
||||
@servers(['docker' => ['stamm-silva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']])
|
||||
|
||||
@task('deploy', ['on' => 'docker'])
|
||||
cd $ADREMA_PATH
|
||||
|
|
|
@ -22,7 +22,6 @@ class InsertMemberAction
|
|||
{
|
||||
$region = Region::firstWhere('nami_id', $member->regionId ?: -1);
|
||||
|
||||
|
||||
$payload = [
|
||||
'firstname' => $member->firstname,
|
||||
'lastname' => $member->lastname,
|
||||
|
@ -61,7 +60,17 @@ class InsertMemberAction
|
|||
}
|
||||
}
|
||||
|
||||
return Member::updateOrCreate(['nami_id' => $member->id], $payload);
|
||||
return tap(Member::updateOrCreate(['nami_id' => $member->id], $payload), function ($insertedMember) use ($member) {
|
||||
$insertedMember->bankAccount->update([
|
||||
'iban' => $member->bankAccount->iban,
|
||||
'bic' => $member->bankAccount->bic,
|
||||
'blz' => $member->bankAccount->blz,
|
||||
'account_number' => $member->bankAccount->accountNumber,
|
||||
'person' => $member->bankAccount->person,
|
||||
'bank_name' => $member->bankAccount->bankName,
|
||||
'nami_id' => $member->bankAccount->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function getSubscription(NamiMember $member): ?Subscription
|
||||
|
|
|
@ -34,7 +34,7 @@ class Activity extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Subactivity>
|
||||
* @return BelongsToMany<Subactivity, $this>
|
||||
*/
|
||||
public function subactivities(): BelongsToMany
|
||||
{
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use App\Country;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function handle(): array
|
||||
{
|
||||
return [
|
||||
'countries' => Country::select('name', 'id')->get(),
|
||||
'data' => [
|
||||
'country' => Country::firstWhere('name', 'Deutschland')->id,
|
||||
],
|
||||
'compilers' => app(ContributionFactory::class)->compilerSelect(),
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(): Response
|
||||
{
|
||||
session()->put('menu', 'contribution');
|
||||
session()->put('title', 'Zuschüsse');
|
||||
|
||||
return Inertia::render('contribution/VIndex', $this->handle());
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use App\Contribution\Documents\ContributionDocument;
|
||||
use App\Rules\JsonBase64Rule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class GenerateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param class-string<ContributionDocument> $document
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function handle(string $document, array $payload): BaseCompiler
|
||||
{
|
||||
return Tex::compile($document::fromRequest($payload));
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): BaseCompiler
|
||||
{
|
||||
$payload = $this->payload($request);
|
||||
$type = data_get($payload, 'type');
|
||||
ValidateAction::validateType($type);
|
||||
Validator::make($payload, app(ContributionFactory::class)->rules($type))->validate();
|
||||
|
||||
return $this->handle($type, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'payload' => [new JsonBase64Rule()],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function payload(ActionRequest $request): array
|
||||
{
|
||||
return json_decode(rawurldecode(base64_decode($request->input('payload', ''))), true);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\Documents\ContributionDocument;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\Tex\BaseCompiler;
|
||||
use Zoomyboy\Tex\Tex;
|
||||
|
||||
class GenerateApiAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param class-string<ContributionDocument> $document
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function handle(string $document, array $payload): BaseCompiler
|
||||
{
|
||||
return Tex::compile($document::fromApiRequest($payload));
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): BaseCompiler
|
||||
{
|
||||
ValidateAction::validateType($request->input('type'));
|
||||
|
||||
return $this->handle($request->input('type'), $request->input());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Contribution\Actions;
|
||||
|
||||
use App\Contribution\ContributionFactory;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ValidateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(): JsonResponse
|
||||
{
|
||||
return response()->json(['valid' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return app(ContributionFactory::class)->rules(request()->type);
|
||||
}
|
||||
|
||||
public function prepareForValidation(ActionRequest $request): void
|
||||
{
|
||||
static::validateType($request->input('type'));
|
||||
}
|
||||
|
||||
public static function validateType(?string $type = null): void
|
||||
{
|
||||
Validator::make(['type' => $type], app(ContributionFactory::class)->typeRule())->validate();
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ use App\Contribution\Documents\CitySolingenDocument;
|
|||
use App\Contribution\Documents\CityFrankfurtMainDocument;
|
||||
use App\Contribution\Documents\WuppertalDocument;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ContributionFactory
|
||||
{
|
||||
|
@ -32,31 +31,8 @@ class ContributionFactory
|
|||
public function compilerSelect(): Collection
|
||||
{
|
||||
return collect($this->documents)->map(fn ($document) => [
|
||||
'title' => $document::buttonName(),
|
||||
'class' => $document,
|
||||
'name' => $document::buttonName(),
|
||||
'id' => $document,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function typeRule(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', Rule::in($this->documents)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string<ContributionDocument> $type
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(string $type): array
|
||||
{
|
||||
return [
|
||||
...$type::globalRules(),
|
||||
...$type::rules(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class MemberData extends Data
|
|||
*/
|
||||
public static function fromModels(array $ids): Collection
|
||||
{
|
||||
return Member::whereIn('id', $ids)->orderByRaw('lastname, firstname')->get()->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([
|
||||
return Member::with('leaderMemberships', 'gender')->whereIn('id', $ids)->orderByRaw('lastname, firstname')->get()->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([
|
||||
...$member->toArray(),
|
||||
'birthday' => $member->birthday->toAtomString(),
|
||||
'isLeader' => $member->isLeader(),
|
||||
|
|
|
@ -120,17 +120,4 @@ class BdkjHesse extends ContributionDocument
|
|||
{
|
||||
return 'BDKJ Hessen';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
'zipLocation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use App\Contribution\Data\MemberData;
|
|||
use App\Contribution\Traits\FormatsDates;
|
||||
use App\Contribution\Traits\HasPdfBackground;
|
||||
use App\Country;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class CityFrankfurtMainDocument extends ContributionDocument
|
||||
|
@ -77,17 +77,4 @@ class CityFrankfurtMainDocument extends ContributionDocument
|
|||
{
|
||||
return 'Frankfurt';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
'zipLocation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,17 +73,4 @@ class CityRemscheidDocument extends ContributionDocument
|
|||
{
|
||||
return 'Remscheid';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'zipLocation' => 'required|string',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Contribution\Documents;
|
||||
|
||||
use App\Contribution\Data\MemberData;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Zoomyboy\Tex\Engine;
|
||||
|
@ -97,16 +97,4 @@ class CitySolingenDocument extends ContributionDocument
|
|||
{
|
||||
return 'Stadt Solingen';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'zipLocation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,23 +21,6 @@ abstract class ContributionDocument extends Document
|
|||
*/
|
||||
abstract public static function fromApiRequest(array $request): self;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
abstract public static function rules(): array;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function globalRules(): array
|
||||
{
|
||||
return [
|
||||
'eventName' => 'required|string',
|
||||
'members' => 'present|array|min:1',
|
||||
'members.*' => 'integer|exists:members,id',
|
||||
];
|
||||
}
|
||||
|
||||
public static function buttonName(): string
|
||||
{
|
||||
return 'Für ' . static::getName() . ' erstellen';;
|
||||
|
|
|
@ -68,17 +68,4 @@ class RdpNrwDocument extends ContributionDocument
|
|||
{
|
||||
return 'RdP NRW';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'country' => 'required|integer|exists:countries,id',
|
||||
'zipLocation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,16 +64,4 @@ class WuppertalDocument extends ContributionDocument
|
|||
{
|
||||
return 'Wuppertal';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function rules(): array
|
||||
{
|
||||
return [
|
||||
'dateFrom' => 'required|string|date_format:Y-m-d',
|
||||
'dateUntil' => 'required|string|date_format:Y-m-d',
|
||||
'zipLocation' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class CourseMember extends Model
|
|||
public $guarded = [];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Course, self>
|
||||
* @return BelongsTo<Course, $this>
|
||||
*/
|
||||
public function course(): BelongsTo
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ class CourseMember extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
|
|
|
@ -129,6 +129,7 @@ class Principal implements PrincipalBackendInterface
|
|||
*/
|
||||
public function findByUri($uri, $principalPrefix)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Efz;
|
||||
|
||||
use App\Dashboard\Blocks\Block;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class EfzPendingBlock extends Block
|
||||
{
|
||||
/**
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
public function query(): Builder
|
||||
{
|
||||
return Member::where(function ($query) {
|
||||
return $query->where('efz', '<=', now()->subYears(5)->endOfYear())
|
||||
->orWhereNull('efz');
|
||||
})
|
||||
->whereCurrentGroup()
|
||||
->orderByRaw('lastname, firstname')
|
||||
->whereHas('memberships', fn ($builder) => $builder->isLeader()->active());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{members: array<int, string>}
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return [
|
||||
'members' => $this->query()->get()->map(fn ($member) => $member->fullname)->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function component(): string
|
||||
{
|
||||
return 'efz-pending';
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return 'Ausstehende Führungszeugnisse';
|
||||
}
|
||||
}
|
|
@ -15,14 +15,13 @@ class Fee extends Model
|
|||
use HasFactory;
|
||||
use HasNamiField;
|
||||
|
||||
/** @var array<int, string> */
|
||||
public $fillable = ['name', 'nami_id'];
|
||||
|
||||
/** @var bool */
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* @return HasMany<Subscription>
|
||||
* @return HasMany<Subscription, $this>
|
||||
*/
|
||||
public function subscriptions(): HasMany
|
||||
{
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
namespace App\Form\Data;
|
||||
|
||||
use App\Fileshare\Data\FileshareResourceData;
|
||||
use App\Form\Fields\Field;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
|
||||
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
|
@ -16,4 +16,13 @@ class ExportData extends Data
|
|||
public function __construct(public ?FileshareResourceData $root = null, public ?string $groupBy = null, public ?string $toGroupField = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
* @return DataEloquentCast<self>
|
||||
*/
|
||||
public static function castUsing(array $arguments): DataEloquentCast
|
||||
{
|
||||
return new DataEloquentCast(static::class, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use Illuminate\Support\Collection;
|
|||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Attributes\WithTransformer;
|
||||
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
|
||||
|
||||
class FormConfigData extends Data
|
||||
{
|
||||
|
@ -29,4 +30,13 @@ class FormConfigData extends Data
|
|||
new FieldCollection([])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
* @return DataEloquentCast<self>
|
||||
*/
|
||||
public static function castUsing(array $arguments): DataEloquentCast
|
||||
{
|
||||
return new DataEloquentCast(static::class, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class Form extends Model implements HasMedia
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Participant>
|
||||
* @return HasMany<Participant, $this>
|
||||
*/
|
||||
public function participants(): HasMany
|
||||
{
|
||||
|
|
|
@ -33,7 +33,7 @@ class Participant extends Model implements Preventable
|
|||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Form, self>
|
||||
* @return BelongsTo<Form, $this>
|
||||
*/
|
||||
public function form(): BelongsTo
|
||||
{
|
||||
|
@ -41,7 +41,7 @@ class Participant extends Model implements Preventable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<self>
|
||||
* @return HasMany<Participant, $this>
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
|
@ -49,7 +49,7 @@ class Participant extends Model implements Preventable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
|
|
|
@ -26,7 +26,7 @@ class Group extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<self, self>
|
||||
* @return BelongsTo<Group, $this>
|
||||
*/
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ class Group extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<self>
|
||||
* @return HasMany<Group, $this>
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Login Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles authenticating users for the application and
|
||||
| redirecting them to your home screen. The controller uses a trait
|
||||
| to conveniently provide its functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = RouteServiceProvider::HOME;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
}
|
||||
|
||||
public function showLoginForm(): Response
|
||||
{
|
||||
session()->put('title', 'Anmelden');
|
||||
|
||||
return \Inertia::render('authentication/VLogin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user login request.
|
||||
*/
|
||||
protected function validateLogin(Request $request): void
|
||||
{
|
||||
$request->validate([
|
||||
$this->username() => 'required|max:255|string|email',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
|
||||
use RegistersUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = RouteServiceProvider::HOME;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
return User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password reset requests
|
||||
| and uses a simple trait to include this behavior. You're free to
|
||||
| explore this trait and override any methods you wish to tweak.
|
||||
|
|
||||
*/
|
||||
|
||||
use ResetsPasswords;
|
||||
|
||||
/**
|
||||
* Where to redirect users after resetting their password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = RouteServiceProvider::HOME;
|
||||
|
||||
/**
|
||||
* Display the password reset view for the given token.
|
||||
*
|
||||
* If no token is present, display the link request form.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function showResetForm(Request $request): Response
|
||||
{
|
||||
$token = $request->route()->parameter('token');
|
||||
|
||||
return Inertia::render('authentication/PasswordResetConfirm', [
|
||||
'token' => $token,
|
||||
'email' => $request->email,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -18,5 +18,7 @@ class Authenticate extends Middleware
|
|||
if (!$request->expectsJson()) {
|
||||
return route('login');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Module\ModuleSettings;
|
||||
use Modules\Module\ModuleSettings;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Inertia\Middleware;
|
||||
|
|
|
@ -39,7 +39,7 @@ class MassStoreAction
|
|||
foreach ($memberGroup as $members) {
|
||||
$invoice = Invoice::createForMember($members->first(), $members, $year);
|
||||
$invoice->save();
|
||||
$invoice->positions()->createMany($invoice->positions);
|
||||
$invoice->positions()->createMany($invoice->positions->toArray());
|
||||
$invoices->push($invoice->fresh('positions'));
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\Invoice\Models\Invoice;
|
|||
use App\Payment\Payment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
use Zoomyboy\Tex\Document;
|
||||
use Zoomyboy\Tex\Engine;
|
||||
use Zoomyboy\Tex\Template;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
use App\Dashboard\Blocks\Block;
|
||||
use App\Invoice\Models\InvoicePosition;
|
||||
use App\Member\Member;
|
||||
|
||||
class MemberPaymentBlock extends Block
|
||||
{
|
||||
/**
|
||||
* @return array<string, string|int>
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
$amount = InvoicePosition::whereHas('invoice', fn ($query) => $query->whereNeedsPayment())
|
||||
->selectRaw('sum(price) AS price')
|
||||
->first();
|
||||
$members = Member::whereHasPendingPayment()->count();
|
||||
|
||||
return [
|
||||
'members' => $members,
|
||||
'total_members' => Member::count(),
|
||||
'amount' => number_format((int) $amount->price / 100, 2, ',', '.') . ' €',
|
||||
];
|
||||
}
|
||||
|
||||
public function component(): string
|
||||
{
|
||||
return 'member-payment';
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return 'Ausstehende Mitgliedsbeiträge';
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ use App\Invoice\BillDocument;
|
|||
use App\Invoice\BillKind;
|
||||
use App\Invoice\Enums\InvoiceStatus;
|
||||
use App\Invoice\InvoiceDocument;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Invoice\Scopes\InvoiceFilterScope;
|
||||
use App\Member\Member;
|
||||
|
@ -37,7 +37,7 @@ class Invoice extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @return HasMany<InvoicePosition>
|
||||
* @return HasMany<InvoicePosition, $this>
|
||||
*/
|
||||
public function positions(): HasMany
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ class InvoicePosition extends Model
|
|||
public $guarded = [];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
|
@ -24,7 +24,7 @@ class InvoicePosition extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Invoice, self>
|
||||
* @return BelongsTo<Invoice, $this>
|
||||
*/
|
||||
public function invoice(): BelongsTo
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ use Spatie\LaravelData\Data;
|
|||
use Spatie\LaravelData\DataCollection;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
|
||||
|
||||
class Condition extends Data
|
||||
{
|
||||
|
@ -42,4 +43,13 @@ class Condition extends Data
|
|||
{
|
||||
return $this->mode === ConditionMode::ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
* @return DataEloquentCast<self>
|
||||
*/
|
||||
public static function castUsing(array $arguments): DataEloquentCast
|
||||
{
|
||||
return new DataEloquentCast(static::class, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
namespace App\Lib\Editor;
|
||||
|
||||
use Livewire\Wireable;
|
||||
use Spatie\LaravelData\Concerns\WireableData;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast;
|
||||
|
||||
/** @todo replace blocks with actual block data classes */
|
||||
class EditorData extends Data implements Editorable
|
||||
class EditorData extends Data implements Editorable, Wireable
|
||||
{
|
||||
|
||||
use WireableData;
|
||||
|
||||
/** @param array<int, mixed> $blocks */
|
||||
public function __construct(
|
||||
public string $version,
|
||||
|
@ -83,4 +88,13 @@ class EditorData extends Data implements Editorable
|
|||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
* @return DataEloquentCast<self>
|
||||
*/
|
||||
public static function castUsing(array $arguments): DataEloquentCast
|
||||
{
|
||||
return new DataEloquentCast(static::class, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use Spatie\LaravelData\Data;
|
|||
|
||||
/**
|
||||
* @template T of Model
|
||||
* @property Builder $query
|
||||
* @property Builder<T> $query
|
||||
*/
|
||||
abstract class Filter extends Data
|
||||
{
|
||||
|
@ -40,7 +40,7 @@ abstract class Filter extends Data
|
|||
}
|
||||
|
||||
/**
|
||||
* @return static(self<T>)
|
||||
* @return static
|
||||
*/
|
||||
public function toDefault(): self
|
||||
{
|
||||
|
|
|
@ -8,7 +8,7 @@ use Spatie\LaravelData\Data;
|
|||
|
||||
/**
|
||||
* @template T of Model
|
||||
* @property Builder $query
|
||||
* @property Builder<T> $query
|
||||
*/
|
||||
abstract class ScoutFilter extends Data
|
||||
{
|
||||
|
|
|
@ -25,7 +25,7 @@ class ResyncAction
|
|||
*/
|
||||
public function getResults(Maildispatcher $dispatcher): Collection
|
||||
{
|
||||
return FilterScope::fromPost($dispatcher->filter)->getQuery()->get()
|
||||
return FilterScope::fromPost($dispatcher->filter)->noPageLimit()->getQuery()->get()
|
||||
->filter(fn ($member) => $member->email || $member->email_parents)
|
||||
->map(fn ($member) => MailEntry::from(['email' => $member->email ?: $member->email_parents]))
|
||||
->unique(fn ($member) => $member->email);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Maildispatcher\Models;
|
||||
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use Modules\Mailgateway\Models\Mailgateway;
|
||||
use Database\Factories\Maildispatcher\Models\MaildispatcherFactory;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
@ -23,7 +23,7 @@ class Maildispatcher extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Mailgateway, self>
|
||||
* @return BelongsTo<Mailgateway, $this>
|
||||
*/
|
||||
public function gateway(): BelongsTo
|
||||
{
|
||||
|
|
|
@ -4,8 +4,7 @@ namespace App\Maildispatcher\Resources;
|
|||
|
||||
use App\Lib\HasMeta;
|
||||
use App\Maildispatcher\Models\Maildispatcher;
|
||||
use App\Mailgateway\Models\Mailgateway;
|
||||
use App\Mailgateway\Resources\MailgatewayResource;
|
||||
use Modules\Mailgateway\Models\Mailgateway;
|
||||
use App\Member\FilterScope;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?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.']);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
<?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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ class MailmanService
|
|||
return app(Paginator::class)->result(
|
||||
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.');
|
||||
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');
|
||||
|
@ -112,7 +112,7 @@ class MailmanService
|
|||
'pre_confirmed' => 'true',
|
||||
]);
|
||||
|
||||
throw_unless(201 === $response->status(), MailmanServiceException::class, 'Adding member '.$email.' to '.$list->listId.' failed');
|
||||
throw_unless(201 === $response->status(), MailmanServiceException::class, 'Adding member ' . $email . ' to ' . $list->listId . ' failed');
|
||||
}
|
||||
|
||||
public function removeMember(Member $member): void
|
||||
|
|
|
@ -10,6 +10,7 @@ use App\Member\Member;
|
|||
use App\Setting\NamiSettings;
|
||||
use App\Subactivity;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\LaravelNami\Data\BankAccount;
|
||||
use Zoomyboy\LaravelNami\Data\Member as NamiMember;
|
||||
|
||||
class NamiPutMemberAction
|
||||
|
@ -47,6 +48,16 @@ class NamiPutMemberAction
|
|||
'id' => $member->nami_id,
|
||||
'version' => $member->version,
|
||||
'keepdata' => $member->keepdata,
|
||||
'bankAccount' => BankAccount::from([
|
||||
'bankName' => $member->bankAccount->bank_name,
|
||||
'id' => $member->bankAccount->nami_id,
|
||||
'memberId' => $member->mitgliedsnr,
|
||||
'iban' => $member->bankAccount->iban,
|
||||
'bic' => $member->bankAccount->bic,
|
||||
'blz' => $member->bankAccount->blz,
|
||||
'person' => $member->bankAccount->person,
|
||||
'accountNumber' => $member->bankAccount->account_number,
|
||||
]),
|
||||
]);
|
||||
$response = $api->putMember($namiMember, $activity ? $activity->nami_id : null, $subactivity ? $subactivity->nami_id : null);
|
||||
Member::withoutEvents(function () use ($response, $member) {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Member;
|
||||
|
||||
use Database\Factories\Member\BankAccountFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BankAccount extends Model
|
||||
{
|
||||
/** @use HasFactory<BankAccountFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $primaryKey = 'member_id';
|
||||
}
|
|
@ -9,7 +9,8 @@ use Zoomyboy\LaravelNami\Data\Member as NamiMember;
|
|||
use Zoomyboy\LaravelNami\Data\MembershipEntry as NamiMembershipEntry;
|
||||
use Spatie\LaravelData\Attributes\DataCollectionOf;
|
||||
|
||||
class FullMember extends Data {
|
||||
class FullMember extends Data
|
||||
{
|
||||
/**
|
||||
* @param DataCollection<int, NamiCourse> $courses
|
||||
* @param DataCollection<int, NamiMembershipEntry> $memberships
|
||||
|
@ -20,6 +21,6 @@ class FullMember extends Data {
|
|||
public DataCollection $courses,
|
||||
#[DataCollectionOf(NamiMembershipEntry::class)]
|
||||
public DataCollection $memberships,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ use Zoomyboy\Osm\HasGeolocation;
|
|||
use Zoomyboy\Phone\HasPhoneNumbers;
|
||||
use App\Prevention\Enums\Prevention;
|
||||
use Database\Factories\Member\MemberFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
|
||||
/**
|
||||
* @property string $subscription_name
|
||||
|
@ -195,7 +196,7 @@ class Member extends Model implements Geolocatable
|
|||
|
||||
// ---------------------------------- Relations ----------------------------------
|
||||
/**
|
||||
* @return BelongsTo<Country, self>
|
||||
* @return BelongsTo<Country, $this>
|
||||
*/
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
|
@ -203,7 +204,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Gender, self>
|
||||
* @return BelongsTo<Gender, $this>
|
||||
*/
|
||||
public function gender(): BelongsTo
|
||||
{
|
||||
|
@ -211,7 +212,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Region, self>
|
||||
* @return BelongsTo<Region, $this>
|
||||
*/
|
||||
public function region(): BelongsTo
|
||||
{
|
||||
|
@ -222,7 +223,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<InvoicePosition>
|
||||
* @return HasMany<InvoicePosition, $this>
|
||||
*/
|
||||
public function invoicePositions(): HasMany
|
||||
{
|
||||
|
@ -230,7 +231,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Confession, self>
|
||||
* @return BelongsTo<Confession, $this>
|
||||
*/
|
||||
public function confession(): BelongsTo
|
||||
{
|
||||
|
@ -238,7 +239,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Nationality, self>
|
||||
* @return BelongsTo<Nationality, $this>
|
||||
*/
|
||||
public function nationality(): BelongsTo
|
||||
{
|
||||
|
@ -246,7 +247,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Subscription, self>
|
||||
* @return BelongsTo<Subscription, $this>
|
||||
*/
|
||||
public function subscription(): BelongsTo
|
||||
{
|
||||
|
@ -254,7 +255,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Group, self>
|
||||
* @return BelongsTo<Group, $this>
|
||||
*/
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
|
@ -262,7 +263,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<CourseMember>
|
||||
* @return HasMany<CourseMember, $this>
|
||||
*/
|
||||
public function courses(): HasMany
|
||||
{
|
||||
|
@ -270,7 +271,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Membership>
|
||||
* @return HasMany<Membership, $this>
|
||||
*/
|
||||
public function memberships(): HasMany
|
||||
{
|
||||
|
@ -278,7 +279,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Membership>
|
||||
* @return HasMany<Membership, $this>
|
||||
*/
|
||||
public function leaderMemberships(): HasMany
|
||||
{
|
||||
|
@ -286,21 +287,34 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Membership>
|
||||
* @return HasMany<Membership, $this>
|
||||
*/
|
||||
public function ageGroupMemberships(): HasMany
|
||||
{
|
||||
return $this->memberships()->isAgeGroup()->active();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasOne<BankAccount, $this>
|
||||
*/
|
||||
public function bankAccount(): HasOne
|
||||
{
|
||||
return $this->hasOne(BankAccount::class);
|
||||
}
|
||||
|
||||
public static function booted()
|
||||
{
|
||||
static::created(function (self $model): void {
|
||||
$model->bankAccount()->create([]);
|
||||
});
|
||||
|
||||
static::deleting(function (self $model): void {
|
||||
$model->memberships->each->delete();
|
||||
$model->courses->each->delete();
|
||||
$model->invoicePositions->each(function ($position) {
|
||||
$position->delete();
|
||||
});
|
||||
$model->bankAccount()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class Membership extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Activity, self>
|
||||
* @return BelongsTo<Activity, $this>
|
||||
*/
|
||||
public function activity(): BelongsTo
|
||||
{
|
||||
|
@ -40,7 +40,7 @@ class Membership extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Group, self>
|
||||
* @return BelongsTo<Group, $this>
|
||||
*/
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
|
@ -48,7 +48,7 @@ class Membership extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Subactivity, self>
|
||||
* @return BelongsTo<Subactivity, $this>
|
||||
*/
|
||||
public function subactivity(): BelongsTo
|
||||
{
|
||||
|
@ -56,7 +56,7 @@ class Membership extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Member, self>
|
||||
* @return BelongsTo<Member, $this>
|
||||
*/
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Membership;
|
||||
|
||||
use App\Dashboard\Blocks\Block;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class TestersBlock extends Block
|
||||
{
|
||||
/**
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
public function query(): Builder
|
||||
{
|
||||
return Member::whereHas('memberships', fn ($q) => $q->isTrying())
|
||||
->with('memberships', fn ($q) => $q->isTrying());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{members: array<int, array{name: string, try_ends_at: string, try_ends_at_human: string}>}
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return [
|
||||
'members' => $this->query()->get()->map(fn ($member) => [
|
||||
'name' => $member->fullname,
|
||||
'try_ends_at' => $member->memberships->first()->from->addWeeks(8)->format('d.m.Y'),
|
||||
'try_ends_at_human' => $member->memberships->first()->from->addWeeks(8)->diffForHumans(),
|
||||
])->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
public function component(): string
|
||||
{
|
||||
return 'testers';
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return 'Endende Schhnupperzeiten';
|
||||
}
|
||||
}
|
|
@ -14,9 +14,6 @@ class Subscription extends Model
|
|||
/** @use HasFactory<SubscriptionFactory> */
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public $fillable = ['name', 'fee_id'];
|
||||
|
||||
public function getAmount(): int
|
||||
|
@ -25,7 +22,7 @@ class Subscription extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Fee, self>
|
||||
* @return BelongsTo<Fee, $this>
|
||||
*/
|
||||
public function fee(): BelongsTo
|
||||
{
|
||||
|
@ -33,7 +30,7 @@ class Subscription extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<SubscriptionChild>
|
||||
* @return HasMany<SubscriptionChild, $this>
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Prevention\Actions;
|
||||
|
||||
use App\Prevention\PreventionSettings;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class SettingApiAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => app(PreventionSettings::class)->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Prevention\Actions;
|
||||
|
||||
use App\Lib\Editor\EditorData;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use App\Prevention\PreventionSettings;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class SettingStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'formmail' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(ActionRequest $request): void
|
||||
{
|
||||
$settings = app(PreventionSettings::class);
|
||||
$settings->formmail = EditorData::from($request->formmail);
|
||||
$settings->save();
|
||||
|
||||
Succeeded::message('Einstellungen gespeichert.')->dispatch();
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Prevention\Mails;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Lib\Editor\EditorData;
|
||||
use App\Prevention\Contracts\Preventable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
@ -11,6 +10,7 @@ use Illuminate\Mail\Mailable;
|
|||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
|
||||
class PreventionRememberMail extends Mailable
|
||||
{
|
||||
|
|
|
@ -3,13 +3,11 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Telescope\Telescope;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -30,14 +28,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
return $this;
|
||||
});
|
||||
|
||||
app()->bind('mail-gateways', fn () => collect([
|
||||
LocalType::class,
|
||||
MailmanType::class,
|
||||
]));
|
||||
|
||||
app()->extend('media-library-helpers', fn ($p) => $p->put('form', Form::class));
|
||||
|
||||
Blade::componentNamespace('App\\View\\Mail', 'mail-view');
|
||||
Model::preventLazyLoading(!app()->isProduction());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class BaseServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
Blade::componentNamespace('App\\View\\Ui', 'ui');
|
||||
Blade::componentNamespace('App\\View\\Page', 'page');
|
||||
Blade::componentNamespace('App\\View\\Form', 'form');
|
||||
|
||||
ComponentAttributeBag::macro('mergeWhen', function ($condition, $key, $attributes) {
|
||||
/** @var ComponentAttributeBag */
|
||||
$self = $this;
|
||||
return $condition ? $self->merge([$key => $attributes]) : $self;
|
||||
});
|
||||
|
||||
Livewire::resolveMissingComponent(function ($name) {
|
||||
'modules.dashboard.components.dashboard-component';
|
||||
if (str($name)->startsWith('modules.')) {
|
||||
return str($name)->explode('.')->map(fn ($name) => str($name)->studly()->toString())->implode('\\');
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ use Illuminate\Http\RedirectResponse;
|
|||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
/** @deprecated */
|
||||
class StoreAction
|
||||
{
|
||||
use AsAction;
|
||||
|
|
|
@ -8,6 +8,7 @@ use Inertia\Inertia;
|
|||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
/** @deprecated */
|
||||
class ViewAction
|
||||
{
|
||||
use AsAction;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Setting\Data;
|
||||
|
||||
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
class SettingSynthesizer extends Synth
|
||||
{
|
||||
public static $key = 'setting-class';
|
||||
|
||||
public static function match($target)
|
||||
{
|
||||
return $target instanceof Settings;
|
||||
}
|
||||
|
||||
public function dehydrate($target)
|
||||
{
|
||||
return [$target->toArray(), ['setting_class' => get_class($target)]];
|
||||
}
|
||||
|
||||
public function hydrate($value, $meta)
|
||||
{
|
||||
return app($meta['setting_class'])->fill($value);
|
||||
}
|
||||
|
||||
public function get(&$target, $key)
|
||||
{
|
||||
return $target->{$key};
|
||||
}
|
||||
|
||||
public function set(&$target, $key, $value)
|
||||
{
|
||||
$target->{$key} = $value;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ abstract class LocalSettings extends Settings
|
|||
|
||||
public function url(): string
|
||||
{
|
||||
return route('setting.view', ['settingGroup' => $this->group()]);
|
||||
return url('setting/' . $this->group());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,14 +3,10 @@
|
|||
namespace App\Setting;
|
||||
|
||||
use App\Group;
|
||||
use App\Initialize\Actions\NamiLoginCheckAction;
|
||||
use App\Nami\Actions\SettingSaveAction;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Zoomyboy\LaravelNami\Api;
|
||||
use Zoomyboy\LaravelNami\Nami;
|
||||
|
||||
class NamiSettings extends LocalSettings implements Storeable
|
||||
class NamiSettings extends LocalSettings
|
||||
{
|
||||
public int $mglnr;
|
||||
|
||||
|
@ -43,14 +39,6 @@ class NamiSettings extends LocalSettings implements Storeable
|
|||
];
|
||||
}
|
||||
|
||||
public function beforeSave(ActionRequest $request): void
|
||||
{
|
||||
NamiLoginCheckAction::run([
|
||||
'mglnr' => $request->mglnr,
|
||||
'password' => $request->password,
|
||||
]);
|
||||
}
|
||||
|
||||
public function localGroup(): ?Group
|
||||
{
|
||||
return Group::firstWhere('nami_id', $this->default_group_id);
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
namespace App\Setting;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SettingFactory
|
||||
{
|
||||
|
@ -26,16 +25,11 @@ class SettingFactory
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{url: string, is_active: bool}>
|
||||
* @return Collection<int, LocalSettings>
|
||||
*/
|
||||
public function getShare(): array
|
||||
public function all(): Collection
|
||||
{
|
||||
return collect($this->settings)->map(fn ($setting) => [
|
||||
'url' => (new $setting)->url(),
|
||||
'is_active' => url(request()->path()) === (new $setting)->url(),
|
||||
'title' => $setting::title(),
|
||||
])
|
||||
->toArray();
|
||||
return collect($this->settings)->map(fn ($setting) => new $setting);
|
||||
}
|
||||
|
||||
public function resolveGroupName(string $name): LocalSettings
|
||||
|
|
|
@ -2,16 +2,12 @@
|
|||
|
||||
namespace App\Setting;
|
||||
|
||||
use App\Fileshare\FileshareSettings;
|
||||
use App\Form\FormSettings;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Mailgateway\MailgatewaySettings;
|
||||
use App\Module\ModuleSettings;
|
||||
use App\Prevention\PreventionSettings;
|
||||
use App\Setting\Actions\StoreAction;
|
||||
use App\Setting\Actions\ViewAction;
|
||||
use Modules\Module\ModuleSettings;
|
||||
use App\Setting\Data\SettingSynthesizer;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Invoice\InvoiceSettings;
|
||||
|
||||
class SettingServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -24,9 +20,6 @@ class SettingServiceProvider extends ServiceProvider
|
|||
{
|
||||
app()->singleton(SettingFactory::class, fn () => new SettingFactory());
|
||||
app(Router::class)->bind('settingGroup', fn ($param) => app(SettingFactory::class)->resolveGroupName($param));
|
||||
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.view')->get('/setting/{settingGroup}', ViewAction::class);
|
||||
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.data')->get('/setting/{settingGroup}/data', ViewAction::class);
|
||||
app(Router::class)->middleware(['web', 'auth:web'])->name('setting.store')->post('/setting/{settingGroup}', StoreAction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,10 +31,8 @@ class SettingServiceProvider extends ServiceProvider
|
|||
{
|
||||
app(SettingFactory::class)->register(ModuleSettings::class);
|
||||
app(SettingFactory::class)->register(InvoiceSettings::class);
|
||||
app(SettingFactory::class)->register(MailgatewaySettings::class);
|
||||
app(SettingFactory::class)->register(NamiSettings::class);
|
||||
app(SettingFactory::class)->register(FormSettings::class);
|
||||
app(SettingFactory::class)->register(FileshareSettings::class);
|
||||
app(SettingFactory::class)->register(PreventionSettings::class);
|
||||
|
||||
Livewire::propertySynthesizer(SettingSynthesizer::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,6 @@ class Subactivity extends Model
|
|||
use HasNamiField;
|
||||
use Sluggable;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public $fillable = ['is_age_group', 'is_filterable', 'slug', 'name', 'nami_id'];
|
||||
|
||||
/**
|
||||
|
@ -26,9 +23,6 @@ class Subactivity extends Model
|
|||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $casts = [
|
||||
'is_age_group' => 'boolean',
|
||||
'is_filterable' => 'boolean',
|
||||
|
@ -47,7 +41,7 @@ class Subactivity extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany<Activity>
|
||||
* @return BelongsToMany<Activity, $this>
|
||||
*/
|
||||
public function activities(): BelongsToMany
|
||||
{
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use App\Auth\ResetPassword;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Modules\Auth\Mails\ResetPassword;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Enums;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use InvalidArgumentException;
|
||||
|
||||
enum Variant: string
|
||||
{
|
||||
|
||||
case PRIMARY = 'primary';
|
||||
case SECONDARY = 'secondary';
|
||||
case PRIMARYLIGHT = 'primary-light';
|
||||
case WARNING = 'warning';
|
||||
case INFO = 'info';
|
||||
case DANGER = 'danger';
|
||||
|
||||
public function foreground(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PRIMARY => 'text-primary-300',
|
||||
self::SECONDARY => 'text-primary-400',
|
||||
self::PRIMARYLIGHT => 'text-primary-200',
|
||||
self::WARNING => 'text-yellow-300',
|
||||
self::INFO => 'text-blue-300',
|
||||
self::DANGER => 'text-red-100',
|
||||
};
|
||||
}
|
||||
|
||||
public function background(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PRIMARY => 'bg-primary-700',
|
||||
self::SECONDARY => 'bg-primary-800',
|
||||
self::PRIMARYLIGHT => 'bg-primary-600',
|
||||
self::WARNING => 'bg-yellow-700',
|
||||
self::INFO => 'bg-blue-700',
|
||||
self::DANGER => 'bg-red-400',
|
||||
};
|
||||
}
|
||||
|
||||
public function hoverForeground(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PRIMARY => 'hover:text-primary-100',
|
||||
self::SECONDARY => 'hover:text-primary-200',
|
||||
self::PRIMARYLIGHT => 'hover:text-primary-100',
|
||||
self::WARNING => 'hover:text-yellow-100',
|
||||
self::INFO => 'hover:text-blue-100',
|
||||
self::DANGER => 'hover:text-red-100',
|
||||
};
|
||||
}
|
||||
|
||||
public function hoverBackground(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PRIMARY => 'hover:bg-primary-500',
|
||||
self::SECONDARY => 'hover:bg-primary-600',
|
||||
self::PRIMARYLIGHT => 'hover:bg-primary-500',
|
||||
self::WARNING => 'hover:bg-yellow-500',
|
||||
self::INFO => 'hover:bg-blue-500',
|
||||
self::DANGER => 'hover:bg-red-500',
|
||||
};
|
||||
}
|
||||
|
||||
public static function fromString(string $input): self
|
||||
{
|
||||
return collect(static::cases())
|
||||
->first(fn ($variant) => $variant->value === $input)
|
||||
?: throw new InvalidArgumentException("Unknown variant: {$input} - Available Variants: " . self::values()->implode(', '));
|
||||
}
|
||||
|
||||
public static function values(): Collection
|
||||
{
|
||||
return collect(static::cases())->map(fn ($variant) => $variant->value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Editor extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $size = 'default',
|
||||
public ?string $hint = null,
|
||||
public bool $required = false,
|
||||
public string $label = '',
|
||||
public string $id = ''
|
||||
) {
|
||||
$this->id = $this->id ? $this->id : $this->name;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="flex flex-col group {{$heightClass}}">
|
||||
@if ($label)
|
||||
<x-form::label :required="$required">{{$label}}</x-form::label>
|
||||
@endif
|
||||
|
||||
<div class="relative w-full h-full">
|
||||
<div
|
||||
class="
|
||||
w-full border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg
|
||||
group-[.size-default]:border-2 group-[.size-sm]:border
|
||||
group-[.size-default]:text-sm group-[.size-sm]:text-xs
|
||||
group-[.size-default]:p-2 group-[.size-sm]:p-1
|
||||
"
|
||||
@updated="$wire.{{$attributes->wire('model')->value}} = $event.detail"
|
||||
x-data="editor($wire.{{$attributes->wire('model')->value}})"
|
||||
id="{{$id}}"
|
||||
{{$attributes}}
|
||||
></div>
|
||||
<x-ui::errors :for="$name" />
|
||||
@if($hint)
|
||||
<x-form::hint>{{$hint}}</x-form::hint>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Hint extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public bool $required = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div {{ $attributes->merge(['class' => 'h-full items-center flex absolute top-0 right-0']) }}>
|
||||
<div x-tooltip.raw="{{$slot}}" class="mr-2">
|
||||
<x-ui::sprite src="info-button" class="w-5 h-5 text-primary-700"></x-ui::sprite>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Label extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public bool $required = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<span class="font-semibold leading-none text-gray-400 group-[.size-default]:text-sm group-[.size-sm]:text-xs">
|
||||
{{ $slot }}
|
||||
@if ($required)
|
||||
<span class="text-red-800"> *</span>
|
||||
@endif
|
||||
</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Lever extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $size = 'default',
|
||||
public $value = null,
|
||||
public ?string $hint = null,
|
||||
public bool $disabled = false,
|
||||
public bool $required = false,
|
||||
public string $label = '',
|
||||
public string $id = ''
|
||||
) {
|
||||
$this->id = $this->id ? $this->id : $this->name;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<label class="flex flex-col items-start group {{$heightClass}} " for="{{$id}}" style="{{$heightVars}}">
|
||||
@if ($label)
|
||||
<x-form::label :required="$required">{{$label}}</x-form::label>
|
||||
@endif
|
||||
<span class="relative flex-none flex h-[var(--height)] @if($hint) pr-8 @endif">
|
||||
<input id="{{$id}}" type="checkbox" name="{{$name}}" value="{{$value}}" @if($disabled) disabled="disabled" @endif class="absolute peer opacity-0" {{ $attributes }} />
|
||||
<span class="relative cursor-pointer h-full w-[calc(var(--height)*2)] rounded peer-focus:bg-red-500 duration-300 bg-gray-700 peer-checked:bg-primary-700"></span>
|
||||
<span class="absolute h-full top-0 left-0 flex-none flex justify-center items-center aspect-square">
|
||||
<x-ui::sprite
|
||||
class="relative text-gray-400 flex-none text-white duration-300 group-[.size-default]:size-3 group-[.size-sm]:size-2"
|
||||
src="check"
|
||||
></x-ui::sprite>
|
||||
</span>
|
||||
<span class="absolute h-full top-0 left-[var(--height)] flex-none flex justify-center items-center aspect-square">
|
||||
<x-ui::sprite
|
||||
class="relative text-gray-400 flex-none text-white duration-300 group-[.size-default]:size-3 group-[.size-sm]:size-2"
|
||||
src="close"
|
||||
></x-ui::sprite>
|
||||
</span>
|
||||
<var class="absolute duration-300 bg-gray-400 rounded
|
||||
top-[var(--padding)] left-[var(--padding)]
|
||||
size-[calc(var(--height)-var(--padding)*2)] peer-checked:left-[calc(var(--height)+var(--padding))]"
|
||||
></var>
|
||||
@if($hint)
|
||||
<x-form::hint>{{$hint}}</x-form::hint>
|
||||
@endif
|
||||
</span>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class SaveButton extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $form = '')
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<button @if($form) form="{{$form}}" @endif type="submit" class="flex items-center transition-all justify-center w-8 h-8 bg-primary-700 hover:bg-primary-600 rounded" x-tooltip="`speichern`">
|
||||
<x-ui::sprite class="w-4 h-4 text-white" src="save"></x-ui::sprite>
|
||||
</button>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Select extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $size = 'default',
|
||||
public ?string $hint = null,
|
||||
public bool $required = false,
|
||||
public string $label = '',
|
||||
public $options = [],
|
||||
public bool $disabled = false,
|
||||
public string $id = ''
|
||||
) {
|
||||
$this->id = $this->id ? $this->id : $this->name;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<label class="flex flex-col group {{$heightClass}}" for="{{$id}}" style="{{$heightVars}}">
|
||||
@if ($label)
|
||||
<x-form::label :required="$required">{{$label}}</x-form::label>
|
||||
@endif
|
||||
|
||||
<div class="relative flex-none flex">
|
||||
<select {{$attributes}} @if($disabled) disabled @endif name="{{$name}}" id="{{$id}}"
|
||||
class="
|
||||
w-full h-[var(--height)] border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg
|
||||
group-[.size-default]:border-2 group-[.size-sm]:border
|
||||
group-[.size-default]:text-sm group-[.size-sm]:text-xs
|
||||
group-[.size-default]:px-2 group-[.size-sm]:px-1
|
||||
py-0
|
||||
"
|
||||
>
|
||||
<option value="">-- kein --</option>
|
||||
@foreach ($options as $option)
|
||||
<option value="{{$option['id']}}">{{ $option['name'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<x-ui::errors :for="$name" />
|
||||
@if($hint)
|
||||
<x-form::hint class="right-6">{{$hint}}</x-form::hint>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Form;
|
||||
|
||||
use App\View\Traits\HasFormDimensions;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Text extends Component
|
||||
{
|
||||
|
||||
use HasFormDimensions;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $size = 'default',
|
||||
public ?string $hint = null,
|
||||
public bool $required = false,
|
||||
public string $label = '',
|
||||
public string $type = 'text',
|
||||
public string $id = ''
|
||||
) {
|
||||
$this->id = $this->id ? $this->id : $this->name;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<label {{ $attributes->merge(['class' => 'flex flex-col group '.$heightClass])->only('class') }} for="{{$id}}" style="{{$heightVars}}">
|
||||
@if ($label)
|
||||
<x-form::label :required="$required">{{$label}}</x-form::label>
|
||||
@endif
|
||||
<div class="relative flex-none flex">
|
||||
<input
|
||||
id="{{$id}}"
|
||||
type="{{$type}}"
|
||||
@if ($type === 'password') autocomplete="off" @endif
|
||||
placeholder=""
|
||||
class="
|
||||
w-full h-[var(--height)] border-gray-600 border-solid text-gray-300 bg-gray-700 leading-none rounded-lg
|
||||
group-[.size-default]:border-2 group-[.size-sm]:border
|
||||
group-[.size-default]:text-sm group-[.size-sm]:text-xs
|
||||
group-[.size-default]:p-2 group-[.size-sm]:p-1
|
||||
"
|
||||
{{ $attributes->except('class') }}
|
||||
/>
|
||||
<x-ui::errors :for="$name" />
|
||||
@if($hint)
|
||||
<x-form::hint>{{$hint}}</x-form::hint>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Full extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $title = '', public ?string $heading = null)
|
||||
{
|
||||
session()->put('title', $title);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="min-w-[16rem] sm:min-w-[18rem] md:min-w-[24rem] bg-gray-800 rounded-xl overflow-hidden shadow-lg @if($heading === null) p-6 md:p-10 @endif">
|
||||
@if ($heading)
|
||||
<div class="h-24 p-6 md:px-10 bg-primary-800 flex justify-between items-center w-full">
|
||||
<span class="text-primary-500 text-xl">{{$heading}}</span>
|
||||
<img src="{{asset('img/dpsg.gif')}}" class="w-24" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div @if($heading !== null) class="p-6 md:p-10" @endif>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Header extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $title)
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="h-16 px-6 flex items-center justify-between border-b border-solid border-gray-600 group-[.is-bright]:border-gray-500">
|
||||
<div class="flex items-center space-x-4">
|
||||
{{ $beforeTitle ?? ''}}
|
||||
<span class="text-sm md:text-xl font-semibold leading-none text-white">{{ $title }}</span>
|
||||
{{ $toolbar ?? '' }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-4 ml-2">
|
||||
{{ $right ?? '' }}
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Layout extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $pageClass = '', public string $title = '', public string $menu = '')
|
||||
{
|
||||
session()->put('title', $title);
|
||||
session()->put('menu', $menu);
|
||||
}
|
||||
|
||||
public function userName(): string
|
||||
{
|
||||
return auth()->user()->firstname . ' ' . auth()->user()->lastname;
|
||||
}
|
||||
|
||||
public function userAvatar(): string
|
||||
{
|
||||
return auth()->user()->getGravatarUrl();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="grow flex flex-col" @refresh-page.window="$wire.$refresh">
|
||||
<div class="grow bg-gray-900 flex flex-col duration-300 navbar:ml-60">
|
||||
<x-page::header :title="$title">
|
||||
<x-slot:beforeTitle>
|
||||
<a href="#" class="lg:hidden" wire:click.prevent="dispatch('toggle-sidebar')">
|
||||
<x-ui::sprite src="menu" class="text-gray-100 w-5 h-5"></x-ui::sprite>
|
||||
</a>
|
||||
</x-slot:beforeTitle>
|
||||
<x-slot:toolbar>
|
||||
{{ $toolbar ?? ''}}
|
||||
</x-slot:toolbar>
|
||||
<x-slot:right>
|
||||
{{ $right ?? '' }}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="rounded-full overflow-hidden border-2 border-solid border-gray-300">
|
||||
<img src="{{ $userAvatar() }}" class="w-8 h-8 object-cover" />
|
||||
</div>
|
||||
<div class="text-gray-300"">{{ $userName() }}</div>
|
||||
</div>
|
||||
</x-slot:right>
|
||||
</x-page::header>
|
||||
|
||||
<div class="grow flex flex-col {{$pageClass}}">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
<livewire:page.sidebar :mobile="true" />
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class MenuEntry extends Component
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public string $href,
|
||||
public string $menu,
|
||||
public string $icon,
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<a class="flex text-white py-2 px-3 rounded-lg hover:bg-gray-600 {{ $menu === session('menu') ? 'bg-gray-700' : '' }}" href="{{$href}}">
|
||||
<x-ui::sprite class="text-white w-6 h-6 mr-4" src="{{$icon}}"></x-ui::sprite>
|
||||
<span class="font-semibold">
|
||||
{{ $slot }}
|
||||
</span>
|
||||
</a>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Modal extends Component
|
||||
{
|
||||
|
||||
public ?string $component = null;
|
||||
public array $props = [];
|
||||
public string $key = '';
|
||||
public array $actions = [];
|
||||
public ?string $size = null;
|
||||
public string $title = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
#[On('openModal')]
|
||||
public function onOpenModal(string $component, string $title, array $props = [], array $actions = ['storeable', 'closeable'], string $size = "xl"): void
|
||||
{
|
||||
$this->component = $component;
|
||||
$this->props = $props;
|
||||
$this->size = $size;
|
||||
$this->title = $title;
|
||||
$this->key = md5(json_encode(['component' => $component, 'props' => $props]));
|
||||
$this->actions = $this->parseActions($actions);
|
||||
}
|
||||
|
||||
public function parseActions(array $actions): array
|
||||
{
|
||||
return collect($actions)->map(function ($action) {
|
||||
if ($action === 'closeable') {
|
||||
return ['event' => 'closeModal', 'icon' => 'close', 'label' => 'Schließen'];
|
||||
}
|
||||
|
||||
if ($action === 'storeable') {
|
||||
return ['event' => 'onStoreFromModal', 'icon' => 'save', 'label' => 'Speichern'];
|
||||
}
|
||||
|
||||
return $action;
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
#[On('closeModal')]
|
||||
public function onCloseModal(): void
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
public function sizeClass(): string
|
||||
{
|
||||
if ($this->size === 'lg') {
|
||||
return 'max-w-lg';
|
||||
}
|
||||
|
||||
if ($this->size === 'xl') {
|
||||
return 'max-w-xl';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div>
|
||||
@if($component)
|
||||
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm" @click.self="$dispatch('closeModal')">
|
||||
<div
|
||||
class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-h-full flex flex-col overflow-auto {{$this->sizeClass()}}"
|
||||
>
|
||||
<div class="flex">
|
||||
<h3 class="font-semibold text-primary-200 text-xl grow">{{$title}}</h3>
|
||||
<div class="flex space-x-6">
|
||||
@foreach ($this->actions as $action)
|
||||
<a x-tooltip.raw="{{$action['label']}}" href="#" @click.prevent="$dispatch('{{$action['event']}}')">
|
||||
<x-ui::sprite :src="$action['icon']" class="text-zinc-400 w-6 h-6"></x-ui::sprite>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-primary-100 group is-popup grow flex flex-col mt-3">
|
||||
<div>
|
||||
@if ($component)
|
||||
@livewire($component, $props, key($key))
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div></div>
|
||||
@endif
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use App\Setting\SettingFactory;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class SettingLayout extends Component
|
||||
{
|
||||
|
||||
public array $entries;
|
||||
|
||||
public function __construct(public string $active)
|
||||
{
|
||||
$this->entries = app(SettingFactory::class)->all()
|
||||
->map(fn ($setting) => [
|
||||
'url' => $setting->url(),
|
||||
'is_active' => get_class($setting) === $active,
|
||||
'title' => $setting->title(),
|
||||
])->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<x-page::layout :title="$active::title()" menu="setting">
|
||||
<x-slot:right>
|
||||
{{ $right ?? '' }}
|
||||
</x-slot:right>
|
||||
<x-slot:toolbar>
|
||||
{{ $toolbar ?? '' }}
|
||||
</x-slot:toolbar>
|
||||
<div class="flex grow relative">
|
||||
<x-ui::menulist :entries="$entries"></x-ui::menulist>
|
||||
<div class="grow">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</x-page::layout>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Page;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Sidebar extends Component
|
||||
{
|
||||
|
||||
public $mobile = false;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div
|
||||
class="fixed z-40 bg-gray-800 p-6 w-60 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between duration-300
|
||||
@if (!$mobile) left-[-16rem] navbar:left-0 @endif"
|
||||
@if($mobile)
|
||||
x-data="{ visible: false }"
|
||||
x-on:toggle-sidebar.window="visible = true"
|
||||
:class="{'left-[-16rem]' : !visible, 'left-0': visible}"
|
||||
@endif
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<x-page::menu-entry href="/" menu="dashboard" icon="loss">Dashboard</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/member" menu="member" icon="user">Mitglieder</x-page::menu-entry>
|
||||
<x-page::menu-entry v-show="hasModule('bill')" href="/subscription" menu="subscription" icon="money">Beiträge</x-page::menu-entry>
|
||||
<x-page::menu-entry v-show="hasModule('bill')" href="/invoice" menu="invoice" icon="moneypaper">Rechnungen</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/contribution" menu="contribution" icon="contribution">Zuschüsse</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/activity" menu="activity" icon="activity">Tätigkeiten</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/group" menu="group" icon="group">Gruppierungen</x-page::menu-entry>
|
||||
<x-page::menu-entry v-if="hasModule('event')" href="/form" menu="form" icon="event">Veranstaltungen</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</x-page::menu-entry>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<a href="#" class="flex w-full px-3 py-2 rounded-xl text-gray-300 bg-gray-700" @click.prevent="dispatch('show-search')">
|
||||
<x-ui::sprite class="text-white w-6 h-6 mr-4" src="search"></x-ui::sprite>
|
||||
<div class="">Suchen</div>
|
||||
</a>
|
||||
<x-page::menu-entry href="/setting" menu="setting" icon="setting">Einstellungen</x-page::menu-entry>
|
||||
<x-page::menu-entry href="/logout" menu="" icon="logout">Abmelden</x-page::menu-entry>
|
||||
</div>
|
||||
@if($mobile)
|
||||
<a href="#" class="absolute right-0 top-0 mr-2 mt-2" @click.prevent="visible = false">
|
||||
<x-ui::sprite src="close" class="w-5 h-5 text-gray-300"></x-ui::sprite>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Traits;
|
||||
|
||||
use App\View\Enums\Variant;
|
||||
|
||||
trait HasColors
|
||||
{
|
||||
public function bgColor(string $variant): string
|
||||
{
|
||||
$variant = Variant::fromString($variant);
|
||||
|
||||
return implode(' ', [
|
||||
$variant->background(),
|
||||
$variant->hoverBackground(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function fgColor(string $variant): string
|
||||
{
|
||||
$variant = Variant::fromString($variant);
|
||||
|
||||
return implode(' ', [
|
||||
$variant->foreground(),
|
||||
$variant->hoverForeground(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function allColors(string $variant): string
|
||||
{
|
||||
return "{$this->bgColor($variant)} {$this->fgColor($variant)}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Traits;
|
||||
|
||||
trait HasFormDimensions
|
||||
{
|
||||
|
||||
public function heightVars(): string
|
||||
{
|
||||
return data_get([
|
||||
'default' => '--height: 35px; --padding: 3px;',
|
||||
'sm' => '--height: 23px; --padding: 2px;',
|
||||
], $this->size);
|
||||
}
|
||||
|
||||
public function heightClass(): string
|
||||
{
|
||||
return data_get([
|
||||
'default' => 'size-default',
|
||||
'sm' => 'size-sm',
|
||||
], $this->size);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use App\View\Traits\HasColors;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Action extends Component
|
||||
{
|
||||
|
||||
use HasColors;
|
||||
|
||||
public function __construct(
|
||||
public string $icon,
|
||||
public string $variant = 'primary'
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<a x-tooltip.raw="{{$slot}}" href="#" {{ $attributes->merge(['class' => 'inline-flex w-6 h-5 flex items-center justify-center rounded '.$allColors($variant)]) }}>
|
||||
<x-ui::sprite class="w-3 h-3 flex-none" :src="$icon"></x-ui::sprite>
|
||||
</a>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use App\View\Traits\HasColors;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Badge extends Component
|
||||
{
|
||||
|
||||
use HasColors;
|
||||
|
||||
public function __construct(
|
||||
public string $icon,
|
||||
public string $variant = 'primary'
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<button type="button" href="#" {{ $attributes->merge(['class' => 'h-6 px-3 space-x-2 items-center rounded-full hidden lg:flex '.$allColors($variant)]) }}>
|
||||
<x-ui::sprite class="w-3 h-3 flex-none" :src="$icon"></x-ui::sprite>
|
||||
<span class="text-sm">
|
||||
{{$slot}}
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" x-tooltip.raw="{{$slot}}" href="#" {{ $attributes }} class="h-6 px-3 space-x-2 flex items-center rounded-full {{ $allColors($variant) }} lg:hidden">
|
||||
<x-ui::sprite class="w-3 h-3 flex-none" :src="$icon"></x-ui::sprite>
|
||||
</button>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class BooleanDisplay extends Component
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public bool $value,
|
||||
public string $hint,
|
||||
public string $right,
|
||||
public string $wrong,
|
||||
public bool $dark = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function spriteClass(): string
|
||||
{
|
||||
return $this->value ? 'text-green-800 group-[.dark]:text-green-600' : 'text-red-800 group-[.dark]:text-red-600';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div x-tooltip.raw="{{$hint}}" {{ $attributes->merge(['class' => 'flex space-x-2 items-center group '.($dark ? 'dark' : '')]) }}">
|
||||
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center
|
||||
@if ($value) border-green-700 group-[.dark]:border-green-500
|
||||
@else border-red-700 group-[.dark]:border-red-500
|
||||
@endif
|
||||
"
|
||||
>
|
||||
<x-ui::sprite :src="$value ? 'check ' :'close'" class="w-3 h-3 flex-none {{$spriteClass}}"></x-ui::sprite>
|
||||
</div>
|
||||
<div class="text-gray-400 text-xs">{{ $value ? $right : $wrong }}</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Box extends Component
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public string $containerClass = '',
|
||||
public bool $second = false,
|
||||
public string $title = '',
|
||||
public string $inTitle = '',
|
||||
) {
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<section {!! $attributes
|
||||
->mergeWhen($second, 'class', 'bg-gray-700 group-[.is-popup]:bg-zinc-600')
|
||||
->mergeWhen(!$second, 'class', 'bg-gray-800 group-[.is-popup]:bg-zinc-700')
|
||||
->mergeWhen(true, 'class', 'p-3 rounded-lg flex flex-col')
|
||||
!!}>
|
||||
<div class="flex items-center">
|
||||
@if($title)
|
||||
<div class="col-span-full font-semibold text-gray-300 group-[.is-popup]:text-zinc-300">{{$title}}</div>
|
||||
@endif
|
||||
{{$inTitle}}
|
||||
</div>
|
||||
<main class="{{ $title ? 'mt-2' : '' }} {{ $containerClass }}">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</section>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Button extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $type = 'button')
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<button type="{{$type}}" {{ $attributes->merge(['class' => 'px-3 py-2 uppercase no-underline text-sm items-center justify-center bg-primary-700 rounded text-primary-300']) }}>
|
||||
{{$slot}}
|
||||
</button>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Errors extends Component
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public string $for,
|
||||
) {
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return 'errors-' . str_replace('.', '--', $this->for);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
@error($for)
|
||||
<div x-data="{error: ''}" x-init="window.setTimeout(() => document.querySelector('#{{$id()}}') ? document.querySelector('#{{$id()}}').remove() : null, 2000)" class="absolute bottom-[calc(100%+0.5rem)] right-0" id="{{$id}}">
|
||||
<div class="tippy-box" tabindex="-1" data-theme="danger" data-placement="top">
|
||||
<div class="tippy-content">{{$message}}</div>
|
||||
<div class="tippy-arrow absolute right-0 mr-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
@enderror
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Menulist extends Component
|
||||
{
|
||||
|
||||
public function __construct(public array $entries)
|
||||
{
|
||||
}
|
||||
|
||||
public function activeClass($entry): string
|
||||
{
|
||||
return $entry['is_active'] ? 'bg-gray-600' : '';
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div {{$attributes->merge(['class' => 'p-6 bg-gray-700 border-r border-gray-600 flex-none w-maxc flex flex-col justify-between']) }}">
|
||||
<div class="grid gap-1">
|
||||
@foreach($entries as $entry)
|
||||
<a href="{{$entry['url']}}" class="rounded py-1 px-3 text-gray-400 duration-200 hover:bg-gray-600 {{$activeClass($entry)}}" @if($entry['is_active']) data-active @endif>
|
||||
{{$entry['title']}}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
<slot name="bottom"></slot>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class SettingIntro extends Component
|
||||
{
|
||||
|
||||
public function __construct(public ?string $title = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div {{$attributes}}>
|
||||
@if ($title) <h2 class="text-lg font-semibold text-gray-300">{{$title}}</h2> @endif
|
||||
<div class="text-gray-100 text-sm">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Sprite extends Component
|
||||
{
|
||||
|
||||
public string $spritemapFile;
|
||||
|
||||
public function __construct(
|
||||
public string $src = '',
|
||||
) {
|
||||
$this->spritemapFile = Cache::rememberForever('spritemap_file', function () {
|
||||
$manifest = json_decode(file_get_contents(public_path('build/manifest.json')), true);
|
||||
return asset('build/' . $manifest['spritemap.svg']['file']);
|
||||
});
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<svg {{ $attributes->merge(['class' => 'fill-current']) }}><use xlink:href="{{$spritemapFile}}#sprite-{{$src}}" /></svg>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Ui;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Table extends Component
|
||||
{
|
||||
|
||||
public function __construct(public string $mode = 'dark')
|
||||
{
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div class="@container/table">
|
||||
<table cellpadding="0" cellspacing="0" border="0" class="w-full @if ($mode === 'dark') table-dark @else table-light @endif
|
||||
[&_th]:text-left [&_th]:px-2 [&_th]:@4xl/table:px-6 [&_th]:text-gray-200 [&_th]:font-semibold [&_th]:py-3 [&_th]:border-gray-600 [&_th]:border-b
|
||||
[&_tbody_tr]:text-gray-200 [&_tbody_tr]:duration-300 [&_tbody_tr]:rounded [&_tbody_tr:hover]:bg-gray-800
|
||||
[&_tr_td]:py-1 [&_tr_td]:px-2 [&_tr_td]:@4xl/table:px-6
|
||||
[&.table-light_th]:border-gray-500 [&.table-light_tbody_tr:hover]:bg-gray-700
|
||||
|
||||
|
||||
">
|
||||
{{ $slot }}
|
||||
</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@
|
|||
echo "drop database scoutrobot;" | sudo mysql
|
||||
echo "create database scoutrobot;" | sudo mysql
|
||||
|
||||
ssh -l stammsilva zoomyboy.de "cd /usr/share/webapps/nami_silva && docker compose exec db mysqldump -udb -p$SCOUTROBOT_DB_PASSWORD db" > db.tmp
|
||||
sudo mysql scoutrobot < db.tmp
|
||||
ssh -l stamm-silva zoomyboy.de "cd /usr/share/webapps/adrema_silva && docker compose exec db mysqldump -udb -p$SCOUTROBOT_DB_PASSWORD db" > db.tmp
|
||||
sudo mysql adrema < db.tmp
|
||||
rm db.tmp
|
||||
|
||||
echo 'app(\App\Form\FormSettings::class)->fill(["registerUrl" => "http://stammsilva.test/anmeldung/{slug}/register", "clearCacheUrl" => "http://stammsilva.test/adrema/clear-cache"])->save();' | php artisan tinker
|
||||
|
||||
exit 0
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"laravel/ui": "^4.0",
|
||||
"league/csv": "^9.9",
|
||||
"league/flysystem-webdav": "dev-master as 3.28.0",
|
||||
"livewire/livewire": "^3.5",
|
||||
"lorisleiva/laravel-actions": "^2.4",
|
||||
"meilisearch/meilisearch-php": "^1.6",
|
||||
"monicahq/laravel-sabre": "^1.6",
|
||||
|
@ -80,12 +81,13 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"larastan/larastan": "^2.0",
|
||||
"larastan/larastan": "^3.0",
|
||||
"laravel/envoy": "^2.8",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"orchestra/testbench": "^9.0",
|
||||
"pestphp/pest": "^3.0",
|
||||
"phpstan/phpstan-mockery": "^1.1"
|
||||
"phpstan/phpstan-mockery": "^2.0",
|
||||
"qossmic/deptrac": "^2.0"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
@ -104,6 +106,7 @@
|
|||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Modules\\": "modules/",
|
||||
"Plugins\\": "plugins/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
|
@ -111,6 +114,7 @@
|
|||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Dashboard\\Tests\\": "modules/dashboard/tests/",
|
||||
"Tests\\": "tests/",
|
||||
"Zoomyboy\\LaravelNami\\Tests\\": "packages/laravel-nami/tests/"
|
||||
}
|
||||
|
@ -118,6 +122,10 @@
|
|||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"scripts": {
|
||||
"archtest": [
|
||||
"./vendor/bin/deptrac analyze",
|
||||
"./vendor/bin/pest tests/Arch.php"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue