Compare commits
148 Commits
d692cf3bdf
...
44ea68cce7
Author | SHA1 | Date |
---|---|---|
|
44ea68cce7 | |
|
c0096c0794 | |
|
05bcb376c8 | |
|
913cabc7a3 | |
|
7cc1f349a1 | |
|
8d2f8ec03d | |
|
9144ae1028 | |
|
c3a2417cac | |
|
91943a6321 | |
|
6f03063cba | |
|
5d576cda6a | |
|
2a979d932e | |
|
80def4abde | |
|
64c2cb9408 | |
|
78d6c6d864 | |
|
a0842afd47 | |
|
43846d1703 | |
|
171dd51200 | |
|
c844f2143a | |
|
6ac539417f | |
|
56ad09c6fa | |
|
2f2e95cd23 | |
|
3d270ce9a4 | |
|
eaa880127d | |
|
6f334765bf | |
|
b806cafc8b | |
|
ada03a11ea | |
|
a12cb53274 | |
|
458c643615 | |
|
60b0052622 | |
|
0bc00207b2 | |
|
8cd2f74477 | |
|
9d11294fde | |
|
acec968963 | |
|
3cbb4768e8 | |
|
09e3bcab9e | |
|
66a76ad2b1 | |
|
5919082211 | |
|
2a5c203cc0 | |
|
5f48b4e64d | |
|
5f47729bb6 | |
|
c9cbf86626 | |
|
8713f941c1 | |
|
26ce8637c1 | |
|
ebf7a1d5c9 | |
|
b86b2b6063 | |
|
94397165c6 | |
|
9d7d039530 | |
|
34fb96dabe | |
|
f601167893 | |
|
54b650fb91 | |
|
f24064b9f3 | |
|
a96d879023 | |
|
b194e77966 | |
|
a830f366ad | |
|
974c73af71 | |
|
445590e060 | |
|
ef47480cc9 | |
|
35b269c647 | |
|
e508dd9e6f | |
|
891f0c21ad | |
|
a0bcd99642 | |
|
857051434e | |
|
c7b23df01e | |
|
7361dcccff | |
|
590696750b | |
|
48383b25da | |
|
de76e195e7 | |
|
ff6c2bbc2c | |
|
5d9d7a0ffc | |
|
89429f9812 | |
|
b813664632 | |
|
a7394e12aa | |
|
ddb44d64d6 | |
|
3d62970baf | |
|
9a3a0402d9 | |
|
665b43c797 | |
|
ee926467ed | |
|
4ffc14d503 | |
|
1edbdab8fd | |
|
6e6a4595cd | |
|
e3a0ca7673 | |
|
28c821eeaf | |
|
e45a59c5ff | |
|
d3c1e89a03 | |
|
6ede944d85 | |
|
b69c895921 | |
|
539e41cffe | |
|
bdf3fa54a9 | |
|
97fabf0f24 | |
|
e011b52534 | |
|
e005ed0d0d | |
|
ae19a6ed2b | |
|
4ce7d428d4 | |
|
e6d0a30c7f | |
|
d16f02c8b3 | |
|
888f4ee976 | |
|
2a6e3aac03 | |
|
7abf746897 | |
|
ed8e7b045d | |
|
2a2cbd196d | |
|
4690e0037f | |
|
1d28827353 | |
|
703084dbc1 | |
|
7aa445ca74 | |
|
a2d7a36fc1 | |
|
23c60d4330 | |
|
79f1c71dd9 | |
|
452416d0a4 | |
|
589b713907 | |
|
eb60852bbf | |
|
c3db854eb5 | |
|
5cf49489d5 | |
|
7d880c6e29 | |
|
743c041c35 | |
|
d3ad48ed30 | |
|
4776c0d71b | |
|
9355daee7e | |
|
89b5cbd4b5 | |
|
c8bc69ae30 | |
|
0fde33803b | |
|
1b5c83a414 | |
|
e0558e4b13 | |
|
b421f85d89 | |
|
0eb8928b22 | |
|
f1d4d3e428 | |
|
5b2a290b49 | |
|
f55c174019 | |
|
d597d5ea2b | |
|
d4c8fdff3f | |
|
b26935fc07 | |
|
4ddf76943d | |
|
d7e7fb3c27 | |
|
87ee15936d | |
|
6262ccdd28 | |
|
b109d40ebb | |
|
3c81dfe7db | |
|
cc52437568 | |
|
ac28466c8d | |
|
bde8d48807 | |
|
288533efd3 | |
|
104b04b639 | |
|
8bf0019c98 | |
|
9f85809195 | |
|
131690262e | |
|
edb1eeb0d8 | |
|
2ecd4b9643 | |
|
ae679641d7 |
|
@ -5,7 +5,7 @@ RUN composer install --ignore-platform-reqs --no-dev
|
|||
RUN php artisan telescope:publish
|
||||
RUN php artisan horizon:publish
|
||||
|
||||
FROM node:17.9.0-slim as node
|
||||
FROM node:18.13.0-slim as node
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN npm install && npm run prod && npm run img && rm -R node_modules
|
||||
|
|
|
@ -22,9 +22,9 @@ steps:
|
|||
- while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done
|
||||
|
||||
- name: node
|
||||
image: node:17.9.0-slim
|
||||
image: node:18.13.0-slim
|
||||
commands:
|
||||
- npm ci && npm run img && npm run prod && rm -R node_modules
|
||||
- npm ci && cd packages/adrema-plugin && npm ci && npm run build && cd ../../ && npm run img && npm run prod && rm -R node_modules
|
||||
|
||||
- name: tests
|
||||
image: zoomyboy/adrema-base:latest
|
||||
|
|
|
@ -7,3 +7,10 @@
|
|||
[submodule "packages/tex"]
|
||||
path = packages/tex
|
||||
url = https://git.zoomyboy.de/pille/tex
|
||||
[submodule "packages/adrema-form"]
|
||||
path = packages/adrema-form
|
||||
url = https://git.zoomyboy.de/silva/adrema-form.git
|
||||
[submodule "packages/medialibrary-helper"]
|
||||
path = packages/medialibrary-helper
|
||||
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
|
||||
branch = version2
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Scopes\FormFilterScope;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Resources\FormApiResource;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormApiListAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param string $filter
|
||||
* @return LengthAwarePaginator<Form>
|
||||
*/
|
||||
public function handle(string $filter, int $perPage): LengthAwarePaginator
|
||||
{
|
||||
return FormFilterScope::fromRequest($filter)->getQuery()->paginate($perPage);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): AnonymousResourceCollection
|
||||
{
|
||||
return FormApiResource::collection($this->handle(
|
||||
$request->input('filter', ''),
|
||||
$request->input('perPage', 10)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormDestroyAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(Form $form): void
|
||||
{
|
||||
$form->delete();
|
||||
|
||||
Succeeded::message('Veranstaltung gelöscht.')->dispatch();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Scopes\FormFilterScope;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Resources\FormResource;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormIndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<Form>
|
||||
*/
|
||||
public function handle(string $filter): LengthAwarePaginator
|
||||
{
|
||||
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): Response
|
||||
{
|
||||
session()->put('menu', 'form');
|
||||
session()->put('title', 'Veranstaltungen');
|
||||
|
||||
return Inertia::render('form/Index', [
|
||||
'data' => FormResource::collection($this->handle($request->input('filter', ''))),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
|
||||
class FormStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalRules(),
|
||||
'description.time' => 'required|integer',
|
||||
'description.blocks' => 'required|array',
|
||||
'description.version' => 'required|string',
|
||||
'excerpt' => 'required|string|max:130',
|
||||
'from' => 'required|date',
|
||||
'to' => 'required|date',
|
||||
'registration_from' => 'present|nullable|date',
|
||||
'registration_until' => 'present|nullable|date',
|
||||
'mail_top' => 'nullable|string',
|
||||
'mail_bottom' => 'nullable|string',
|
||||
'header_image' => 'required|exclude',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function handle(array $attributes): Form
|
||||
{
|
||||
return tap(
|
||||
Form::create($attributes),
|
||||
fn ($form) => $form->setDeferredUploads(request()->input('header_image'))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalValidationAttributes(),
|
||||
'from' => 'Start',
|
||||
'to' => 'Ende',
|
||||
'header_image' => 'Bild',
|
||||
'description.blocks' => 'Beschreibung',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($request->validated());
|
||||
|
||||
Succeeded::message('Veranstaltung gespeichert.')->dispatch();
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
|
||||
class FormUpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalRules(),
|
||||
'description' => 'required|array',
|
||||
'description.time' => 'required|integer',
|
||||
'description.blocks' => 'required|array',
|
||||
'description.version' => 'required|string',
|
||||
'excerpt' => 'required|string|max:130',
|
||||
'from' => 'required|date',
|
||||
'to' => 'required|date',
|
||||
'registration_from' => 'present|nullable|date',
|
||||
'registration_until' => 'present|nullable|date',
|
||||
'mail_top' => 'nullable|string',
|
||||
'mail_bottom' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function handle(Form $form, array $attributes): Form
|
||||
{
|
||||
$form->update($attributes);
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalValidationAttributes(),
|
||||
'from' => 'Start',
|
||||
'to' => 'Ende',
|
||||
'description.blocks' => 'Beschreibung',
|
||||
];
|
||||
}
|
||||
|
||||
public function asController(Form $form, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($form, $request->validated());
|
||||
|
||||
Succeeded::message('Veranstaltung aktualisiert.')->dispatch();
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
|
||||
class FormUpdateMetaAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Form */
|
||||
$form = request()->route('form');
|
||||
|
||||
return [
|
||||
'sorting' => 'array',
|
||||
'sorting.0' => 'required|string',
|
||||
'sorting.1' => 'required|string|in:asc,desc',
|
||||
'active_columns' => 'array',
|
||||
'active_columns.*' => ['string', Rule::in($form->getFields()->pluck('key')->toArray())]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(Form $form, array $input): void
|
||||
{
|
||||
$form->update(['meta' => $input]);
|
||||
}
|
||||
|
||||
public function asController(Form $form, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($form, $request->validated());
|
||||
|
||||
return response()->json($form->fresh()->meta);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormtemplateDestroyAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Formtemplate $formtemplate): void
|
||||
{
|
||||
$formtemplate->delete();
|
||||
}
|
||||
|
||||
public function asController(Formtemplate $formtemplate): JsonResponse
|
||||
{
|
||||
$this->handle($formtemplate);
|
||||
|
||||
Succeeded::message('Vorlage gelöscht.')->dispatch();
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Form\Resources\FormtemplateResource;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormtemplateIndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<Formtemplate>
|
||||
*/
|
||||
public function handle(): LengthAwarePaginator
|
||||
{
|
||||
return Formtemplate::paginate(15);
|
||||
}
|
||||
|
||||
public function asController(): Response
|
||||
{
|
||||
session()->put('menu', 'form');
|
||||
session()->put('title', 'Formular-Vorlagen');
|
||||
|
||||
return Inertia::render('formtemplate/Index', [
|
||||
'data' => FormtemplateResource::collection($this->handle()),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormtemplateStoreAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalRules(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalValidationAttributes(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function handle(array $attributes): Formtemplate
|
||||
{
|
||||
return Formtemplate::create($attributes);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($request->validated());
|
||||
|
||||
Succeeded::message('Vorlage gespeichert.')->dispatch();
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormtemplateUpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
use HasValidation;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalRules(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
...$this->globalValidationAttributes(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function handle(Formtemplate $formtemplate, array $attributes): void
|
||||
{
|
||||
$formtemplate->update($attributes);
|
||||
}
|
||||
|
||||
public function asController(Formtemplate $formtemplate, ActionRequest $request): JsonResponse
|
||||
{
|
||||
$this->handle($formtemplate, $request->validated());
|
||||
|
||||
Succeeded::message('Vorlage aktualisiert.')->dispatch();
|
||||
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Form\Fields\Field;
|
||||
use Illuminate\Validation\Validator;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
|
||||
trait HasValidation
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function globalRules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'config' => 'array',
|
||||
'config.sections.*.name' => 'required',
|
||||
'config.sections.*.fields' => 'array',
|
||||
'config.sections.*.fields.*.name' => 'required|string',
|
||||
'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))],
|
||||
'config.sections.*.fields.*.key' => ['required', 'string', 'regex:/^[a-zA-Z_]*$/'],
|
||||
'config.sections.*.fields.*.columns' => 'required|array',
|
||||
'config.sections.*.fields.*.*' => '',
|
||||
'config.sections.*.fields.*.columns.mobile' => 'required|numeric|gt:0|lte:2',
|
||||
'config.sections.*.fields.*.columns.tablet' => 'required|numeric|gt:0|lte:4',
|
||||
'config.sections.*.fields.*.columns.desktop' => 'required|numeric|gt:0|lte:6',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function globalValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'config.sections.*.name' => 'Sektionsname',
|
||||
'config.sections.*.fields.*.name' => 'Feldname',
|
||||
'config.sections.*.fields.*.type' => 'Feldtyp',
|
||||
'config.sections.*.fields.*.key' => 'Feldkey',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator, ActionRequest $request): void
|
||||
{
|
||||
if (!$validator->passes()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($request->input('config.sections') as $sindex => $section) {
|
||||
foreach (data_get($section, 'fields') as $findex => $field) {
|
||||
$fieldClass = Field::classFromType($field['type']);
|
||||
if (!$fieldClass) {
|
||||
continue;
|
||||
}
|
||||
foreach ($fieldClass::metaRules() as $fieldName => $rules) {
|
||||
$validator->addRules(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $rules]);
|
||||
}
|
||||
foreach ($fieldClass::metaAttributes() as $fieldName => $attribute) {
|
||||
$validator->addCustomAttributes(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $attribute]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Resources\ParticipantResource;
|
||||
use App\Form\Scopes\ParticipantFilterScope;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ParticipantIndexAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<Participant>
|
||||
*/
|
||||
public function handle(Form $form, ParticipantFilterScope $filter): LengthAwarePaginator
|
||||
{
|
||||
return $form->participants()->withFilter($filter)->with('form')->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(Form $form): AnonymousResourceCollection
|
||||
{
|
||||
$filter = ParticipantFilterScope::fromRequest(request()->input('filter'));
|
||||
return ParticipantResource::collection($this->handle($form, $filter))
|
||||
->additional(['meta' => ParticipantResource::meta($form)]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Data\FieldCollection;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class RegisterAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public function handle(Form $form, array $input): Participant
|
||||
{
|
||||
$memberQuery = FieldCollection::fromRequest($form, $input)
|
||||
->withNamiType()
|
||||
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
|
||||
$mglnr = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first()->mitgliedsnr : null;
|
||||
|
||||
$participant = $form->participants()->create([
|
||||
'data' => $input,
|
||||
'mitgliedsnr' => $mglnr,
|
||||
]);
|
||||
|
||||
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
|
||||
|
||||
$participant->sendConfirmationMail();
|
||||
|
||||
return $participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
/** @var Form */
|
||||
$form = request()->route('form');
|
||||
|
||||
return $form->getRegistrationRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationAttributes(): array
|
||||
{
|
||||
/** @var Form */
|
||||
$form = request()->route('form');
|
||||
|
||||
return $form->getRegistrationAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValidationMessages(): array
|
||||
{
|
||||
/** @var Form */
|
||||
$form = request()->route('form');
|
||||
|
||||
return $form->getRegistrationMessages();
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request, Form $form): JsonResponse
|
||||
{
|
||||
$participant = $this->handle($form, $request->validated());
|
||||
|
||||
return response()->json($participant);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Casts;
|
||||
|
||||
use App\Form\Fields\Field;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Casts\Cast;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Support\DataProperty;
|
||||
|
||||
class CollectionCast implements Cast
|
||||
{
|
||||
|
||||
/**
|
||||
* @param class-string<Data> $target
|
||||
*/
|
||||
public function __construct(public string $target)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $value
|
||||
* @param array<string, mixed> $context
|
||||
* @return Collection<int, Data>
|
||||
*/
|
||||
public function cast(DataProperty $property, mixed $value, array $context): mixed
|
||||
{
|
||||
return collect($value)->map(fn ($item) => $this->target::from($item));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Casts;
|
||||
|
||||
use App\Form\Data\FieldCollection;
|
||||
use App\Form\Fields\Field;
|
||||
use Spatie\LaravelData\Casts\Cast;
|
||||
use Spatie\LaravelData\Support\DataProperty;
|
||||
|
||||
class FieldCollectionCast implements Cast
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $value
|
||||
* @param array<string, mixed> $context
|
||||
* @return FieldCollection
|
||||
*/
|
||||
public function cast(DataProperty $property, mixed $value, array $context): mixed
|
||||
{
|
||||
return new FieldCollection(collect($value)->map(fn ($value) => Field::classFromType($value['type'])::from($value))->all());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Data;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class ColumnData extends Data
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public int $mobile,
|
||||
public int $tablet,
|
||||
public int $desktop,
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Data;
|
||||
|
||||
use App\Form\Enums\SpecialType;
|
||||
use App\Form\Fields\Field;
|
||||
use App\Form\Fields\NamiField;
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Support\Collection;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* @extends Collection<int, Field>
|
||||
*/
|
||||
class FieldCollection extends Collection
|
||||
{
|
||||
|
||||
public function forMembers(): self
|
||||
{
|
||||
return $this->filter(fn ($field) => $field->forMembers === true);
|
||||
}
|
||||
|
||||
public function withNamiType(): self
|
||||
{
|
||||
return $this->filter(fn ($field) => $field->namiType !== null);
|
||||
}
|
||||
|
||||
public function noNamiType(): self
|
||||
{
|
||||
return $this->filter(fn ($field) => $field->namiType === null);
|
||||
}
|
||||
|
||||
public function noNamiField(): self
|
||||
{
|
||||
return $this->filter(fn ($field) => !is_a($field, NamiField::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return stdClass
|
||||
*/
|
||||
public function getMailRecipient(): ?stdClass
|
||||
{
|
||||
$email = $this->findBySpecialType(SpecialType::EMAIL)?->value;
|
||||
|
||||
return $this->getFullname() && $email
|
||||
? (object) [
|
||||
'name' => $this->getFullname(),
|
||||
"email" => $email,
|
||||
] : null;
|
||||
}
|
||||
|
||||
public function getFullname(): ?string
|
||||
{
|
||||
$firstname = $this->findBySpecialType(SpecialType::FIRSTNAME)?->value;
|
||||
$lastname = $this->findBySpecialType(SpecialType::LASTNAME)?->value;
|
||||
|
||||
return $firstname && $lastname ? "$firstname $lastname" : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
public static function fromRequest(Form $form, array $input): self
|
||||
{
|
||||
return $form->getFields()->each(fn ($field) => $field->value = array_key_exists($field->key, $input) ? $input[$field->key] : $field->default());
|
||||
}
|
||||
|
||||
public function find(Field $givenField): ?Field
|
||||
{
|
||||
return $this->first(fn ($field) => $field->key === $givenField->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function present(): array
|
||||
{
|
||||
$attributes = collect([]);
|
||||
|
||||
foreach ($this as $field) {
|
||||
$attributes = $attributes->merge($field->present());
|
||||
}
|
||||
|
||||
return $attributes->toArray();
|
||||
}
|
||||
|
||||
private function findBySpecialType(SpecialType $specialType): ?Field
|
||||
{
|
||||
return $this->first(fn ($field) => $field->specialType === $specialType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Data;
|
||||
|
||||
use App\Form\Casts\CollectionCast;
|
||||
use App\Form\Transformers\CollectionTransformer;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Attributes\WithTransformer;
|
||||
|
||||
class FormConfigData extends Data
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Collection<int, SectionData> $sections
|
||||
*/
|
||||
public function __construct(
|
||||
#[WithCast(CollectionCast::class, target: SectionData::class)]
|
||||
#[WithTransformer(CollectionTransformer::class, target: SectionData::class)]
|
||||
public Collection $sections
|
||||
) {
|
||||
}
|
||||
|
||||
public function fields(): FieldCollection
|
||||
{
|
||||
return $this->sections->reduce(
|
||||
fn ($carry, $current) => $carry->merge($current->fields->all()),
|
||||
new FieldCollection([])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Data;
|
||||
|
||||
use App\Form\Casts\FieldCollectionCast;
|
||||
use Spatie\LaravelData\Data;
|
||||
use App\Form\Transformers\FieldCollectionTransformer;
|
||||
use Spatie\LaravelData\Attributes\WithCast;
|
||||
use Spatie\LaravelData\Attributes\WithTransformer;
|
||||
|
||||
class SectionData extends Data
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
#[WithCast(FieldCollectionCast::class)]
|
||||
#[WithTransformer(FieldCollectionTransformer::class)]
|
||||
public FieldCollection $fields
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Enums;
|
||||
|
||||
use App\Group\Enums\Level;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
enum NamiType: string
|
||||
{
|
||||
case FIRSTNAME = 'Vorname';
|
||||
case LASTNAME = 'Nachname';
|
||||
case BIRTHDAY = 'Geburtstag';
|
||||
case REGION = 'Bezirk';
|
||||
case STAMM = 'Stamm';
|
||||
case EMAIL = 'E-Mail-Adresse';
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, id: string}>
|
||||
*/
|
||||
public static function forSelect(): array
|
||||
{
|
||||
return collect(static::cases())
|
||||
->map(fn ($case) => ['id' => $case->value, 'name' => $case->value])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getMemberAttribute(Member $member): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
static::FIRSTNAME => $member->firstname,
|
||||
static::LASTNAME => $member->lastname,
|
||||
static::BIRTHDAY => $member->birthday?->format('Y-m-d'),
|
||||
static::REGION => $this->matchRegion($member),
|
||||
static::STAMM => $this->matchGroup($member),
|
||||
static::EMAIL => $member->email,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Member> $query
|
||||
* @return Builder<Member>
|
||||
*/
|
||||
public function performQuery(Builder $query, mixed $value): Builder
|
||||
{
|
||||
return match ($this) {
|
||||
static::FIRSTNAME => $query->where('firstname', $value),
|
||||
static::LASTNAME => $query->where('lastname', $value),
|
||||
static::BIRTHDAY => $query->where('birthday', $value),
|
||||
static::REGION => $query,
|
||||
static::STAMM => $query,
|
||||
static::EMAIL => $query->where('email', $value),
|
||||
};
|
||||
}
|
||||
|
||||
protected function matchGroup(Member $member): ?int
|
||||
{
|
||||
if ($member->group->level === Level::GROUP) {
|
||||
return $member->group_id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function matchRegion(Member $member): ?int
|
||||
{
|
||||
if ($member->group->parent?->level === Level::REGION) {
|
||||
return $member->group->parent->id;
|
||||
}
|
||||
|
||||
if ($member->group->level === Level::REGION) {
|
||||
return $member->group_id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Enums;
|
||||
|
||||
enum SpecialType: string
|
||||
{
|
||||
case FIRSTNAME = 'Vorname';
|
||||
case LASTNAME = 'Nachname';
|
||||
case EMAIL = 'E-Mail-Adresse';
|
||||
|
||||
/**
|
||||
* @return array<int, array{name: string, id: string}>
|
||||
*/
|
||||
public static function forSelect(): array
|
||||
{
|
||||
return collect(static::cases())
|
||||
->map(fn ($case) => ['id' => $case->value, 'name' => $case->value])
|
||||
->toArray();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CheckboxField extends Field
|
||||
{
|
||||
public bool $required;
|
||||
public string $description;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Checkbox';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'description', 'default' => '', 'rules' => ['description' => 'required|string'], 'label' => 'Beschreibung'],
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'description' => $faker->text(),
|
||||
'required' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->required ? ['boolean', 'accepted'] : ['present', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\EnumPresenter;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CheckboxesField extends Field
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Checkboxes';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'options', 'default' => [], 'rules' => ['options' => 'array', 'options.*' => 'required|string'], 'label' => 'Optionen'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'options' => $faker->words(4),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => 'array',
|
||||
$this->key . '.*' => ['string', Rule::in($this->options)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [
|
||||
...collect($this->options)->mapWithKeys(fn ($option, $key) => [$this->key . '.' . $key => $this->name])->toArray(),
|
||||
$this->key => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getPresenter(): Presenter
|
||||
{
|
||||
return app(EnumPresenter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Contracts\Displayable;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\DatePresenter;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use Carbon\Carbon;
|
||||
use Faker\Generator;
|
||||
|
||||
class DateField extends Field
|
||||
{
|
||||
|
||||
public bool $required;
|
||||
public bool $maxToday;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Datum';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
['key' => 'max_today', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Nur daten bis heute erlauben'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'required' => $faker->boolean(),
|
||||
'max_today' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
$rules = [$this->required ? 'required' : 'nullable'];
|
||||
|
||||
$rules[] = 'date';
|
||||
|
||||
if ($this->maxToday) {
|
||||
$rules[] = 'before_or_equal:' . now()->format('Y-m-d');
|
||||
}
|
||||
|
||||
return [$this->key => $rules];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key . '.before_or_equal' => $this->name . ' muss ein Datum vor oder gleich dem ' . now()->format('d.m.Y') . ' sein.',
|
||||
];
|
||||
}
|
||||
|
||||
public function getPresenter(): Presenter
|
||||
{
|
||||
return app(DatePresenter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class DropdownField extends Field
|
||||
{
|
||||
public bool $required;
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Dropdown';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'options', 'default' => [], 'rules' => ['options' => 'present|array', 'options.*' => 'string'], 'label' => 'Optionen'],
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'options' => $faker->words(4),
|
||||
'required' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->required ? ['required', 'string', Rule::in($this->options)] : ['nullable', 'string', Rule::in($this->options)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Data\ColumnData;
|
||||
use App\Form\Enums\NamiType;
|
||||
use App\Form\Enums\SpecialType;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\DefaultPresenter;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use Faker\Generator;
|
||||
use Spatie\LaravelData\Data;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
abstract class Field extends Data
|
||||
{
|
||||
|
||||
public string $key;
|
||||
public string $name;
|
||||
public ?NamiType $namiType = null;
|
||||
public ColumnData $columns;
|
||||
public bool $forMembers;
|
||||
public ?SpecialType $specialType = null;
|
||||
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $input
|
||||
*/
|
||||
abstract public function afterRegistration(Form $form, Participant $participant, array $input): void;
|
||||
|
||||
abstract public static function name(): string;
|
||||
|
||||
/** @return array<int, array{key: string, default: mixed, label: string, rules: array<string, mixed>}> */
|
||||
abstract public static function meta(): array;
|
||||
|
||||
/** @return mixed */
|
||||
abstract public static function default();
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
abstract public function getRegistrationRules(Form $form): array;
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
abstract public function getRegistrationAttributes(Form $form): array;
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
abstract public function getRegistrationMessages(Form $form): array;
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
abstract public static function fake(Generator $faker): array;
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function asMeta(): array
|
||||
{
|
||||
return array_map(fn ($class) => $class::allMeta(), self::classNames());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, class-string<self>>
|
||||
*/
|
||||
private static function classNames(): array
|
||||
{
|
||||
return collect(glob(base_path('app/Form/Fields/*.php')))
|
||||
->filter(fn ($fieldClass) => preg_match('/[A-Za-z]Field\.php$/', $fieldClass) === 1)
|
||||
->map(fn ($fieldClass) => str($fieldClass)->replace(base_path(''), '')->replace('/app', '/App')->replace('.php', '')->replace('/', '\\')->toString())
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function classFromType(string $type): ?string
|
||||
{
|
||||
/** @var class-string<Field> */
|
||||
$fieldClass = '\\App\\Form\\Fields\\' . $type;
|
||||
if (!class_exists($fieldClass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $fieldClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function present()
|
||||
{
|
||||
return [
|
||||
$this->key => $this->value,
|
||||
$this->getDisplayAttribute() => $this->presentRaw(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function presentRaw()
|
||||
{
|
||||
return $this->getPresenter()->present($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function metaAttributes(): array
|
||||
{
|
||||
return collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['label']])->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
**/
|
||||
public static function metaRules(): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (static::meta() as $meta) {
|
||||
foreach ($meta['rules'] as $fieldName => $rules) {
|
||||
$result[$fieldName] = $rules;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function type(): string
|
||||
{
|
||||
return class_basename(static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function allMeta(): array
|
||||
{
|
||||
return [
|
||||
'id' => static::type(),
|
||||
'name' => static::name(),
|
||||
'default' => [
|
||||
'name' => '',
|
||||
'type' => static::type(),
|
||||
'columns' => ['mobile' => 2, 'tablet' => 4, 'desktop' => 6],
|
||||
'value' => static::default(),
|
||||
'required' => false,
|
||||
'nami_type' => null,
|
||||
'for_members' => true,
|
||||
'special_type' => null,
|
||||
...collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['default']])->toArray(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getPresenter(): Presenter
|
||||
{
|
||||
return app(DefaultPresenter::class);
|
||||
}
|
||||
|
||||
public function getDisplayAttribute(): string
|
||||
{
|
||||
return $this->key . '_display';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\GroupPresenter;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use App\Group;
|
||||
use App\Group\Enums\Level;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class GroupField extends Field
|
||||
{
|
||||
public bool $required;
|
||||
public ?string $parentField = null;
|
||||
public ?int $parentGroup = null;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Gruppierungs-Auswahl';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
['key' => 'parent_field', 'default' => null, 'rules' => ['parent_field' => 'present|nullable|string'], 'label' => 'Übergeordnetes Feld'],
|
||||
['key' => 'parent_group', 'default' => null, 'rules' => ['parent_group' => ['present', 'nullable', Rule::in(Group::pluck('id')->toArray())]], 'label' => 'Übergeordnete Gruppierung'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'required' => $faker->boolean(),
|
||||
'parent_field' => null,
|
||||
'parent_group' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
|
||||
$rules = [$this->required ? 'required' : 'nullable'];
|
||||
|
||||
$rules[] = 'integer';
|
||||
|
||||
if ($this->parentGroup) {
|
||||
$rules[] = Rule::in(Group::find($this->parentGroup)->children()->pluck('id'));
|
||||
}
|
||||
|
||||
if ($this->parentField && request()->input($this->parentField)) {
|
||||
$rules[] = Rule::in(Group::find(request()->input($this->parentField))->children()->pluck('id'));
|
||||
}
|
||||
|
||||
return [$this->key => $rules];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getPresenter(): Presenter
|
||||
{
|
||||
return app(GroupPresenter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Data\FieldCollection;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\NamiPresenter;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use App\Member\Member;
|
||||
use Faker\Generator;
|
||||
|
||||
class NamiField extends Field
|
||||
{
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'NaMi-Mitglieder';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function default(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
$rules = [$this->key => 'present|array'];
|
||||
|
||||
$c = $form->getFields()->forMembers()->noNamiType()->noNamiField()
|
||||
->map(fn ($field) => $field->getRegistrationRules($form))
|
||||
->toArray();
|
||||
|
||||
foreach ($c as $field) {
|
||||
foreach ($field as $ruleKey => $rule) {
|
||||
$rules[$this->key . '.*.' . $ruleKey] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$this->key . '.*.id' => ['required', 'numeric', 'exists:members,mitgliedsnr'],
|
||||
...$rules,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
$rules = [];
|
||||
$inputMembers = request($this->key);
|
||||
|
||||
if (!is_array($inputMembers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$c = $form->getFields()->noNamiField()->forMembers();
|
||||
|
||||
foreach ($c as $field) {
|
||||
foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) {
|
||||
foreach ($inputMembers as $memberIndex => $inputMember) {
|
||||
|
||||
$message = $field->name . ' für ein Mitglied';
|
||||
$rules = array_merge(
|
||||
$rules,
|
||||
str($ruleKey)->contains('*')
|
||||
? collect(request($this->key . '.' . $memberIndex . '.' . $field->key))
|
||||
->mapWithKeys(fn ($value, $key) => [$this->key . '.' . $memberIndex . '.' . str($ruleKey)->replace('*', $key) => $message])
|
||||
->toArray()
|
||||
: [$this->key . '.' . $memberIndex . '.' . $ruleKey => $message]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($inputMembers as $memberIndex => $inputMember) {
|
||||
$rules[$this->key . '.' . $memberIndex . '.id'] = 'Mitglied Nr ' . $inputMember['id'];
|
||||
}
|
||||
|
||||
return [
|
||||
$this->key => $this->name,
|
||||
...$rules,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
foreach ($input[$this->key] as $memberData) {
|
||||
$member = Member::firstWhere(['mitgliedsnr' => $memberData['id']]);
|
||||
$data = [];
|
||||
foreach (FieldCollection::fromRequest($form, $memberData) as $field) {
|
||||
$data[$field->key] = $field->namiType === null
|
||||
? $field->value
|
||||
: $field->namiType->getMemberAttribute($member);
|
||||
}
|
||||
|
||||
$data[$this->key] = [];
|
||||
$form->participants()->create(['data' => $data, 'mitgliedsnr' => $memberData['id'], 'parent_id' => $participant->id]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPresenter(): Presenter
|
||||
{
|
||||
return app(NamiPresenter::class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Presenters\Presenter;
|
||||
use Faker\Generator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class RadioField extends Field
|
||||
{
|
||||
public bool $required;
|
||||
/** @var array<int, string> */
|
||||
public array $options;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Radio';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'options', 'default' => [], 'rules' => ['options' => 'present|array', 'options.*' => 'required|string'], 'label' => 'Optionen'],
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'options' => $faker->words(4),
|
||||
'required' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [
|
||||
$this->key => $this->required ? ['required', 'string', Rule::in($this->options)] : ['nullable', 'string', Rule::in($this->options)],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Faker\Generator;
|
||||
|
||||
class TextField extends Field
|
||||
{
|
||||
|
||||
public bool $required;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Text';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'required' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->required ? ['required', 'string'] : ['nullable', 'string']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Fields;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Faker\Generator;
|
||||
|
||||
class TextareaField extends Field
|
||||
{
|
||||
public bool $required;
|
||||
public int $rows;
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Textarea';
|
||||
}
|
||||
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
['key' => 'rows', 'default' => 5, 'rules' => ['rows' => 'present|integer|gt:0'], 'label' => 'Zeilen'],
|
||||
['key' => 'required', 'default' => false, 'rules' => ['required' => 'present|boolean'], 'label' => 'Erforderlich'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function default(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public static function fake(Generator $faker): array
|
||||
{
|
||||
return [
|
||||
'rows' => $faker->numberBetween(5, 10),
|
||||
'required' => $faker->boolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationRules(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->required ? ['required', 'string'] : ['nullable', 'string']];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationAttributes(Form $form): array
|
||||
{
|
||||
return [$this->key => $this->name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function getRegistrationMessages(Form $form): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function afterRegistration(Form $form, Participant $participant, array $input): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Mails;
|
||||
|
||||
use App\Form\Data\FormConfigData;
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ConfirmRegistrationMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public string $fullname;
|
||||
public FormConfigData $config;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(public Participant $participant)
|
||||
{
|
||||
$this->fullname = $participant->getFields()->getFullname();
|
||||
$this->config = $participant->getConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return \Illuminate\Mail\Mailables\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'mail.form.confirm-registration',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Models;
|
||||
|
||||
use App\Form\Data\FieldCollection;
|
||||
use App\Form\Data\FormConfigData;
|
||||
use App\Form\Fields\Field;
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Image\Manipulations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Zoomyboy\MedialibraryHelper\DefersUploads;
|
||||
|
||||
class Form extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory;
|
||||
use Sluggable;
|
||||
use InteractsWithMedia;
|
||||
use DefersUploads;
|
||||
use Searchable;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $casts = [
|
||||
'config' => FormConfigData::class,
|
||||
'meta' => 'json',
|
||||
'description' => 'json',
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public $dates = ['from', 'to', 'registration_from', 'registration_until'];
|
||||
|
||||
/**
|
||||
* @return SluggableConfig
|
||||
*/
|
||||
public function sluggable(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['source' => ['name']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Participant>
|
||||
*/
|
||||
public function participants(): HasMany
|
||||
{
|
||||
return $this->hasMany(Participant::class);
|
||||
}
|
||||
|
||||
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('headerImage')
|
||||
->singleFile()
|
||||
->maxWidth(fn () => 500)
|
||||
->forceFileName(fn (Form $model, string $name) => $model->slug)
|
||||
->registerMediaConversions(function (Media $media) {
|
||||
$this->addMediaConversion('square')->fit(Manipulations::FIT_CROP, 400, 400);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getRegistrationRules(): array
|
||||
{
|
||||
return $this->getFields()->reduce(fn ($carry, $field) => [
|
||||
...$carry,
|
||||
...$field->getRegistrationRules($this),
|
||||
], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getRegistrationMessages(): array
|
||||
{
|
||||
return $this->getFields()->reduce(fn ($carry, $field) => [
|
||||
...$carry,
|
||||
...$field->getRegistrationMessages($this),
|
||||
], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getRegistrationAttributes(): array
|
||||
{
|
||||
return $this->getFields()->reduce(fn ($carry, $field) => [
|
||||
...$carry,
|
||||
...$field->getRegistrationAttributes($this),
|
||||
], []);
|
||||
}
|
||||
|
||||
public function getFields(): FieldCollection
|
||||
{
|
||||
return $this->config->fields();
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------- Searching ---------------------------------
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'from' => $this->from->timestamp,
|
||||
'to' => $this->to->timestamp,
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
public static function booted(): void
|
||||
{
|
||||
static::saving(function (self $model) {
|
||||
if (is_null($model->meta)) {
|
||||
$model->setAttribute('meta', [
|
||||
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
|
||||
'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
if (is_array(data_get($model->meta, 'active_columns'))) {
|
||||
$model->setAttribute('meta', [
|
||||
...$model->meta,
|
||||
'active_columns' => array_values(array_intersect($model->getFields()->pluck('key')->toArray(), $model->meta['active_columns'])),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Models;
|
||||
|
||||
use App\Form\Data\FormConfigData;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property FormConfigData $config
|
||||
*/
|
||||
class Formtemplate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $casts = [
|
||||
'config' => FormConfigData::class,
|
||||
];
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Models;
|
||||
|
||||
use App\Form\Data\FieldCollection;
|
||||
use App\Form\Data\FormConfigData;
|
||||
use App\Form\Mails\ConfirmRegistrationMail;
|
||||
use App\Form\Scopes\ParticipantFilterScope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class Participant extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $guarded = [];
|
||||
|
||||
public $casts = [
|
||||
'data' => 'json',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Form, self>
|
||||
*/
|
||||
public function form(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Form::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<self>
|
||||
*/
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<self> $query
|
||||
* @return Builder<self>
|
||||
*/
|
||||
public function scopeWithFilter(Builder $query, ParticipantFilterScope $filter): Builder
|
||||
{
|
||||
return $filter->apply($query);
|
||||
}
|
||||
|
||||
public function getFields(): FieldCollection
|
||||
{
|
||||
return FieldCollection::fromRequest($this->form, $this->data);
|
||||
}
|
||||
|
||||
public function getConfig(): FormConfigData
|
||||
{
|
||||
return tap($this->form->config, function ($config) {
|
||||
$config->sections->each(function ($section) {
|
||||
$section->fields->each(function ($field) {
|
||||
$field->value = $this->getFields()->find($field)->value;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function sendConfirmationMail(): void
|
||||
{
|
||||
if (!$this->getFields()->getMailRecipient()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mail::to($this->getFields()->getMailRecipient())->queue(new ConfirmRegistrationMail($this));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Policies;
|
||||
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
|
||||
class FormPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function listMedia(User $user, HasMedia $model): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function storeMedia(User $user, ?HasMedia $model, ?string $collection = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateMedia(User $user, HasMedia $model, string $collection): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroyMedia(User $user, HasMedia $model, string $collection): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class DatePresenter extends Presenter
|
||||
{
|
||||
|
||||
public function present($value): string
|
||||
{
|
||||
return $value ? Carbon::parse($value)->format('d.m.Y') : '';
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
class DefaultPresenter extends Presenter
|
||||
{
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function present($value): string
|
||||
{
|
||||
return ((string) $value) ?: '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
class EnumPresenter extends Presenter
|
||||
{
|
||||
|
||||
/**
|
||||
* @param ?array<int, string> $value
|
||||
*/
|
||||
public function present($value): string
|
||||
{
|
||||
return is_array($value)
|
||||
? implode(', ', $value)
|
||||
: '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
use App\Group;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class GroupPresenter extends Presenter
|
||||
{
|
||||
|
||||
/**
|
||||
* @param ?int $value
|
||||
*/
|
||||
public function present($value): string
|
||||
{
|
||||
if (!$value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return Group::find($value)?->display() ?: '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
class NamiPresenter extends Presenter
|
||||
{
|
||||
|
||||
/**
|
||||
* @param ?array<int, array{id: int}> $value
|
||||
*/
|
||||
public function present($value): string
|
||||
{
|
||||
return collect(array_column($value, 'id'))->implode(', ');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Presenters;
|
||||
|
||||
abstract class Presenter
|
||||
{
|
||||
/** @var mixed */
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
abstract public function present($value): string;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Resources;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\HasMeta;
|
||||
use App\Subactivity;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Form
|
||||
*/
|
||||
class FormApiResource extends JsonResource
|
||||
{
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'from_human' => $this->from->format('d.m.Y'),
|
||||
'to_human' => $this->to->format('d.m.Y'),
|
||||
'excerpt' => $this->excerpt,
|
||||
'description' => $this->description,
|
||||
'config' => $this->config,
|
||||
'slug' => $this->slug,
|
||||
'dates' => $this->from->equalTo($this->to) ? $this->from->format('d.m.Y') : $this->from->format('d.m.Y') . ' - ' . $this->to->format('d.m.Y'),
|
||||
'image' => $this->getMedia('headerImage')->first()->getFullUrl('square'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'base_url' => url(''),
|
||||
'agegroups' => Subactivity::remote()->where('is_age_group', true)->get()
|
||||
->map(fn ($subactivity) => ['id' => $subactivity->nami_id, 'name' => $subactivity->name]),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Resources;
|
||||
|
||||
use App\Form\Enums\NamiType;
|
||||
use App\Form\Enums\SpecialType;
|
||||
use App\Form\Fields\Field;
|
||||
use App\Form\Scopes\FormFilterScope;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Group;
|
||||
use App\Lib\HasMeta;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Form
|
||||
*/
|
||||
class FormResource extends JsonResource
|
||||
{
|
||||
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'from_human' => $this->from->format('d.m.Y'),
|
||||
'to_human' => $this->to->format('d.m.Y'),
|
||||
'from' => $this->from->format('Y-m-d'),
|
||||
'to' => $this->to->format('Y-m-d'),
|
||||
'excerpt' => $this->excerpt,
|
||||
'description' => $this->description,
|
||||
'mail_top' => $this->mail_top,
|
||||
'mail_bottom' => $this->mail_bottom,
|
||||
'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'),
|
||||
'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'),
|
||||
'config' => $this->config,
|
||||
'participants_count' => $this->participants_count,
|
||||
'links' => [
|
||||
'participant_index' => route('form.participant.index', ['form' => $this->getModel()]),
|
||||
'update' => route('form.update', ['form' => $this->getModel()]),
|
||||
'destroy' => route('form.destroy', ['form' => $this->getModel()]),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'base_url' => url(''),
|
||||
'groups' => Group::forSelect(),
|
||||
'fields' => Field::asMeta(),
|
||||
'filter' => FormFilterScope::fromRequest(request()->input('filter', '')),
|
||||
'links' => [
|
||||
'store' => route('form.store'),
|
||||
'formtemplate_index' => route('formtemplate.index'),
|
||||
],
|
||||
'templates' => FormtemplateResource::collection(Formtemplate::get()),
|
||||
'namiTypes' => NamiType::forSelect(),
|
||||
'specialTypes' => SpecialType::forSelect(),
|
||||
'default' => [
|
||||
'description' => [],
|
||||
'name' => '',
|
||||
'excerpt' => '',
|
||||
'from' => null,
|
||||
'to' => null,
|
||||
'registration_from' => null,
|
||||
'registration_until' => null,
|
||||
'mail_top' => null,
|
||||
'mail_bottom' => null,
|
||||
'config' => null,
|
||||
'header_image' => null,
|
||||
'id' => null,
|
||||
],
|
||||
'section_default' => [
|
||||
'name' => '',
|
||||
'intro' => '',
|
||||
'fields' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Resources;
|
||||
|
||||
use App\Form\Enums\NamiType;
|
||||
use App\Form\Enums\SpecialType;
|
||||
use App\Form\Fields\Field;
|
||||
use App\Form\Models\Formtemplate;
|
||||
use App\Group;
|
||||
use App\Lib\HasMeta;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Formtemplate
|
||||
*/
|
||||
class FormtemplateResource extends JsonResource
|
||||
{
|
||||
|
||||
use HasMeta;
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
...parent::toArray($request),
|
||||
'links' => [
|
||||
'update' => route('formtemplate.update', ['formtemplate' => $this->getModel()]),
|
||||
'destroy' => route('formtemplate.destroy', ['formtemplate' => $this->getModel()]),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(): array
|
||||
{
|
||||
return [
|
||||
'base_url' => url(''),
|
||||
'groups' => Group::forSelect(),
|
||||
'fields' => Field::asMeta(),
|
||||
'namiTypes' => NamiType::forSelect(),
|
||||
'specialTypes' => SpecialType::forSelect(),
|
||||
'links' => [
|
||||
'store' => route('formtemplate.store'),
|
||||
'form_index' => route('form.index'),
|
||||
],
|
||||
'default' => [
|
||||
'name' => '',
|
||||
'config' => [
|
||||
'sections' => [],
|
||||
]
|
||||
],
|
||||
'section_default' => [
|
||||
'name' => '',
|
||||
'intro' => '',
|
||||
'fields' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Resources;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @mixin Participant
|
||||
*/
|
||||
class ParticipantResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return $this->getModel()->getFields()->present();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function meta(Form $form): array
|
||||
{
|
||||
return [
|
||||
'form_meta' => $form->meta,
|
||||
'links' => [
|
||||
'update_form_meta' => route('form.update-meta', ['form' => $form]),
|
||||
],
|
||||
'columns' => $form->getFields()
|
||||
->map(fn ($field) => [
|
||||
'name' => $field->name,
|
||||
'base_type' => class_basename($field),
|
||||
'id' => $field->key,
|
||||
'display_attribute' => $field->getDisplayAttribute(),
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Scopes;
|
||||
|
||||
use Laravel\Scout\Builder;
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\ScoutFilter;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
/**
|
||||
* @extends ScoutFilter<Form>
|
||||
*/
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class FormFilterScope extends ScoutFilter
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $search = '',
|
||||
public bool $past = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getQuery(): Builder
|
||||
{
|
||||
$this->search = $this->search ?: '';
|
||||
|
||||
return Form::search($this->search, function ($engine, string $query, array $options) {
|
||||
$options['sort'] = ['from:asc'];
|
||||
|
||||
$filters = collect([]);
|
||||
|
||||
if ($this->past === false) {
|
||||
$filters->push('to > ' . now()->timestamp);
|
||||
}
|
||||
|
||||
$options['filter'] = $filters->implode(' AND ');
|
||||
|
||||
return $engine->search($query, $options);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Scopes;
|
||||
|
||||
use App\Form\Models\Participant;
|
||||
use App\Lib\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
/**
|
||||
* @extends Filter<Participant>
|
||||
*/
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class ParticipantFilterScope extends Filter
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $parent = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function apply(Builder $query): Builder
|
||||
{
|
||||
if ($this->parent === -1) {
|
||||
$query = $query->whereNull('parent_id');
|
||||
}
|
||||
|
||||
if (!is_null($this->parent) && $this->parent > 0) {
|
||||
$query = $query->where('parent_id', $this->parent);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Transformers;
|
||||
|
||||
use App\Form\Fields\Field;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Support\DataProperty;
|
||||
use Spatie\LaravelData\Transformers\Transformer;
|
||||
|
||||
class CollectionTransformer implements Transformer
|
||||
{
|
||||
|
||||
public function __construct(public string $target)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Field> $value
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function transform(DataProperty $property, mixed $value): mixed
|
||||
{
|
||||
return $value->toArray();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Transformers;
|
||||
|
||||
use App\Form\Fields\Field;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\LaravelData\Support\DataProperty;
|
||||
use Spatie\LaravelData\Transformers\Transformer;
|
||||
|
||||
class FieldCollectionTransformer implements Transformer
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Collection<int, Field> $value
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function transform(DataProperty $property, mixed $value): mixed
|
||||
{
|
||||
return $value->map(fn ($field) => [
|
||||
...$field->toArray(),
|
||||
'type' => class_basename($field),
|
||||
])->toArray();
|
||||
}
|
||||
}
|
|
@ -45,4 +45,25 @@ class Group extends Model
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{id: int, name: string}>
|
||||
*/
|
||||
public static function forSelect(?self $parent = null, string $prefix = ''): array
|
||||
{
|
||||
$result = self::where('parent_id', $parent ? $parent->id : null)->withCount('children')->get();
|
||||
|
||||
return $result
|
||||
->reduce(
|
||||
fn ($before, $group) => $before->concat([['id' => $group->id, 'name' => $prefix . ($group->display())]])
|
||||
->concat($group->children_count > 0 ? self::forSelect($group, $prefix . '-- ') : []),
|
||||
collect([])
|
||||
)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function display(): string
|
||||
{
|
||||
return $this->inner_name ?: $this->name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ class NamiSearchAction
|
|||
*
|
||||
* @return LengthAwarePaginator<MemberEntry>
|
||||
*/
|
||||
public function handle(Api $api, int $page, array $params): LengthAwarePaginator
|
||||
public function handle(Api $api, int $page, array $params, int $perPage = 10): LengthAwarePaginator
|
||||
{
|
||||
return $api->pageSearch($params, $page, 10);
|
||||
return $api->pageSearch($params, $page, $perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Lib;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Scout\Builder;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
|
@ -14,10 +14,10 @@ abstract class Filter extends Data
|
|||
{
|
||||
|
||||
/**
|
||||
* @return Builder
|
||||
* @param Builder<T> $query
|
||||
* @return Builder<T>
|
||||
*/
|
||||
abstract public function getQuery(): Builder;
|
||||
protected Builder $query;
|
||||
abstract public function apply(Builder $query): Builder;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string|null $request
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Scout\Builder;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
/**
|
||||
* @template T of Model
|
||||
* @property Builder $query
|
||||
*/
|
||||
abstract class ScoutFilter extends Data
|
||||
{
|
||||
|
||||
/**
|
||||
* @return Builder
|
||||
*/
|
||||
abstract public function getQuery(): Builder;
|
||||
protected Builder $query;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|string|null $request
|
||||
*/
|
||||
public static function fromRequest(array|string|null $request = null): static
|
||||
{
|
||||
$payload = is_string($request)
|
||||
? json_decode(rawurldecode(base64_decode($request)), true)
|
||||
: $request;
|
||||
|
||||
return static::fromPost($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $post
|
||||
*/
|
||||
public static function fromPost(?array $post = null): static
|
||||
{
|
||||
return static::withoutMagicalCreationFrom($post ?: []);
|
||||
}
|
||||
}
|
|
@ -5,8 +5,8 @@ namespace App\Member;
|
|||
use App\Activity;
|
||||
use App\Group;
|
||||
use App\Invoice\BillKind;
|
||||
use App\Lib\Filter;
|
||||
use App\Subactivity;
|
||||
use App\Lib\ScoutFilter;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Builder;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
|
@ -14,11 +14,11 @@ use Spatie\LaravelData\Attributes\MapOutputName;
|
|||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
||||
/**
|
||||
* @extends Filter<Member>
|
||||
* @extends ScoutFilter<Member>
|
||||
*/
|
||||
#[MapInputName(SnakeCaseMapper::class)]
|
||||
#[MapOutputName(SnakeCaseMapper::class)]
|
||||
class FilterScope extends Filter
|
||||
class FilterScope extends ScoutFilter
|
||||
{
|
||||
/**
|
||||
* @param array<int, int> $activityIds
|
||||
|
|
|
@ -93,7 +93,7 @@ class Member extends Model implements Geolocatable
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{source: array<int, string>}>
|
||||
* @return SluggableConfig
|
||||
*/
|
||||
public function sluggable(): array
|
||||
{
|
||||
|
|
|
@ -7,12 +7,14 @@ enum Module: string
|
|||
|
||||
case BILL = 'bill';
|
||||
case COURSE = 'course';
|
||||
case EVENT = 'event';
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return match ($this) {
|
||||
static::BILL => 'Zahlungs-Management',
|
||||
static::COURSE => 'Ausbildung',
|
||||
static::EVENT => 'Veranstaltungen',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Mailgateway\Types\LocalType;
|
||||
use App\Mailgateway\Types\MailmanType;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
@ -33,6 +34,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
LocalType::class,
|
||||
MailmanType::class,
|
||||
]));
|
||||
|
||||
app()->extend('media-library-helpers', fn ($p) => $p->put('form', Form::class));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Policies\FormPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Laravel\Passport\Passport;
|
||||
|
||||
|
@ -10,10 +12,10 @@ class AuthServiceProvider extends ServiceProvider
|
|||
/**
|
||||
* The policy mappings for the application.
|
||||
*
|
||||
* @var array
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Model' => 'App\Policies\ModelPolicy',
|
||||
Form::class => FormPolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -57,5 +57,8 @@ class RouteServiceProvider extends ServiceProvider
|
|||
Route::middleware('api')
|
||||
->prefix('api')
|
||||
->group(base_path('routes/api.php'));
|
||||
Route::middleware('api')
|
||||
->prefix('remote')
|
||||
->group(base_path('routes/remote.php'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Remote\Actions;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\LaravelNami\Nami;
|
||||
|
||||
class LoginAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
|
||||
return [
|
||||
'mglnr' => 'required|numeric',
|
||||
'password' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(ActionRequest $request): JsonResponse
|
||||
{
|
||||
Nami::freshLogin($request->mglnr, $request->password);
|
||||
|
||||
return response()->json([
|
||||
'access_token' => Crypt::encryptString(json_encode(['mglnr' => $request->mglnr, 'password' => $request->password])),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Remote\Actions;
|
||||
|
||||
use App\Initialize\Actions\NamiSearchAction;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
use Zoomyboy\LaravelNami\Nami;
|
||||
|
||||
class SearchAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<array<string, mixed>>
|
||||
*/
|
||||
public function handle(ActionRequest $request): LengthAwarePaginator
|
||||
{
|
||||
$token = str($request->header('Authorization'))->replace('Bearer ', '')->toString();
|
||||
$credentials = json_decode(Crypt::decryptString($token));
|
||||
|
||||
$api = Nami::login($credentials->mglnr, $credentials->password);
|
||||
|
||||
$results = NamiSearchAction::run($api, $request->input('page', 1), $request->except('page'), 50);
|
||||
$results->transform(fn ($member) => ['id' => $member->memberId, 'name' => $member->firstname . ' ' . $member->lastname]);
|
||||
return $results;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,13 @@
|
|||
"options": {
|
||||
"symlink": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"url": "./packages/medialibrary-helper",
|
||||
"options": {
|
||||
"symlink": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
|
@ -53,12 +60,12 @@
|
|||
"zoomyboy/laravel-nami": "dev-master",
|
||||
"zoomyboy/osm": "1.0.3",
|
||||
"zoomyboy/phone": "^1.0",
|
||||
"zoomyboy/tex": "dev-main as 1.0"
|
||||
"zoomyboy/tex": "dev-main as 1.0",
|
||||
"zoomyboy/medialibrary-helper": "dev-master as 1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/envoy": "^2.8",
|
||||
"laravel/sail": "^1.0.1",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"larastan/larastan": "^2.0",
|
||||
"orchestra/testbench": "^7.0",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,267 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* The disk on which to store added files and derived images by default. Choose
|
||||
* one or more of the disks you've configured in config/filesystems.php.
|
||||
*/
|
||||
'disk_name' => env('MEDIA_DISK', 'public'),
|
||||
|
||||
/*
|
||||
* The maximum file size of an item in bytes.
|
||||
* Adding a larger file will result in an exception.
|
||||
*/
|
||||
'max_file_size' => 1024 * 1024 * 10, // 10MB
|
||||
|
||||
/*
|
||||
* This queue connection will be used to generate derived and responsive images.
|
||||
* Leave empty to use the default queue connection.
|
||||
*/
|
||||
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
|
||||
|
||||
/*
|
||||
* This queue will be used to generate derived and responsive images.
|
||||
* Leave empty to use the default queue.
|
||||
*/
|
||||
'queue_name' => '',
|
||||
|
||||
/*
|
||||
* By default all conversions will be performed on a queue.
|
||||
*/
|
||||
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
|
||||
|
||||
/*
|
||||
* The fully qualified class name of the media model.
|
||||
*/
|
||||
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
|
||||
|
||||
/*
|
||||
* When enabled, media collections will be serialised using the default
|
||||
* laravel model serialization behaviour.
|
||||
*
|
||||
* Keep this option disabled if using Media Library Pro components (https://medialibrary.pro)
|
||||
*/
|
||||
'use_default_collection_serialization' => false,
|
||||
|
||||
/*
|
||||
* The fully qualified class name of the model used for temporary uploads.
|
||||
*
|
||||
* This model is only used in Media Library Pro (https://medialibrary.pro)
|
||||
*/
|
||||
'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class,
|
||||
|
||||
/*
|
||||
* When enabled, Media Library Pro will only process temporary uploads that were uploaded
|
||||
* in the same session. You can opt to disable this for stateless usage of
|
||||
* the pro components.
|
||||
*/
|
||||
'enable_temporary_uploads_session_affinity' => true,
|
||||
|
||||
/*
|
||||
* When enabled, Media Library pro will generate thumbnails for uploaded file.
|
||||
*/
|
||||
'generate_thumbnails_for_temporary_uploads' => true,
|
||||
|
||||
/*
|
||||
* This is the class that is responsible for naming generated files.
|
||||
*/
|
||||
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
|
||||
|
||||
/*
|
||||
* The class that contains the strategy for determining a media file's path.
|
||||
*/
|
||||
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
|
||||
|
||||
/*
|
||||
* The class that contains the strategy for determining how to remove files.
|
||||
*/
|
||||
'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class,
|
||||
|
||||
/*
|
||||
* Here you can specify which path generator should be used for the given class.
|
||||
*/
|
||||
'custom_path_generators' => [
|
||||
// Model::class => PathGenerator::class
|
||||
// or
|
||||
// 'model_morph_alias' => PathGenerator::class
|
||||
],
|
||||
|
||||
/*
|
||||
* When urls to files get generated, this class will be called. Use the default
|
||||
* if your files are stored locally above the site root or on s3.
|
||||
*/
|
||||
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
|
||||
|
||||
/*
|
||||
* Moves media on updating to keep path consistent. Enable it only with a custom
|
||||
* PathGenerator that uses, for example, the media UUID.
|
||||
*/
|
||||
'moves_media_on_update' => false,
|
||||
|
||||
/*
|
||||
* Whether to activate versioning when urls to files get generated.
|
||||
* When activated, this attaches a ?v=xx query string to the URL.
|
||||
*/
|
||||
'version_urls' => false,
|
||||
|
||||
/*
|
||||
* The media library will try to optimize all converted images by removing
|
||||
* metadata and applying a little bit of compression. These are
|
||||
* the optimizers that will be used by default.
|
||||
*/
|
||||
'image_optimizers' => [
|
||||
Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [
|
||||
'-m85', // set maximum quality to 85%
|
||||
'--force', // ensure that progressive generation is always done also if a little bigger
|
||||
'--strip-all', // this strips out all text information such as comments and EXIF data
|
||||
'--all-progressive', // this will make sure the resulting image is a progressive one
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Pngquant::class => [
|
||||
'--force', // required parameter for this package
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Optipng::class => [
|
||||
'-i0', // this will result in a non-interlaced, progressive scanned image
|
||||
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
|
||||
'-quiet', // required parameter for this package
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Svgo::class => [
|
||||
'--disable=cleanupIDs', // disabling because it is known to cause troubles
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [
|
||||
'-b', // required parameter for this package
|
||||
'-O3', // this produces the slowest but best results
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Cwebp::class => [
|
||||
'-m 6', // for the slowest compression method in order to get the best compression.
|
||||
'-pass 10', // for maximizing the amount of analysis pass.
|
||||
'-mt', // multithreading for some speed improvements.
|
||||
'-q 90', //quality factor that brings the least noticeable changes.
|
||||
],
|
||||
Spatie\ImageOptimizer\Optimizers\Avifenc::class => [
|
||||
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
|
||||
'-j all', // number of jobs (worker threads, "all" uses all available cores).
|
||||
'--min 0', // min quantizer for color (0-63).
|
||||
'--max 63', // max quantizer for color (0-63).
|
||||
'--minalpha 0', // min quantizer for alpha (0-63).
|
||||
'--maxalpha 63', // max quantizer for alpha (0-63).
|
||||
'-a end-usage=q', // rate control mode set to Constant Quality mode.
|
||||
'-a tune=ssim', // SSIM as tune the encoder for distortion metric.
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* These generators will be used to create an image of media files.
|
||||
*/
|
||||
'image_generators' => [
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class,
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class,
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class,
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class,
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class,
|
||||
Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class,
|
||||
],
|
||||
|
||||
/*
|
||||
* The path where to store temporary files while performing image conversions.
|
||||
* If set to null, storage_path('media-library/temp') will be used.
|
||||
*/
|
||||
'temporary_directory_path' => null,
|
||||
|
||||
/*
|
||||
* The engine that should perform the image conversions.
|
||||
* Should be either `gd` or `imagick`.
|
||||
*/
|
||||
'image_driver' => env('IMAGE_DRIVER', 'gd'),
|
||||
|
||||
/*
|
||||
* FFMPEG & FFProbe binaries paths, only used if you try to generate video
|
||||
* thumbnails and have installed the php-ffmpeg/php-ffmpeg composer
|
||||
* dependency.
|
||||
*/
|
||||
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
|
||||
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
|
||||
|
||||
/*
|
||||
* Here you can override the class names of the jobs used by this package. Make sure
|
||||
* your custom jobs extend the ones provided by the package.
|
||||
*/
|
||||
'jobs' => [
|
||||
'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class,
|
||||
'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class,
|
||||
],
|
||||
|
||||
/*
|
||||
* When using the addMediaFromUrl method you may want to replace the default downloader.
|
||||
* This is particularly useful when the url of the image is behind a firewall and
|
||||
* need to add additional flags, possibly using curl.
|
||||
*/
|
||||
'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class,
|
||||
|
||||
'remote' => [
|
||||
/*
|
||||
* Any extra headers that should be included when uploading media to
|
||||
* a remote disk. Even though supported headers may vary between
|
||||
* different drivers, a sensible default has been provided.
|
||||
*
|
||||
* Supported by S3: CacheControl, Expires, StorageClass,
|
||||
* ServerSideEncryption, Metadata, ACL, ContentEncoding
|
||||
*/
|
||||
'extra_headers' => [
|
||||
'CacheControl' => 'max-age=604800',
|
||||
],
|
||||
],
|
||||
|
||||
'responsive_images' => [
|
||||
/*
|
||||
* This class is responsible for calculating the target widths of the responsive
|
||||
* images. By default we optimize for filesize and create variations that each are 30%
|
||||
* smaller than the previous one. More info in the documentation.
|
||||
*
|
||||
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
|
||||
*/
|
||||
'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class,
|
||||
|
||||
/*
|
||||
* By default rendering media to a responsive image will add some javascript and a tiny placeholder.
|
||||
* This ensures that the browser can already determine the correct layout.
|
||||
*/
|
||||
'use_tiny_placeholders' => true,
|
||||
|
||||
/*
|
||||
* This class will generate the tiny placeholder used for progressive image loading. By default
|
||||
* the media library will use a tiny blurred jpg image.
|
||||
*/
|
||||
'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class,
|
||||
],
|
||||
|
||||
/*
|
||||
* When enabling this option, a route will be registered that will enable
|
||||
* the Media Library Pro Vue and React components to move uploaded files
|
||||
* in a S3 bucket to their right place.
|
||||
*/
|
||||
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
|
||||
|
||||
/*
|
||||
* When converting Media instances to response the media library will add
|
||||
* a `loading` attribute to the `img` tag. Here you can set the default
|
||||
* value of that attribute.
|
||||
*
|
||||
* Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction.
|
||||
*
|
||||
* More info: https://css-tricks.com/native-lazy-loading/
|
||||
*/
|
||||
'default_loading_attribute_value' => null,
|
||||
|
||||
/*
|
||||
* You can specify a prefix for that is used for storing all media.
|
||||
* If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory.
|
||||
*/
|
||||
'prefix' => env('MEDIA_PREFIX', ''),
|
||||
|
||||
/*
|
||||
* The temp disk for deferred media data.
|
||||
* Only used by the medialibrary-helper package.
|
||||
*/
|
||||
'temp_disk' => 'temp',
|
||||
];
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Member\Member;
|
||||
|
||||
return [
|
||||
|
@ -140,6 +141,12 @@ return [
|
|||
'searchableAttributes' => ['fullname', 'address'],
|
||||
'sortableAttributes' => ['lastname', 'firstname'],
|
||||
'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'],
|
||||
],
|
||||
Form::class => [
|
||||
'filterableAttributes' => ['to'],
|
||||
'searchableAttributes' => ['name'],
|
||||
'sortableAttributes' => ['from',],
|
||||
'displayedAttributes' => ['from', 'name', 'id', 'to'],
|
||||
]
|
||||
],
|
||||
],
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Form\Models;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use Database\Factories\Traits\FakesMedia;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Tests\Feature\Form\FormtemplateFieldRequest;
|
||||
use Tests\Feature\Form\FormtemplateSectionRequest;
|
||||
|
||||
/**
|
||||
* @extends Factory<Form>
|
||||
* @method self name(string $name)
|
||||
* @method self from(string $from)
|
||||
* @method self to(string $to)
|
||||
* @method self mailTop(string $content)
|
||||
* @method self mailBottom(string $content)
|
||||
* @method self excerpt(string $excerpt)
|
||||
* @method self description(string $description)
|
||||
* @method self registrationFrom(string|null $date)
|
||||
* @method self registrationUntil(string|null $date)
|
||||
*/
|
||||
class FormFactory extends Factory
|
||||
{
|
||||
use FakesMedia;
|
||||
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<Form>
|
||||
*/
|
||||
protected $model = Form::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(4, true),
|
||||
'description' => $this->faker->text(),
|
||||
'excerpt' => $this->faker->words(10, true),
|
||||
'config' => ['sections' => []],
|
||||
'from' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
|
||||
'to' => $this->faker->dateTimeBetween('+1 week', '+4 weeks')->format('Y-m-d H:i:s'),
|
||||
'registration_from' => $this->faker->dateTimeBetween('-2 weeks', 'now')->format('Y-m-d H:i:s'),
|
||||
'registration_until' => $this->faker->dateTimeBetween('now', '+2 weeks')->format('Y-m-d H:i:s'),
|
||||
'mail_top' => $this->faker->text(),
|
||||
'mail_bottom' => $this->faker->text(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, FormtemplateSectionRequest> $sections
|
||||
*/
|
||||
public function sections(array $sections): self
|
||||
{
|
||||
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, FormtemplateFieldRequest> $fields
|
||||
*/
|
||||
public function fields(array $fields): self
|
||||
{
|
||||
return $this->sections([FormtemplateSectionRequest::new()->fields($fields)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function __call($method, $parameters): self
|
||||
{
|
||||
return $this->state([str($method)->snake()->toString() => $parameters[0]]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Form\Models;
|
||||
|
||||
use App\Form\Models\Formtemplate;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Tests\Feature\Form\FormtemplateSectionRequest;
|
||||
|
||||
/**
|
||||
* @extends Factory<Formtemplate>
|
||||
* @method self name(string $name)
|
||||
*/
|
||||
class FormtemplateFactory extends Factory
|
||||
{
|
||||
|
||||
public $model = Formtemplate::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->words(4, true),
|
||||
'config' => [
|
||||
'sections' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, FormtemplateSectionRequest> $sections
|
||||
*/
|
||||
public function sections(array $sections): self
|
||||
{
|
||||
return $this->state(['config' => ['sections' => array_map(fn ($section) => $section->create(), $sections)]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $parameters
|
||||
*/
|
||||
public function __call($method, $parameters): self
|
||||
{
|
||||
return $this->state([str($method)->snake()->toString() => $parameters[0]]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Form\Models;
|
||||
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Tests\Feature\Form\FormtemplateSectionRequest;
|
||||
|
||||
/**
|
||||
* @extends Factory<Participant>
|
||||
*/
|
||||
class ParticipantFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<Participant>
|
||||
*/
|
||||
protected $model = Participant::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'data' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function data(array $data): self
|
||||
{
|
||||
return $this->state(['data' => $data]);
|
||||
}
|
||||
|
||||
public function nr(int $number): self
|
||||
{
|
||||
return $this->state(['mitgliedsnr' => $number]);
|
||||
}
|
||||
}
|
|
@ -37,4 +37,14 @@ class GroupFactory extends Factory
|
|||
{
|
||||
return $this->state(['name' => $name]);
|
||||
}
|
||||
|
||||
public function level(Level $level): self
|
||||
{
|
||||
return $this->state(['level' => $level]);
|
||||
}
|
||||
|
||||
public function innerName(string $name): self
|
||||
{
|
||||
return $this->state(['inner_name' => $name]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class MemberFactory extends Factory
|
|||
: Nationality::factory()->create();
|
||||
$subscription = Subscription::count()
|
||||
? Subscription::get()->random()
|
||||
: Subscription::factory()->for(Fee::factory())->create();
|
||||
: Subscription::factory()->forFee()->create();
|
||||
|
||||
return $this
|
||||
->for($country)
|
||||
|
|
|
@ -19,10 +19,16 @@ class SubscriptionFactory extends Factory
|
|||
{
|
||||
return [
|
||||
'name' => $this->faker->word,
|
||||
'fee_id' => Fee::factory()->createOne()->id,
|
||||
];
|
||||
}
|
||||
|
||||
public function forFee(?int $namiId = null): self
|
||||
{
|
||||
$namiId = $namiId ?: $this->faker->numberBetween(600, 800);
|
||||
|
||||
return $this->for(Fee::factory()->inNami($namiId));
|
||||
}
|
||||
|
||||
public function name(string $name): self
|
||||
{
|
||||
return $this->state(['name' => $name]);
|
||||
|
|
|
@ -48,9 +48,9 @@ class SubactivityFactory extends Factory
|
|||
return $this->state(['nami_id' => $namiId]);
|
||||
}
|
||||
|
||||
public function ageGroup(): self
|
||||
public function ageGroup(bool $ageGroup = true): self
|
||||
{
|
||||
return $this->state(['is_age_group' => true]);
|
||||
return $this->state(['is_age_group' => $ageGroup]);
|
||||
}
|
||||
|
||||
public function filterable(): self
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Traits;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
|
||||
trait FakesMedia
|
||||
{
|
||||
|
||||
public function withImage(string $collection, string $filename): self
|
||||
{
|
||||
return $this->afterCreating(function (HasMedia $model) use ($filename, $collection) {
|
||||
$pathinfo = pathinfo($filename);
|
||||
|
||||
UploadedFile::fake()->image($filename, 1000, 1000)->storeAs('media-library', $filename, 'temp');
|
||||
|
||||
$model->addMediaFromDisk('media-library/' . $filename, 'temp')
|
||||
->usingName($pathinfo['filename'])
|
||||
->usingFileName($pathinfo['basename'])
|
||||
->toMediaCollection($collection);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('formtemplates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->json('config');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('forms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug');
|
||||
$table->json('description');
|
||||
$table->text('excerpt');
|
||||
$table->json('config');
|
||||
$table->date('from');
|
||||
$table->date('to');
|
||||
$table->dateTime('registration_from')->nullable();
|
||||
$table->dateTime('registration_until')->nullable();
|
||||
$table->text('mail_top')->nullable();
|
||||
$table->text('mail_bottom')->nullable();
|
||||
$table->json('meta');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('formtemplates');
|
||||
Schema::dropIfExists('forms');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('media', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->morphs('model');
|
||||
$table->uuid('uuid')->nullable()->unique();
|
||||
$table->string('collection_name');
|
||||
$table->string('name');
|
||||
$table->string('file_name');
|
||||
$table->string('mime_type')->nullable();
|
||||
$table->string('disk');
|
||||
$table->string('conversions_disk')->nullable();
|
||||
$table->unsignedBigInteger('size');
|
||||
$table->json('manipulations');
|
||||
$table->json('custom_properties');
|
||||
$table->json('generated_conversions');
|
||||
$table->json('responsive_images');
|
||||
$table->unsignedInteger('order_column')->nullable()->index();
|
||||
|
||||
$table->nullableTimestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('media');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('participants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('data');
|
||||
$table->foreignId('form_id');
|
||||
$table->foreignId('parent_id')->nullable();
|
||||
$table->string('mitgliedsnr')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('participants');
|
||||
}
|
||||
};
|
|
@ -5,6 +5,10 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.29.0",
|
||||
"@editorjs/header": "^2.8.1",
|
||||
"@editorjs/nested-list": "^1.4.2",
|
||||
"@editorjs/paragraph": "^2.11.3",
|
||||
"@inertiajs/vue3": "^1.0.14",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
|
@ -25,6 +29,7 @@
|
|||
"wnumb": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"accounting": "^0.4.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"axios": "^1.6.6",
|
||||
"dayjs": "^1.11.10",
|
||||
|
@ -67,6 +72,11 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codexteam/icons": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.5.tgz",
|
||||
"integrity": "sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA=="
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
|
@ -85,6 +95,45 @@
|
|||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@editorjs/editorjs": {
|
||||
"version": "2.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.29.0.tgz",
|
||||
"integrity": "sha512-w2BVboSHokMBd/cAOZn0UU328o3gSZ8XUvFPA2e9+ciIGKILiRSPB70kkNdmhHkuNS3q2px+vdaIFaywBl7wGA=="
|
||||
},
|
||||
"node_modules/@editorjs/header": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/header/-/header-2.8.1.tgz",
|
||||
"integrity": "sha512-y0HVXRP7m2W617CWo3fsb5HhXmSLaRpb9GzFx0Vkp/HEm9Dz5YO1s8tC7R8JD3MskwoYh7V0hRFQt39io/r6hA==",
|
||||
"dependencies": {
|
||||
"@codexteam/icons": "^0.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@editorjs/nested-list": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/nested-list/-/nested-list-1.4.2.tgz",
|
||||
"integrity": "sha512-qb1dAoJ+bihqmlR3822TC2GuIxEjTCLTZsZVWNces3uJIZ+W4019G3IJKBt/MOOgz4Evzad/RvUEKwPCPe6YOQ==",
|
||||
"dependencies": {
|
||||
"@codexteam/icons": "^0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@editorjs/nested-list/node_modules/@codexteam/icons": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.2.tgz",
|
||||
"integrity": "sha512-KdeKj3TwaTHqM3IXd5YjeJP39PBUZTb+dtHjGlf5+b0VgsxYD4qzsZkb11lzopZbAuDsHaZJmAYQ8LFligIT6Q=="
|
||||
},
|
||||
"node_modules/@editorjs/paragraph": {
|
||||
"version": "2.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/paragraph/-/paragraph-2.11.3.tgz",
|
||||
"integrity": "sha512-ON72lhvhgWzPrq4VXpHUeut9bsFeJgVATDeL850FVToOwYHKvdsNpfu0VgxEodhkXgzU/IGl4FzdqC2wd3AJUQ==",
|
||||
"dependencies": {
|
||||
"@codexteam/icons": "^0.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@editorjs/paragraph/node_modules/@codexteam/icons": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@codexteam/icons/-/icons-0.0.4.tgz",
|
||||
"integrity": "sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q=="
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
|
@ -1083,6 +1132,12 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accounting": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/accounting/-/accounting-0.4.1.tgz",
|
||||
"integrity": "sha512-RU6KY9Y5wllyaCNBo1W11ZOTnTHMMgOZkIwdOOs6W5ibMTp72i4xIbEA48djxVGqMNTUNbvrP/1nWg5Af5m2gQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"fix": "eslint \"resources/js/**/*.{js,vue}\" --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"accounting": "^0.4.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"axios": "^1.6.6",
|
||||
"dayjs": "^1.11.10",
|
||||
|
@ -23,6 +24,10 @@
|
|||
"vue-axios": "^3.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.29.0",
|
||||
"@editorjs/header": "^2.8.1",
|
||||
"@editorjs/nested-list": "^1.4.2",
|
||||
"@editorjs/paragraph": "^2.11.3",
|
||||
"@inertiajs/vue3": "^1.0.14",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 2e232a95fd84f6dcca80abe4d2ed37e1fac7e352
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 91e5cc3e3b6d7d0a8a3e2361514ec8c1ce9cb655
|
26
phpstan.neon
26
phpstan.neon
|
@ -24,6 +24,7 @@ parameters:
|
|||
ContributionApiRequestArray: 'array{dateFrom: string, dateUntil: string, zipLocation: string, country: int, eventName: string, member_data: array<int, ContributionMemberData>}'
|
||||
MailgatewayCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string}'
|
||||
MailgatewayParsedCustomField: 'array{name: string, label: string, type: string, storeValidator: string, updateValidator: string, default: string, is_required: bool}'
|
||||
SluggableConfig: 'array<string, array{source: array<int, string>}>'
|
||||
|
||||
ignoreErrors:
|
||||
-
|
||||
|
@ -136,16 +137,6 @@ parameters:
|
|||
count: 1
|
||||
path: app/Payment/SubscriptionResource.php
|
||||
|
||||
-
|
||||
message: "#^PHPDoc type array of property App\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies is not covariant with PHPDoc type array\\<class\\-string, class\\-string\\> of overridden property Illuminate\\\\Foundation\\\\Support\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies\\.$#"
|
||||
count: 1
|
||||
path: app/Providers/AuthServiceProvider.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Providers\\\\AuthServiceProvider\\:\\:\\$policies type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: app/Providers/AuthServiceProvider.php
|
||||
|
||||
-
|
||||
message: "#^Method Database\\\\Factories\\\\ActivityFactory\\:\\:definition\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
@ -541,3 +532,18 @@ parameters:
|
|||
message: "#^Parameter \\#1 \\.\\.\\.\\$parts of method App\\\\Member\\\\FilterScope\\:\\:combinations\\(\\) expects array\\<int, int\\>, array\\<int, array\\<int, int\\>\\> given\\.$#"
|
||||
count: 1
|
||||
path: app/Member/FilterScope.php
|
||||
|
||||
-
|
||||
message: "#^Call to function is_null\\(\\) with array will always evaluate to false\\.$#"
|
||||
count: 1
|
||||
path: app/Form/Models/Form.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TKey in call to function collect$#"
|
||||
count: 1
|
||||
path: app/Form/Fields/NamiField.php
|
||||
|
||||
-
|
||||
message: "#^Unable to resolve the template type TValue in call to function collect$#"
|
||||
count: 1
|
||||
path: app/Form/Fields/NamiField.php
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
@import 'table';
|
||||
@import 'bool';
|
||||
@import 'form';
|
||||
@import 'editor';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
::selection {
|
||||
@apply bg-blue-800;
|
||||
}
|
||||
|
||||
.button-group > div {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
.ce-inline-toolbar,
|
||||
.codex-editor--narrow .ce-toolbox,
|
||||
.ce-conversion-toolbar,
|
||||
.ce-settings,
|
||||
.ce-settings__button,
|
||||
.ce-toolbar__settings-btn,
|
||||
.cdx-button,
|
||||
.ce-popover,
|
||||
.ce-toolbar__plus:hover {
|
||||
@apply bg-primary-700;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.ce-inline-tool-input {
|
||||
@apply bg-primary-700 text-primary-200 placeholder-primary-500;
|
||||
}
|
||||
|
||||
.ce-block--selected {
|
||||
@apply bg-gray-800;
|
||||
}
|
||||
|
||||
h2.ce-header {
|
||||
@apply font-semibold text-white text-xl mt-2;
|
||||
}
|
||||
|
||||
h3.ce-header {
|
||||
@apply font-semibold text-white text-lg mt-2;
|
||||
}
|
||||
|
||||
h4.ce-header {
|
||||
@apply font-semibold text-white mt-2;
|
||||
}
|
||||
|
||||
.ce-block--selected .ce-block__content {
|
||||
@apply bg-gray-800;
|
||||
}
|
||||
|
||||
.ce-conversion-tool__icon,
|
||||
.ce-popover-item__icon {
|
||||
@apply bg-primary-800;
|
||||
}
|
||||
|
||||
.ce-popover-item__title {
|
||||
@apply text-primary-200;
|
||||
}
|
||||
|
||||
.ce-popover-item:hover:not(.ce-popover-item--no-hover) {
|
||||
@apply bg-primary-800;
|
||||
}
|
||||
|
||||
.ce-inline-tool,
|
||||
.ce-conversion-toolbar__label,
|
||||
.ce-toolbox__button,
|
||||
.cdx-settings-button,
|
||||
.ce-toolbar__plus {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.codex-editor ::selection {
|
||||
@apply bg-blue-800;
|
||||
}
|
||||
|
||||
.cdx-settings-button:hover,
|
||||
.ce-settings__button:hover,
|
||||
.ce-toolbox__button--active,
|
||||
.ce-toolbox__button:hover,
|
||||
.cdx-button:hover,
|
||||
.ce-inline-toolbar__dropdown:hover,
|
||||
.ce-inline-tool:hover,
|
||||
.ce-popover__item:hover,
|
||||
.ce-toolbar__settings-btn:hover {
|
||||
background-color: #439a86;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.cdx-notify--error {
|
||||
background: #fb5d5d !important;
|
||||
}
|
||||
|
||||
.cdx-notify__cross::after,
|
||||
.cdx-notify__cross::before {
|
||||
background: white;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.664 503.664" style="enable-background:new 0 0 503.664 503.664" xml:space="preserve"><path d="M467.98 39.376H35.792C16.112 39.376.1 55.384.1 75.064L0 369.52c0 19.676 16.004 35.748 35.684 35.748h141.4v35.408h-27.612c-2.172 0-3.864 1.848-3.864 4.02v15.532c0 2.172 1.688 4.06 3.864 4.06h204c2.164 0 4.584-1.888 4.584-4.06v-15.532c0-2.172-2.42-4.02-4.584-4.02H326.58v-35.408h141.304c19.664 0 35.688-16.072 35.688-35.748l.092-294.424c.004-19.684-16.004-35.72-35.684-35.72zM251.34 377.524c-6.524 0-11.804-5.284-11.804-11.804 0-6.516 5.28-11.8 11.804-11.8 6.52 0 11.804 5.284 11.804 11.8 0 6.52-5.28 11.804-11.804 11.804zm220.812-50.944H31.512V70.848h440.64V326.58z"/></svg>
|
After Width: | Height: | Size: 723 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="124.813" height="124.813" style="enable-background:new 0 0 124.813 124.813" xml:space="preserve"><path d="m48.083 80.355-1.915 11.374a4.158 4.158 0 0 0 1.65 4.05 4.15 4.15 0 0 0 4.361.32l10.226-5.338L72.631 96.1a4.184 4.184 0 0 0 1.924.472c.859 0 1.716-.269 2.439-.792a4.152 4.152 0 0 0 1.651-4.05l-1.913-11.374 8.234-8.077a4.159 4.159 0 0 0 1.044-4.247 4.152 4.152 0 0 0-3.341-2.823l-11.41-1.692-5.139-10.329a4.147 4.147 0 0 0-3.716-2.303 4.16 4.16 0 0 0-3.718 2.303l-5.134 10.329-11.41 1.691a4.147 4.147 0 0 0-3.339 2.823 4.147 4.147 0 0 0 1.042 4.247l8.238 8.077z"/><path d="M111.443 13.269H98.378V6.022A6.022 6.022 0 0 0 92.355 0H91.4a6.021 6.021 0 0 0-6.021 6.022v7.247H39.282V6.022A6.022 6.022 0 0 0 33.261 0h-.956a6.021 6.021 0 0 0-6.021 6.022v7.247H13.371C6.538 13.269.977 18.828.977 25.663v86.757c0 6.831 5.561 12.394 12.394 12.394h98.073c6.832 0 12.394-5.562 12.394-12.394V25.663c-.001-6.835-5.563-12.394-12.395-12.394zm-1.617 97.534H14.988V43.268h94.838v67.535z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.604 503.604" style="enable-background:new 0 0 503.604 503.604" xml:space="preserve"><path d="M337.324 0H167.192c-28.924 0-53.5 23.584-53.5 52.5v398.664c0 28.916 24.056 52.44 52.98 52.44l170.412-.184c28.92 0 52.58-23.528 52.58-52.448l.248-398.5C389.908 23.452 366.364 0 337.324 0zM227.68 31.476h49.36c4.336 0 7.868 3.52 7.868 7.868 0 4.348-3.532 7.868-7.868 7.868h-49.36a7.865 7.865 0 0 1-7.868-7.868 7.865 7.865 0 0 1 7.868-7.868zm-29.66 2.504c2.916-2.912 8.224-2.952 11.136 0a7.973 7.973 0 0 1 2.324 5.588c0 2.048-.864 4.088-2.324 5.548-1.452 1.46-3.504 2.32-5.548 2.32-2.084 0-4.088-.86-5.588-2.32-1.452-1.456-2.28-3.5-2.28-5.548-.004-2.088.828-4.132 2.28-5.588zm52.752 454.028c-12.984 0-23.544-10.568-23.544-23.548 0-12.984 10.56-23.548 23.544-23.548s23.544 10.564 23.544 23.548c0 12.98-10.564 23.548-23.544 23.548zm114.716-63.1H141.232V74.756h224.256v350.152z"/></svg>
|
After Width: | Height: | Size: 929 B |
|
@ -1,18 +1 @@
|
|||
<?xml version="1.0" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<polygon points="51.2,353.28 0,512 158.72,460.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="89.73" y="169.097" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -95.8575 260.3719)" width="353.277" height="153.599"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M504.32,79.36L432.64,7.68c-10.24-10.24-25.6-10.24-35.84,0l-23.04,23.04l107.52,107.52l23.04-23.04 C514.56,104.96,514.56,89.6,504.32,79.36z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><path d="M51.2 353.28 0 512l158.72-51.2zm35.96-36.788L336.96 66.69 445.57 175.3l-249.8 249.802zM504.32 79.36 432.64 7.68c-10.24-10.24-25.6-10.24-35.84 0l-23.04 23.04 107.52 107.52 23.04-23.04c10.24-10.24 10.24-25.6 0-35.84z"/></svg>
|
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 357 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 503.592 503.592" style="enable-background:new 0 0 503.592 503.592" xml:space="preserve"><path d="M428.636 27.852C428.636 12.508 416.144 0 400.792 0H102.66C87.312 0 74.828 12.504 74.828 27.852l.132 447.892c0 15.352 12.488 27.848 27.832 27.848h298.136c15.352 0 27.836-12.496 27.836-27.848l-.128-447.892zm-196.276 3.62h38.884c4.36 0 7.868 3.52 7.868 7.868a7.863 7.863 0 0 1-7.868 7.872H232.36c-4.348 0-7.868-3.524-7.868-7.872s3.52-7.868 7.868-7.868zm-29.396 2.44c2.916-2.948 8.184-2.948 11.136 0 1.452 1.46 2.324 3.504 2.324 5.552 0 2.08-.872 4.084-2.324 5.544-1.46 1.496-3.504 2.324-5.584 2.324a7.747 7.747 0 0 1-5.552-2.324c-1.452-1.456-2.316-3.464-2.316-5.544 0-2.048.868-4.092 2.316-5.552zm47.252 454.216c-12.98 0-23.544-10.564-23.544-23.544 0-12.984 10.568-23.548 23.544-23.548 12.988 0 23.544 10.564 23.544 23.548s-10.552 23.544-23.544 23.544zM94.424 424.904V74.752h314.744v350.152H94.424z"/></svg>
|
After Width: | Height: | Size: 954 B |
|
@ -50,6 +50,10 @@ createInertiaApp({
|
|||
requireModules(import.meta.glob('./components/form/*.vue'), app, 'f');
|
||||
requireModules(import.meta.glob('./components/ui/*.vue'), app, 'ui');
|
||||
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), app, 'page');
|
||||
app.component(
|
||||
'f-singlefile',
|
||||
defineAsyncComponent(() => import('!/medialibrary-helper/assets/components/SingleFile.vue'))
|
||||
);
|
||||
|
||||
app.provide('axios', app.config.globalProperties.axios);
|
||||
app.mount(el);
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div>
|
||||
<span v-if="label" class="font-semibold text-gray-400" :class="labelClass(size)">{{ label }}<span v-show="required" class="text-red-800"> *</span></span>
|
||||
<div class="relative w-full h-full">
|
||||
<div :id="id" :class="[defaultFieldClass, fieldClass(size)]"></div>
|
||||
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
|
||||
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {debounce} from 'lodash';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import EditorJS from '@editorjs/editorjs';
|
||||
import Header from '@editorjs/header';
|
||||
import Paragraph from '@editorjs/paragraph';
|
||||
import NestedList from '@editorjs/nested-list';
|
||||
import useFieldSize from '../../composables/useFieldSize.js';
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const {labelClass, fieldClass, defaultFieldClass} = useFieldSize();
|
||||
|
||||
const props = defineProps({
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
},
|
||||
rows: {
|
||||
default: function () {
|
||||
return 4;
|
||||
},
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
},
|
||||
hint: {
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const editor = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
editor.value = new EditorJS({
|
||||
placeholder: props.placeholder,
|
||||
holder: props.id,
|
||||
minHeight: 0,
|
||||
defaultBlock: 'paragraph',
|
||||
data: JSON.parse(JSON.stringify(props.modelValue)),
|
||||
tools: {
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
shortcut: 'CTRL+P',
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
preserveBlank: true,
|
||||
placeholder: 'Absatz',
|
||||
},
|
||||
},
|
||||
heading: {
|
||||
class: Header,
|
||||
shortcut: 'CTRL+H',
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
placeholder: 'Überschrift',
|
||||
levels: [2, 3, 4],
|
||||
defaultLevel: 2,
|
||||
},
|
||||
},
|
||||
list: {
|
||||
class: NestedList,
|
||||
shortcut: 'CTRL+L',
|
||||
inlineToolbar: true,
|
||||
},
|
||||
},
|
||||
onChange: debounce(async (api, event) => {
|
||||
const data = await editor.value.save();
|
||||
emit('update:modelValue', data);
|
||||
}, 500),
|
||||
});
|
||||
await editor.value.isReady;
|
||||
console.log('Editor is ready');
|
||||
});
|
||||
</script>
|
|
@ -85,8 +85,8 @@ export default {
|
|||
return Array.isArray(this.options)
|
||||
? this.options
|
||||
: map(this.options, (value, key) => {
|
||||
return { name: value, id: key };
|
||||
});
|
||||
return {name: value, id: key};
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -5,8 +5,20 @@
|
|||
<span v-show="required" class="text-red-800"> *</span>
|
||||
</span>
|
||||
<div class="real-field-wrap size-sm" :class="sizes[size].field">
|
||||
<input :name="name" :type="type" :value="transformedValue" :disabled="disabled" :placeholder="placeholder"
|
||||
@keypress="$emit('keypress', $event)" @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
|
||||
<input
|
||||
:name="name"
|
||||
:type="type"
|
||||
:value="transformedValue"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@keypress="$emit('keypress', $event)"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<div v-if="hint" class="info-wrap">
|
||||
<div v-tooltip="hint">
|
||||
<ui-sprite src="info-button" class="info-button"></ui-sprite>
|
||||
|
@ -281,6 +293,12 @@ export default {
|
|||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
min: {
|
||||
default: () => '',
|
||||
},
|
||||
max: {
|
||||
default: () => '',
|
||||
},
|
||||
name: {},
|
||||
},
|
||||
data: function () {
|
||||
|
@ -324,7 +342,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
if (typeof this.modelValue === 'undefined') {
|
||||
this.$emit('input', this.default === undefined ? '' : this.default);
|
||||
this.$emit('update:modelValue', this.default === undefined ? '' : this.default);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,104 +1,54 @@
|
|||
<template>
|
||||
<label class="flex flex-col relative">
|
||||
<span
|
||||
v-if="label && !inset"
|
||||
class="font-semibold text-gray-400"
|
||||
:class="{
|
||||
'text-xs': size == 'sm',
|
||||
'text-sm': size === null,
|
||||
}"
|
||||
>{{ label }}<span v-show="required" class="text-red-800"> *</span></span
|
||||
>
|
||||
<span
|
||||
v-if="label && inset"
|
||||
class="absolute top-0 left-0 -mt-2 px-1 ml-3 inset-bg font-semibold text-gray-700"
|
||||
:class="{
|
||||
'text-xs': size == 'sm',
|
||||
'text-sm': size === null,
|
||||
}"
|
||||
>{{ label }}<span v-show="required" class="text-red-800"> *</span></span
|
||||
>
|
||||
<textarea
|
||||
v-text="value"
|
||||
@input="trigger"
|
||||
:placeholder="placeholder"
|
||||
class="h-full outline-none bg-gray-700 border-gray-600 border-solid"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'rounded-lg text-sm border-2 p-2 text-gray-300': size === null,
|
||||
'rounded-lg py-2 px-2 text-xs border-2 text-gray-300': size == 'sm',
|
||||
}"
|
||||
></textarea>
|
||||
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
|
||||
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
|
||||
<label class="flex flex-col">
|
||||
<span v-if="label" class="font-semibold text-gray-400" :class="labelClass(size)">{{ label }}<span v-show="required" class="text-red-800"> *</span></span>
|
||||
<div class="relative w-full h-full">
|
||||
<textarea :placeholder="placeholder" class="h-full w-full outline-none" :class="[defaultFieldClass, fieldClass(size)]" :rows="rows" @input="trigger" v-text="modelValue"></textarea>
|
||||
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
|
||||
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
focus: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inset: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
},
|
||||
rows: {
|
||||
default: function () {
|
||||
return 4;
|
||||
},
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
},
|
||||
hint: {
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
default: undefined,
|
||||
},
|
||||
mask: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
required: false,
|
||||
default: function () {
|
||||
return 'text';
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
trigger(v) {
|
||||
this.$emit('input', v.target.value);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (typeof this.value === 'undefined') {
|
||||
this.$emit('input', '');
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script setup>
|
||||
import useFieldSize from '../../composables/useFieldSize.js';
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
<style scope>
|
||||
.inset-bg {
|
||||
background: linear-gradient(to bottom, hsl(247.5, 66.7%, 97.6%) 0%, hsl(247.5, 66.7%, 97.6%) 41%, hsl(0deg 0% 100%) 41%, hsl(180deg 0% 100%) 100%);
|
||||
const {labelClass, fieldClass, defaultFieldClass} = useFieldSize();
|
||||
|
||||
const props = defineProps({
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: null,
|
||||
},
|
||||
rows: {
|
||||
default: function () {
|
||||
return 4;
|
||||
},
|
||||
},
|
||||
id: {
|
||||
required: true,
|
||||
},
|
||||
hint: {
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
function trigger(v) {
|
||||
emit('update:modelValue', v.target.value);
|
||||
}
|
||||
</style>
|
||||
if (typeof props.modelValue === 'undefined') {
|
||||
emit('update:modelValue', '');
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div 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">
|
||||
<a v-for="(item, index) in entries" :key="index" href="#" class="rounded py-1 px-3 text-gray-400"
|
||||
:class="index === modelValue ? `bg-gray-600` : ''" @click.prevent="openMenu(index)" v-text="item.title"></a>
|
||||
</div>
|
||||
<slot name="bottom"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
modelValue: {},
|
||||
entries: {},
|
||||
},
|
||||
methods: {
|
||||
openMenu(index) {
|
||||
this.$emit('update:modelValue', index);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
|
||||
<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 overflow-auto" :class="full ? 'h-full' : innerWidth">
|
||||
<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"
|
||||
:class="full ? 'h-full' : innerWidth"
|
||||
>
|
||||
<div class="absolute top-0 right-0 mt-6 mr-6 flex space-x-6">
|
||||
<slot name="actions"></slot>
|
||||
<a href="#" @click.prevent="$emit('close')">
|
||||
|
@ -8,8 +11,18 @@
|
|||
</a>
|
||||
</div>
|
||||
<h3 v-if="heading" class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
|
||||
<div class="text-primary-100 group is-popup">
|
||||
<slot></slot>
|
||||
<div class="text-primary-100 group is-popup grow flex flex-col">
|
||||
<suspense>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #fallback>
|
||||
<div class="flex flex-col items-center justify-center h-full">
|
||||
<ui-spinner class="border-primary-400 w-32 h-32"></ui-spinner>
|
||||
<div class="text-3xl mt-10">Lade …</div>
|
||||
</div>
|
||||
</template>
|
||||
</suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue