Compare commits

..

No commits in common. "master" and "1.9.20" have entirely different histories.

390 changed files with 6588 additions and 12413 deletions

View File

@ -1,14 +1,13 @@
FROM php:8.3.11-fpm as php FROM php:8.1.6-fpm as php
WORKDIR /app WORKDIR /app
RUN ls /app RUN ls /app
RUN apt-get update RUN apt-get update
RUN apt-get install -y rsync libcurl3-dev apt-utils zlib1g-dev libpng-dev libicu-dev libonig-dev unzip poppler-utils libpng-dev libjpeg-dev default-mysql-client libzip-dev imagemagick libmagickwand-dev RUN apt-get install -y rsync libcurl3-dev apt-utils zlib1g-dev libpng-dev libicu-dev libonig-dev unzip poppler-utils libpng-dev libjpeg-dev default-mysql-client
RUN apt-get install -y --no-install-recommends texlive-base texlive-latex-base texlive-pictures texlive-latex-extra texlive-lang-german texlive-plain-generic texlive-fonts-recommended texlive-fonts-extra texlive-extra-utils RUN apt-get install -y --no-install-recommends texlive-base texlive-latex-base texlive-pictures texlive-latex-extra texlive-lang-german texlive-plain-generic texlive-fonts-recommended texlive-fonts-extra texlive-extra-utils
RUN docker-php-ext-install pdo_mysql curl exif intl mbstring pcntl zip RUN docker-php-ext-install pdo_mysql curl gd exif intl mbstring pcntl
RUN pecl install redis && docker-php-ext-enable redis RUN pecl install redis && docker-php-ext-enable redis
RUN pecl install imagick && docker-php-ext-enable imagick
RUN docker-php-ext-configure gd --with-jpeg RUN docker-php-ext-configure gd --with-jpeg
RUN docker-php-ext-install gd RUN docker-php-ext-enable gd
RUN usermod -s /bin/bash www-data RUN usermod -s /bin/bash www-data
RUN echo 'memory_limit = 2G' >> /usr/local/etc/php/conf.d/99-custom-php-memlimit.ini RUN echo 'memory_limit = 2G' >> /usr/local/etc/php/conf.d/99-custom-php-memlimit.ini

View File

@ -1,6 +0,0 @@
#!/bin/bash
docker buildx build -f .docker/base.Dockerfile .
docker image tag sha256:... zoomyboy/adrema-base
docker push zoomyboy/adrema-base

View File

@ -1,11 +1,11 @@
FROM composer:2.7.9 as composer FROM composer:2.2.7 as composer
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN composer install --ignore-platform-reqs --no-dev RUN composer install --ignore-platform-reqs --no-dev
RUN php artisan telescope:publish RUN php artisan telescope:publish
RUN php artisan horizon:publish RUN php artisan horizon:publish
FROM node:20.15.0-slim as node FROM node:18.13.0-slim as node
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules RUN npm install && npm run prod && npm run img && rm -R node_modules

View File

@ -1,9 +1,9 @@
FROM composer:2.7.9 as composer FROM composer:2.2.7 as composer
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN composer install --ignore-platform-reqs --no-dev RUN composer install --ignore-platform-reqs --no-dev
FROM node:20.15.0-slim as node FROM node:17.9.0-slim as node
WORKDIR /app WORKDIR /app
COPY . /app COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules RUN npm install && npm run prod && npm run img && rm -R node_modules
@ -20,7 +20,7 @@ RUN php artisan horizon:publish
USER root USER root
COPY ./.docker/php /bin COPY ./.docker/php /bin
VOLUME ["/app/packages/laravel-nami/.cookies", "/app/storage/app", "/app/resources/views/tex/invoice"] VOLUME ["/app/packages/laravel-nami/.cookies", "/app/storage/app"]
EXPOSE 9000 EXPOSE 9000

View File

@ -15,11 +15,6 @@ mkdir -p /app/storage/app/public || true
chown -R www-data:www-data /app/packages/laravel-nami/.cookies chown -R www-data:www-data /app/packages/laravel-nami/.cookies
chown -R www-data:www-data /app/storage/app chown -R www-data:www-data /app/storage/app
if [ $APP_KEY = "YOUR_APP_KEY" ]; then
echo "----------------------- Keinen APP KEY gefunden. Key wird generiert: $(su www-data -c 'php artisan key:generate --show') ----------------------- Füge diesen Key als APP_KEY ein ---------------------"
exit 1
fi
if [ $1 == "horizon" ]; then if [ $1 == "horizon" ]; then
wait_for_db wait_for_db
su www-data -c 'php artisan horizon' su www-data -c 'php artisan horizon'
@ -27,6 +22,10 @@ fi
if [ $1 == "app" ]; then if [ $1 == "app" ]; then
# --------------------------- ensure appkey is set ---------------------------- # --------------------------- ensure appkey is set ----------------------------
if [ $APP_KEY = "YOUR_APP_KEY" ]; then
echo "----------------------- Keinen APP KEY gefunden. Key wird generiert: $(su www-data -c 'php artisan key:generate --show') ----------------------- Füge diesen Key als APP_KEY ein ---------------------"
exit 1
fi
wait_for_db wait_for_db
php -r '$connection = new PDO("mysql:host='$DB_HOST';dbname='$DB_DATABASE'", "'$DB_USERNAME'", "'$DB_PASSWORD'"); $connection->query("DESCRIBE migrations");' > /dev/null || php artisan migrate --seed --force php -r '$connection = new PDO("mysql:host='$DB_HOST';dbname='$DB_DATABASE'", "'$DB_USERNAME'", "'$DB_PASSWORD'"); $connection->query("DESCRIBE migrations");' > /dev/null || php artisan migrate --seed --force
su www-data -c 'php artisan migrate --force' su www-data -c 'php artisan migrate --force'

View File

@ -12,7 +12,7 @@ steps:
- git submodule update --init --recursive - git submodule update --init --recursive
- name: composer_dev - name: composer_dev
image: composer:2.7.9 image: composer:2.2.7
commands: commands:
- composer install --ignore-platform-reqs --dev - composer install --ignore-platform-reqs --dev
@ -21,25 +21,14 @@ steps:
commands: commands:
- while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done - while ! mysqladmin ping -h db -u db -pdb --silent; do sleep 1; done
- name: ocdb_healthcheck
image: mysql:oracle
commands:
- while ! mysqladmin ping -h ownclouddb -u owncloud -powncloud --silent; do sleep 1; done
- name: oc_healthcheck
image: zoomyboy/adrema-base:latest
commands:
- while ! curl --silent 'http://owncloudserver:8080/ocs/v1.php/cloud/capabilities?format=json' -u admin:admin | grep '"status":"ok"'; do sleep 1; done
- name: node - name: node
image: node:20.15.0-slim image: node:18.13.0-slim
commands: commands:
- npm ci && cd packages/adrema-form && npm ci && npm run build && rm -R node_modules && cd ../../ && npm run img && npm run prod && rm -R node_modules - npm ci && cd packages/adrema-form && npm ci && npm run build && cd ../../ && npm run img && npm run prod && rm -R node_modules
- name: tests - name: tests
image: zoomyboy/adrema-base:latest image: zoomyboy/adrema-base:latest
commands: commands:
- touch .env
- php artisan migrate - php artisan migrate
- php artisan test - php artisan test
- rm -f .env - rm -f .env
@ -69,8 +58,6 @@ steps:
XELATEX_BIN: /usr/bin/xelatex XELATEX_BIN: /usr/bin/xelatex
SCOUT_DRIVER: database SCOUT_DRIVER: database
MEILI_MASTER_KEY: abc MEILI_MASTER_KEY: abc
TEST_OWNCLOUD_DOMAIN: http://owncloudserver:8080
TEST_NEXTCLOUD_DOMAIN: http://nextcloudserver:80
- name: docker_app_push - name: docker_app_push
image: plugins/docker image: plugins/docker
@ -103,7 +90,7 @@ steps:
event: tag event: tag
- name: deploy - name: deploy
image: zoomyboy/adrema-base:latest image: php:8.1.6
environment: environment:
SSH_KEY: SSH_KEY:
from_secret: deploy_private_key from_secret: deploy_private_key
@ -136,7 +123,7 @@ steps:
event: push event: push
- name: composer_no_dev - name: composer_no_dev
image: composer:2.7.9 image: composer:2.2.7
commands: commands:
- composer install --ignore-platform-reqs --no-dev - composer install --ignore-platform-reqs --no-dev
@ -173,47 +160,6 @@ services:
image: getmeili/meilisearch:v1.6 image: getmeili/meilisearch:v1.6
commands: commands:
- meilisearch --master-key="abc" - meilisearch --master-key="abc"
- name: ownclouddb
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: owncloud
MYSQL_USER: owncloud
MYSQL_PASSWORD: owncloud
MYSQL_DATABASE: owncloud
MARIADB_AUTO_UPGRADE: 1
- name: owncloudserver
image: owncloud/server:10.10.0
environment:
OWNCLOUD_DOMAIN: http://owncloudserver:8080
OWNCLOUD_TRUSTED_DOMAINS: owncloudserver
OWNCLOUD_DB_TYPE: mysql
OWNCLOUD_DB_NAME: owncloud
OWNCLOUD_DB_USERNAME: owncloud
OWNCLOUD_DB_PASSWORD: owncloud
OWNCLOUD_DB_HOST: ownclouddb
OWNCLOUD_ADMIN_USERNAME: admin
OWNCLOUD_ADMIN_PASSWORD: admin
OWNCLOUD_MYSQL_UTF8MB4: true
OWNCLOUD_REDIS_ENABLED: false
OWNCLOUD_REDIS_HOST: false
- name: nextclouddb
image: mariadb:10.11
environment:
MYSQL_ROOT_PASSWORD: nextcloud
MYSQL_USER: nextcloud
MYSQL_PASSWORD: nextcloud
MYSQL_DATABASE: nextcloud
MARIADB_AUTO_UPGRADE: 1
- name: nextcloudserver
image: nextcloud
environment:
MYSQL_PASSWORD: nextcloud
MYSQL_DATABASE: nextcloud
MYSQL_USER: nextcloud
MYSQL_HOST: nextclouddb
NEXTCLOUD_ADMIN_USER: admin
NEXTCLOUD_ADMIN_PASSWORD: admin
NEXTCLOUD_TRUSTED_DOMAINS: nextcloudserver
trigger: trigger:
event: event:

3
.gitignore vendored
View File

@ -8,7 +8,6 @@ yarn-error.log
/public/build /public/build
/public/vendor /public/vendor
storage/*.key storage/*.key
/storage/media-library/*
/vendor/ /vendor/
Homestead.yaml Homestead.yaml
Homestead.json Homestead.json
@ -16,7 +15,6 @@ Homestead.json
.phpunit.result.cache .phpunit.result.cache
/storage/temp/ /storage/temp/
/storage/debugbar/ /storage/debugbar/
/tests/Fileshare/oc_tmp/*
# User data files # User data files
/data/ /data/
@ -40,4 +38,3 @@ Homestead.json
/public/sprite.svg /public/sprite.svg
/.php-cs-fixer.cache /.php-cs-fixer.cache
/groups.sql /groups.sql
/.phpunit.cache

9
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "packages/silvaletter"]
path = packages/silvaletter
url = https://git.zoomyboy.de/silva/silvaletter.git
[submodule "packages/laravel-nami"] [submodule "packages/laravel-nami"]
path = packages/laravel-nami path = packages/laravel-nami
url = https://git.zoomyboy.de/silva/laravel-nami-api url = https://git.zoomyboy.de/silva/laravel-nami-api
@ -11,9 +14,3 @@
path = packages/medialibrary-helper path = packages/medialibrary-helper
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
branch = version2 branch = version2
[submodule "packages/flysystem-webdav"]
path = packages/flysystem-webdav
url = https://github.com/zoomyboy/flysystem-webdav.git
[submodule "packages/table-document"]
path = packages/table-document
url = https://git.zoomyboy.de/zoomyboy/table-document.git

View File

@ -1,94 +0,0 @@
# Letzte Änderungen
### 1.12.2
- Zuschussliste Gallier
### 1.12.1
- In Teilnehmer-Liste von Veranstaltungen kann nun sortiert und gefiltert werden.
- Formulare: Feld Registrierung von / bis
### 1.11.5
- Fix: Synchronisation von NaMi-Mitgliedern
### 1.11.4
- Fix: Nicht an Präventions-Unterlagen für vergangene Veranstaltungen erinnern
### 1.11.1
- Es kann nun auch das Feld "Datenweiterverwendung" über Adrema gepflegt werden.
### 1.10.20
- Fixed: Bei Textfeldern wird nun die Einleitung dargestellt
### 1.10.19
- Fixed: Erweiterte Führungszeugnisse und Präventionsschulungen nur für aktive Mitgliedschaften auf Dashboard anzeigen
### 1.10.18
- Fixed: All Gruppen als Option anbieten bei Bedingungen
### 1.10.17
- Es können nun auch Bedingungen für Felder vom Typ Gruppierung definiert werden
### 1.10.16
- Rechnungen und Erinnerungen werden nun automatisch täglich um 10 Uhr verschickt
- Es kann eingestellt werden, nach wie vielen Wochen an Rechnungen erinnert werden soll (Standard: 12)
- Name und Profilbild des angemeldeten Benutzers wird nun oben rechts angezeigt
### 1.10.15
- "Für Mitglieder zusätzlich abfragen" kann nun im Formular auch gesetzt werden, wenn ein NaMi Feld ausgewählt ist.
### 1.10.14
- Fixed: Ist eine Präventionsschulung älter als 5 Jahre, so ist nur eine Auffrischungs-Schulung erforderlich
### 1.10.13
- Bei Veranstaltungs-Übersicht alle Veranstaltungen anzeigen
- Download von Teilnehmern als Tabellen-Dokument
- Spalte "Prävention" bei TN-Liste eingefügt für benötigte Unterlagen
### 1.10.11
- Bei Prävention auch an Verhaltenskodex erinnern
### 1.10.10
- Eine Formular-Vorlage kann nun auch Mail-Inhalte enthalten, die für die Formulare übernommen werden
### 1.10.9
- Nextcloud als neuen Type bei Datei-Verbindungen angelegt
### 1.10.8
- Anmeldeformulare: Bearbeiten von Teilnehmer\*innen ist nun möglich
### 1.10.7
- Anmeldeformulare: Versenden von Präventions-Unterlagen können nun an eine Feld-Bedingung geknüpft werden innerhalb der Veranstaltung
### 1.10.6
- Kleinere Fehler behoben
### 1.10.5
- Kleinere Fehler behoben
### 1.10.4
- Anmeldeformular: Erinnerung an Präventions-Unterlagen bei Teilnehmer\*innnen
### 1.10.3
- Anmeldeformulare: Ist NaMi-Feld "E-Mail" ausgewählt bei Formular-Feldern, muss die E-Mail-Adresse nun nicht mehr auf das Mitglied matchen. Dies ist nur noch bei Formular-Feldern mit NaMi-Feldern "Vorname", "Nachname" und "Geburtsdatum" der Fall.

View File

@ -85,12 +85,6 @@ Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles
7. Nach kurzer Zeit ist AdReMa über <http://localhost:8000> erreichbar und es kann sich mit dem zuvor festgelegten Login eingeloggt werden 7. Nach kurzer Zeit ist AdReMa über <http://localhost:8000> erreichbar und es kann sich mit dem zuvor festgelegten Login eingeloggt werden
### Individuelle anpassungen
#### Rechnungswesen
Bei dem Setup wird im Daten-Verzeichniss ein Ordner `./data/setup` angelegt. Hier kann das Logo des Stammes in den Briefkopf eingefügt werden. Zusätzlich kann der Text der Rechnung und der Zahlungserinnerung angepasst werden, dafür ist ein grundlegendes Verständnis für `.tex` Datein erforderlich.
## Nutzen des Entwicklungssystmes ## Nutzen des Entwicklungssystmes
1. Klonen des Reposetories 1. Klonen des Reposetories

View File

@ -1,23 +0,0 @@
<?php
namespace App\Actions;
use DB;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Http;
use Laravel\Telescope\Console\PruneCommand;
use Lorisleiva\Actions\Concerns\AsAction;
class DbMaintainAction
{
use AsAction;
public string $commandSignature = 'db:maintain';
public function handle(): void
{
Artisan::call(PruneCommand::class, ['--hours' => 168]); // 168h = 7 Tage
DB::select('optimize table telescope_entries');
Http::post('https://zoomyboy.de/maintain', ['url' => url()->current()]);
}
}

View File

@ -50,7 +50,6 @@ class InsertMemberAction
'nationality_id' => Nationality::where('nami_id', $member->nationalityId)->firstOrFail()->id, 'nationality_id' => Nationality::where('nami_id', $member->nationalityId)->firstOrFail()->id,
'mitgliedsnr' => $member->memberId, 'mitgliedsnr' => $member->memberId,
'version' => $member->version, 'version' => $member->version,
'keepdata' => $member->keepdata,
]); ]);
} }

View File

@ -4,14 +4,12 @@ namespace App;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Database\Factories\ActivityFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Activity extends Model class Activity extends Model
{ {
/** @use HasFactory<ActivityFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;
use Sluggable; use Sluggable;

View File

@ -2,13 +2,11 @@
namespace App; namespace App;
use Database\Factories\ConfessionFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Confession extends Model class Confession extends Model
{ {
/** @use HasFactory<ConfessionFactory> */
use HasFactory; use HasFactory;
public $fillable = ['name', 'nami_id', 'is_null']; public $fillable = ['name', 'nami_id', 'is_null'];

View File

@ -2,12 +2,11 @@
namespace App\Console; namespace App\Console;
use App\Actions\DbMaintainAction;
use App\Form\Actions\PreventionRememberAction;
use App\Initialize\InitializeMembers; use App\Initialize\InitializeMembers;
use App\Invoice\Actions\InvoiceSendAction; use App\Invoice\Actions\InvoiceSendAction;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Laravel\Telescope\Console\PruneCommand;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@ -19,8 +18,6 @@ class Kernel extends ConsoleKernel
protected $commands = [ protected $commands = [
InvoiceSendAction::class, InvoiceSendAction::class,
InitializeMembers::class, InitializeMembers::class,
DbMaintainAction::class,
PreventionRememberAction::class,
]; ];
/** /**
@ -30,10 +27,8 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
$schedule->command(DbMaintainAction::class)->daily(); $schedule->command(PruneCommand::class, ['--hours' => 168])->daily(); // 168h = 7 Tage
$schedule->command(InitializeMembers::class)->dailyAt('03:00'); $schedule->command(InitializeMembers::class)->dailyAt('03:00');
$schedule->command(PreventionRememberAction::class)->dailyAt('11:00');
$schedule->command(InvoiceSendAction::class)->dailyAt('10:00');
} }
/** /**

View File

@ -8,7 +8,6 @@ use App\Contribution\Documents\RdpNrwDocument;
use App\Contribution\Documents\CityRemscheidDocument; use App\Contribution\Documents\CityRemscheidDocument;
use App\Contribution\Documents\CitySolingenDocument; use App\Contribution\Documents\CitySolingenDocument;
use App\Contribution\Documents\CityFrankfurtMainDocument; use App\Contribution\Documents\CityFrankfurtMainDocument;
use App\Contribution\Documents\WuppertalDocument;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
@ -23,16 +22,15 @@ class ContributionFactory
CityRemscheidDocument::class, CityRemscheidDocument::class,
CityFrankfurtMainDocument::class, CityFrankfurtMainDocument::class,
BdkjHesse::class, BdkjHesse::class,
WuppertalDocument::class,
]; ];
/** /**
* @return Collection<int, array{title: string, class: class-string<ContributionDocument>}> * @return Collection<int, array{title: mixed, class: mixed}>
*/ */
public function compilerSelect(): Collection public function compilerSelect(): Collection
{ {
return collect($this->documents)->map(fn ($document) => [ return collect($this->documents)->map(fn ($document) => [
'title' => $document::buttonName(), 'title' => $document::getName(),
'class' => $document, 'class' => $document,
]); ]);
} }

View File

@ -29,7 +29,7 @@ class MemberData extends Data
*/ */
public static function fromModels(array $ids): Collection public static function fromModels(array $ids): Collection
{ {
return Member::whereIn('id', $ids)->orderByRaw('lastname, firstname')->get()->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([ return Member::whereIn('id', $ids)->orderByRaw('lastname, firstname')->get()->map(fn ($member) => self::withoutMagicalCreationFrom([
...$member->toArray(), ...$member->toArray(),
'birthday' => $member->birthday->toAtomString(), 'birthday' => $member->birthday->toAtomString(),
'isLeader' => $member->isLeader(), 'isLeader' => $member->isLeader(),
@ -44,7 +44,7 @@ class MemberData extends Data
*/ */
public static function fromApi(array $data): Collection public static function fromApi(array $data): Collection
{ {
return collect($data)->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([ return collect($data)->map(fn ($member) => self::withoutMagicalCreationFrom([
...$member, ...$member,
'birthday' => Carbon::parse($member['birthday'])->toAtomString(), 'birthday' => Carbon::parse($member['birthday'])->toAtomString(),
'gender' => Gender::fromString($member['gender']), 'gender' => Gender::fromString($member['gender']),
@ -72,23 +72,13 @@ class MemberData extends Data
return $this->zip.' '.$this->location; return $this->zip.' '.$this->location;
} }
public function age(): int public function age(): string
{ {
return intval($this->birthday->diffInYears(now())); return (string) $this->birthday->diffInYears(now()) ?: '';
} }
public function birthYear(): string public function birthYear(): string
{ {
return (string) $this->birthday->year; return (string) $this->birthday->year;
} }
public function birthdayHuman(): string
{
return $this->birthday->format('d.m.Y');
}
public function genderLetter(): string
{
return $this->gender->short;
}
} }

View File

@ -3,16 +3,15 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData; use App\Contribution\Data\MemberData;
use App\Contribution\Traits\HasPdfBackground;
use App\Country; use App\Country;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class BdkjHesse extends ContributionDocument class BdkjHesse extends ContributionDocument
{ {
use HasPdfBackground;
/** /**
* @param Collection<int, Collection<int, MemberData>> $members * @param Collection<int, Collection<int, MemberData>> $members
*/ */
@ -26,7 +25,6 @@ class BdkjHesse extends ContributionDocument
public ?string $filename = '', public ?string $filename = '',
public string $type = 'F', public string $type = 'F',
) { ) {
$this->setEventName($eventName);
} }
public function dateFrom(): string public function dateFrom(): string
@ -76,7 +74,7 @@ class BdkjHesse extends ContributionDocument
public function durationDays(): int public function durationDays(): int
{ {
return intVal(Carbon::parse($this->dateUntil)->diffInDays(Carbon::parse($this->dateFrom))) + 1; return Carbon::parse($this->dateUntil)->diffInDays(Carbon::parse($this->dateFrom)) + 1;
} }
/** /**
@ -116,9 +114,36 @@ class BdkjHesse extends ContributionDocument
return $member->birthYear(); return $member->birthYear();
} }
public function basename(): string
{
return 'zuschuesse-bdkj-hessen' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.bdkj-hesse';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string public static function getName(): string
{ {
return 'BDKJ Hessen'; return 'Für BDKJ Hessen erstellen';
} }
/** /**
@ -131,6 +156,7 @@ class BdkjHesse extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d', 'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id', 'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string', 'zipLocation' => 'required|string',
'eventName' => 'required|string',
]; ];
} }
} }

View File

@ -3,17 +3,16 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData; use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country; use App\Country;
use App\Invoice\InvoiceSettings; use App\Invoice\InvoiceSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CityFrankfurtMainDocument extends ContributionDocument class CityFrankfurtMainDocument extends ContributionDocument
{ {
use HasPdfBackground;
use FormatsDates;
public string $fromName; public string $fromName;
/** /**
@ -29,7 +28,6 @@ class CityFrankfurtMainDocument extends ContributionDocument
public ?string $filename = '', public ?string $filename = '',
public string $type = 'F', public string $type = 'F',
) { ) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long; $this->fromName = app(InvoiceSettings::class)->from_long;
} }
@ -63,6 +61,17 @@ class CityFrankfurtMainDocument extends ContributionDocument
); );
} }
public function dateFromHuman(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function dateUntilHuman(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function countryName(): string public function countryName(): string
{ {
return $this->country->name; return $this->country->name;
@ -73,9 +82,56 @@ class CityFrankfurtMainDocument extends ContributionDocument
return count($this->members); return count($this->members);
} }
public function memberShort(MemberData $member): string
{
return $member->isLeader ? 'L' : '';
}
public function memberName(MemberData $member): string
{
return $member->separatedName();
}
public function memberAddress(MemberData $member): string
{
return $member->fullAddress();
}
public function memberAge(MemberData $member): string
{
return $member->age();
}
public function basename(): string
{
return 'zuschuesse-frankfurt-' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.city-frankfurt-main';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string public static function getName(): string
{ {
return 'Frankfurt'; return 'Für Frankfurt erstellen';
} }
/** /**
@ -88,6 +144,7 @@ class CityFrankfurtMainDocument extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d', 'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id', 'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string', 'zipLocation' => 'required|string',
'eventName' => 'required|string',
]; ];
} }
} }

View File

@ -3,17 +3,15 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData; use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country; use App\Country;
use App\Member\Member; use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CityRemscheidDocument extends ContributionDocument class CityRemscheidDocument extends ContributionDocument
{ {
use HasPdfBackground;
use FormatsDates;
/** /**
* @param Collection<int, Collection<int, Member>> $leaders * @param Collection<int, Collection<int, Member>> $leaders
* @param Collection<int, Collection<int, Member>> $children * @param Collection<int, Collection<int, Member>> $children
@ -27,9 +25,17 @@ class CityRemscheidDocument extends ContributionDocument
public Collection $children, public Collection $children,
public ?string $filename = '', public ?string $filename = '',
public string $type = 'F', public string $type = 'F',
public string $eventName = '',
) { ) {
$this->setEventName($eventName); }
public function niceDateFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceDateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
} }
/** /**
@ -46,7 +52,6 @@ class CityRemscheidDocument extends ContributionDocument
country: Country::where('id', $request['country'])->firstOrFail(), country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6), leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20), children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
); );
} }
@ -65,13 +70,39 @@ class CityRemscheidDocument extends ContributionDocument
country: Country::where('id', $request['country'])->firstOrFail(), country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6), leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20), children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
); );
} }
public function basename(): string
{
return 'zuschuesse-remscheid';
}
public function view(): string
{
return 'tex.contribution.city-remscheid';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string public static function getName(): string
{ {
return 'Remscheid'; return 'Für Remscheid erstellen';
} }
/** /**

View File

@ -6,7 +6,9 @@ use App\Contribution\Data\MemberData;
use App\Invoice\InvoiceSettings; use App\Invoice\InvoiceSettings;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine; use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class CitySolingenDocument extends ContributionDocument class CitySolingenDocument extends ContributionDocument
{ {
@ -23,7 +25,6 @@ class CitySolingenDocument extends ContributionDocument
public string $eventName, public string $eventName,
public string $type = 'F', public string $type = 'F',
) { ) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long; $this->fromName = app(InvoiceSettings::class)->from_long;
} }
@ -73,6 +74,11 @@ class CitySolingenDocument extends ContributionDocument
return Carbon::parse($this->dateUntil)->format('d.m.Y'); return Carbon::parse($this->dateUntil)->format('d.m.Y');
} }
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function checkboxes(): string public function checkboxes(): string
{ {
$output = ''; $output = '';
@ -88,6 +94,16 @@ class CitySolingenDocument extends ContributionDocument
return $firstRow . "\n" . $secondRow; return $firstRow . "\n" . $secondRow;
} }
public function basename(): string
{
return 'zuschuesse-solingen-' . Str::slug($this->eventName);
}
public function view(): string
{
return 'tex.contribution.city-solingen';
}
public function getEngine(): Engine public function getEngine(): Engine
{ {
return Engine::PDFLATEX; return Engine::PDFLATEX;
@ -95,7 +111,7 @@ class CitySolingenDocument extends ContributionDocument
public static function getName(): string public static function getName(): string
{ {
return 'Stadt Solingen'; return 'Für Stadt Solingen erstellen';
} }
/** /**
@ -107,6 +123,7 @@ class CitySolingenDocument extends ContributionDocument
'dateFrom' => 'required|string|date_format:Y-m-d', 'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d', 'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string', 'zipLocation' => 'required|string',
'eventName' => 'required|string',
]; ];
} }
} }

View File

@ -3,12 +3,9 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use Zoomyboy\Tex\Document; use Zoomyboy\Tex\Document;
use Zoomyboy\Tex\Template;
abstract class ContributionDocument extends Document abstract class ContributionDocument extends Document
{ {
private string $eventName;
abstract public static function getName(): string; abstract public static function getName(): string;
/** /**
@ -32,34 +29,8 @@ abstract class ContributionDocument extends Document
public static function globalRules(): array public static function globalRules(): array
{ {
return [ return [
'eventName' => 'required|string',
'members' => 'present|array|min:1', 'members' => 'present|array|min:1',
'members.*' => 'integer|exists:members,id', 'members.*' => 'integer|exists:members,id',
]; ];
} }
public static function buttonName(): string
{
return 'Für ' . static::getName() . ' erstellen';;
}
public function setEventName(string $eventName): void
{
$this->eventName = $eventName;
}
public function basename(): string
{
return str('Zuschüsse ')->append($this->getName())->append(' ')->append($this->eventName)->slug();
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function view(): string
{
return 'tex.contribution.' . str(class_basename(static::class))->replace('Document', '')->kebab()->toString();
}
} }

View File

@ -3,16 +3,14 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData; use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country; use App\Country;
use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class RdpNrwDocument extends ContributionDocument class RdpNrwDocument extends ContributionDocument
{ {
use HasPdfBackground;
use FormatsDates;
/** /**
* @param Collection<int, Collection<int, MemberData>> $members * @param Collection<int, Collection<int, MemberData>> $members
*/ */
@ -24,9 +22,14 @@ class RdpNrwDocument extends ContributionDocument
public Collection $members, public Collection $members,
public ?string $filename = '', public ?string $filename = '',
public string $type = 'F', public string $type = 'F',
public string $eventName = '',
) { ) {
$this->setEventName($eventName); }
public function dateRange(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y')
. ' - '
. Carbon::parse($this->dateUntil)->format('d.m.Y');
} }
/** /**
@ -40,7 +43,6 @@ class RdpNrwDocument extends ContributionDocument
zipLocation: $request['zipLocation'], zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(), country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(17), members: MemberData::fromModels($request['members'])->chunk(17),
eventName: $request['eventName'],
); );
} }
@ -55,7 +57,6 @@ class RdpNrwDocument extends ContributionDocument
zipLocation: $request['zipLocation'], zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(), country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromApi($request['member_data'])->chunk(17), members: MemberData::fromApi($request['member_data'])->chunk(17),
eventName: $request['eventName'],
); );
} }
@ -64,9 +65,65 @@ class RdpNrwDocument extends ContributionDocument
return $this->country->name; return $this->country->name;
} }
public function memberShort(MemberData $member): string
{
return $member->isLeader ? 'L' : '';
}
public function memberName(MemberData $member): string
{
return $member->separatedName();
}
public function memberAddress(MemberData $member): string
{
return $member->fullAddress();
}
public function memberGender(MemberData $member): string
{
if (!$member->gender) {
return '';
}
return strtolower(substr($member->gender->name, 0, 1));
}
public function memberAge(MemberData $member): string
{
return $member->age();
}
public function basename(): string
{
return 'zuschuesse-rdp-nrw';
}
public function view(): string
{
return 'tex.contribution.rdp-nrw';
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function setFilename(string $filename): static
{
$this->filename = $filename;
return $this;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string public static function getName(): string
{ {
return 'RdP NRW'; return 'Für RdP NRW erstellen';
} }
/** /**
@ -79,6 +136,7 @@ class RdpNrwDocument extends ContributionDocument
'dateUntil' => 'required|string|date_format:Y-m-d', 'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id', 'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string', 'zipLocation' => 'required|string',
'eventName' => 'required|string',
]; ];
} }
} }

View File

@ -1,79 +0,0 @@
<?php
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\FormatsDates;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use Illuminate\Support\Collection;
class WuppertalDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
/**
* @param Collection<int, Collection<int, MemberData>> $members
*/
public function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $members,
public ?string $filename = '',
public string $type = 'F',
public string $eventName = '',
) {
$this->setEventName($eventName);
}
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromModels($request['members'])->chunk(14),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): self
{
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
members: MemberData::fromApi($request['member_data'])->chunk(14),
eventName: $request['eventName'],
);
}
public static function getName(): string
{
return 'Wuppertal';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string',
];
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Contribution\Traits;
use Carbon\Carbon;
trait FormatsDates
{
public function niceDateFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceDateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function dateRange(): string
{
return implode(' - ', [$this->niceDateFrom(), $this->niceDateUntil()]);
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Contribution\Traits;
use Zoomyboy\Tex\Engine;
trait HasPdfBackground
{
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
}

View File

@ -3,13 +3,11 @@
namespace App; namespace App;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Database\Factories\CountryFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Country extends Model class Country extends Model
{ {
/** @use HasFactory<CountryFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;

View File

@ -3,13 +3,11 @@
namespace App\Course\Models; namespace App\Course\Models;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Database\Factories\Course\Models\CourseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Course extends Model class Course extends Model
{ {
/** @use HasFactory<CourseFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;

View File

@ -3,14 +3,12 @@
namespace App\Course\Models; namespace App\Course\Models;
use App\Member\Member; use App\Member\Member;
use Database\Factories\Course\Models\CourseMemberFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CourseMember extends Model class CourseMember extends Model
{ {
/** @use HasFactory<CourseMemberFactory> */
use HasFactory; use HasFactory;
/** @var array<int, string> */ /** @var array<int, string> */

View File

@ -2,9 +2,7 @@
namespace App\Dashboard; namespace App\Dashboard;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
class DashboardServiceProvider extends ServiceProvider class DashboardServiceProvider extends ServiceProvider
{ {
@ -25,8 +23,5 @@ class DashboardServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
app(Router::class)->middleware(['web', 'auth:web'])->group(function ($router) {
$router->get('/', DashboardIndexAction::class)->name('home');
});
} }
} }

View File

@ -8,6 +8,9 @@ use Sabre\CardDAV\Backend\AbstractBackend;
use Sabre\DAV\PropPatch; use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCard; use Sabre\VObject\Component\VCard;
/**
* @template M as array{lastmodified: int, etag: string, uri: string, id: int, size: int}
*/
class AddressBookBackend extends AbstractBackend class AddressBookBackend extends AbstractBackend
{ {
/** /**
@ -112,7 +115,7 @@ class AddressBookBackend extends AbstractBackend
* *
* @param mixed $addressbookId * @param mixed $addressbookId
* *
* @return array<int, AddressBookCard> * @return array<int, M>
*/ */
public function getCards($addressbookId): array public function getCards($addressbookId): array
{ {
@ -130,7 +133,7 @@ class AddressBookBackend extends AbstractBackend
* @param mixed $addressBookId * @param mixed $addressBookId
* @param string $cardUri * @param string $cardUri
* *
* @return AddressBookCard|bool * @return M
*/ */
public function getCard($addressBookId, $cardUri) public function getCard($addressBookId, $cardUri)
{ {
@ -155,9 +158,8 @@ class AddressBookBackend extends AbstractBackend
* If the backend supports this, it may allow for some speed-ups. * If the backend supports this, it may allow for some speed-ups.
* *
* @param mixed $addressBookId * @param mixed $addressBookId
* @param array<int, string> $uris
* *
* @return array<int, mixed> * @return array
*/ */
public function getMultipleCards($addressBookId, array $uris) public function getMultipleCards($addressBookId, array $uris)
{ {
@ -246,7 +248,7 @@ class AddressBookBackend extends AbstractBackend
} }
/** /**
* @return AddressBookCard * @return M
*/ */
private function cardMeta(Member $member): array private function cardMeta(Member $member): array
{ {

View File

@ -136,11 +136,10 @@ class Principal implements PrincipalBackendInterface
* *
* @param string $principal * @param string $principal
* *
* @return array<int, string>|null * @return array
*/ */
public function getGroupMemberSet($principal) public function getGroupMemberSet($principal)
{ {
return [];
} }
/** /**
@ -148,7 +147,7 @@ class Principal implements PrincipalBackendInterface
* *
* @param string $principal * @param string $principal
* *
* @return array<int, string>|null * @return array
*/ */
public function getGroupMembership($principal) public function getGroupMembership($principal)
{ {
@ -165,8 +164,6 @@ class Principal implements PrincipalBackendInterface
* The principals should be passed as a list of uri's. * The principals should be passed as a list of uri's.
* *
* @param string $principal * @param string $principal
* @param array<int, string> $members
* @return void
*/ */
public function setGroupMemberSet($principal, array $members) public function setGroupMemberSet($principal, array $members)
{ {
@ -178,7 +175,7 @@ class Principal implements PrincipalBackendInterface
private function userToPrincipal(User $user): array private function userToPrincipal(User $user): array
{ {
return [ return [
'{DAV:}displayname' => $user->firstname . ' ' . $user->lastname, '{DAV:}displayname' => $user->name,
'uri' => 'principals/'.$user->email, 'uri' => 'principals/'.$user->email,
'{http://sabredav.org/ns}email-address' => $user->email, '{http://sabredav.org/ns}email-address' => $user->email,
]; ];

View File

@ -9,7 +9,6 @@ use Sabre\CardDAV\AddressBookRoot;
use Sabre\CardDAV\Plugin as CardDAVPlugin; use Sabre\CardDAV\Plugin as CardDAVPlugin;
use Sabre\DAV\Auth\Plugin as AuthPlugin; use Sabre\DAV\Auth\Plugin as AuthPlugin;
use Sabre\DAV\Browser\Plugin as BrowserPlugin; use Sabre\DAV\Browser\Plugin as BrowserPlugin;
use Sabre\DAV\ServerPlugin;
use Sabre\DAVACL\AbstractPrincipalCollection; use Sabre\DAVACL\AbstractPrincipalCollection;
use Sabre\DAVACL\Plugin as AclPlugin; use Sabre\DAVACL\Plugin as AclPlugin;
use Sabre\DAVACL\PrincipalCollection; use Sabre\DAVACL\PrincipalCollection;
@ -51,9 +50,6 @@ class ServiceProvider extends BaseServiceProvider
]; ];
} }
/**
* @return array<int, ServerPlugin>
*/
private function plugins(): array private function plugins(): array
{ {
$authBackend = new AuthBackend(); $authBackend = new AuthBackend();

View File

@ -19,7 +19,7 @@ class EfzPendingBlock extends Block
}) })
->whereCurrentGroup() ->whereCurrentGroup()
->orderByRaw('lastname, firstname') ->orderByRaw('lastname, firstname')
->whereHas('memberships', fn ($builder) => $builder->isLeader()->active()); ->whereHas('memberships', fn ($builder) => $builder->isLeader());
} }
/** /**

View File

@ -12,14 +12,15 @@ class Handler extends ExceptionHandler
/** /**
* A list of the exception types that are not reported. * A list of the exception types that are not reported.
* *
* @var array<int, class-string<Throwable>> * @var string[]
*/ */
protected $dontReport = []; protected $dontReport = [
];
/** /**
* A list of the inputs that are never flashed for validation exceptions. * A list of the inputs that are never flashed for validation exceptions.
* *
* @var array<int, string> * @var string[]
*/ */
protected $dontFlash = [ protected $dontFlash = [
'password', 'password',

View File

@ -4,14 +4,12 @@ namespace App;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use App\Payment\Subscription; use App\Payment\Subscription;
use Database\Factories\FeeFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class Fee extends Model class Fee extends Model
{ {
/** @use HasFactory<FeeFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;

View File

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

View File

@ -1,42 +0,0 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Lib\Events\Succeeded;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareStoreAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'type' => 'required|string|max:255|exclude',
'config' => 'array|exclude',
];
}
public function asController(ActionRequest $request): void
{
$type = $request->input('type')::from($request->input('config'));
if (!$type->check()) {
throw ValidationException::withMessages(['type' => 'Verbindung fehlgeschlagen']);
}
Fileshare::create([
...$request->validated(),
'type' => $type,
]);
Succeeded::message('Verbindung erstellt.')->dispatch();
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Models\Fileshare;
use App\Lib\Events\Succeeded;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FileshareUpdateAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'type' => 'required|string|max:255|exclude',
'config' => 'array|exclude',
];
}
public function handle(ActionRequest $request, Fileshare $fileshare): void
{
$type = $request->input('type')::from($request->input('config'));
if (!$type->check()) {
throw ValidationException::withMessages(['type' => 'Verbindung fehlgeschlagen']);
}
$fileshare->update([
...$request->validated(),
'type' => $type,
]);
Succeeded::message('Verbindung bearbeitet.')->dispatch();
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Fileshare\Actions;
use App\Fileshare\Data\ResourceData;
use App\Fileshare\Models\Fileshare;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\LaravelData\DataCollection;
class ListFilesAction
{
use AsAction;
/**
* @return DataCollection<int, ResourceData>
*/
public function handle(ActionRequest $request, Fileshare $fileshare): DataCollection
{
return ResourceData::collect($fileshare->type->getSubDirectories($request->input('parent')), DataCollection::class)->wrap('data');
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace App\Fileshare\ConnectionTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
abstract class ConnectionType extends Data
{
abstract public function check(): bool;
/**
* @return array<string, mixed>
*/
abstract public static function defaults(): array;
abstract public static function title(): string;
abstract public function getFilesystem(): FilesystemAdapter;
/**
* @return array<int, array{label: string, key: string, type: string}>
*/
abstract public static function fields(): array;
/**
* @return array<int, mixed>
*/
public static function forSelect(): array
{
return self::types()
->map(fn ($file) => ['id' => $file, 'name' => $file::title(), 'defaults' => $file::defaults(), 'fields' => $file::fields()])
->toArray();
}
/**
* @return array<int, string>
*/
public function getSubDirectories(?string $parent): array
{
$filesystem = $this->getFilesystem();
return $filesystem->directories($parent ?: '/');
}
/**
* @return Collection<int, class-string<ConnectionType>>
*/
private static function types(): Collection
{
return collect(glob(base_path('app/Fileshare/ConnectionTypes/*')))
->map(fn ($file) => 'App\\Fileshare\\ConnectionTypes\\' . pathinfo($file, PATHINFO_FILENAME))
->filter(fn ($file) => $file !== static::class)
->values();
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Fileshare\ConnectionTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use Sabre\DAV\Client;
class NextcloudConnection extends OwncloudConnection
{
public function check(): bool
{
try {
$response = Http::withoutVerifying()
->withBasicAuth($this->user, $this->password)
->withHeaders(['OCS-APIRequest' => 'true'])
->acceptJson()
->get($this->baseUrl . '/ocs/v2.php/cloud/capabilities');
return $response->ok();
} catch (ConnectionException $e) {
return false;
}
}
public static function title(): string
{
return 'Nextcloud';
}
public function getFilesystem(): FilesystemAdapter
{
$adapter = new WebDAVAdapter(new Client([
'baseUri' => $this->baseUrl . '/remote.php/dav/files/' . $this->user,
'userName' => $this->user,
'password' => $this->password,
]), '/remote.php/dav/files/' . $this->user);
return new FilesystemAdapter(new Filesystem($adapter), $adapter);
}
}

View File

@ -1,76 +0,0 @@
<?php
namespace App\Fileshare\ConnectionTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use Sabre\DAV\Client;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class OwncloudConnection extends ConnectionType
{
public function __construct(
public string $user,
public string $password,
public string $baseUrl,
) {
}
public function check(): bool
{
try {
$response = Http::withoutVerifying()->withBasicAuth($this->user, $this->password)->acceptJson()->get($this->baseUrl . '/ocs/v1.php/cloud/capabilities?format=json');
return $response->ok();
} catch (ConnectionException $e) {
return false;
}
}
/**
* @inheritdoc
*/
public static function defaults(): array
{
return [
'user' => '',
'password' => '',
'base_url' => '',
];
}
public static function title(): string
{
return 'Owncloud';
}
/**
* @inheritdoc
*/
public static function fields(): array
{
return [
['label' => 'URL', 'key' => 'base_url', 'type' => 'text'],
['label' => 'Benutzer', 'key' => 'user', 'type' => 'text'],
['label' => 'Passwort', 'key' => 'password', 'type' => 'password'],
];
}
public function getFilesystem(): FilesystemAdapter
{
$adapter = new WebDAVAdapter(new Client([
'baseUri' => $this->baseUrl . '/remote.php/dav/files/' . $this->user,
'userName' => $this->user,
'password' => $this->password,
]), '/remote.php/dav/files/' . $this->user);
return new FilesystemAdapter(new Filesystem($adapter), $adapter);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Fileshare\Data;
use App\Fileshare\Models\Fileshare;
use Illuminate\Filesystem\FilesystemAdapter;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class FileshareResourceData extends Data
{
public function __construct(public int $connectionId, public string $resource)
{
}
public function getConnection(): Fileshare
{
return Fileshare::find($this->connectionId);
}
public function getStorage(): FilesystemAdapter
{
return $this->getConnection()->type->getFilesystem();
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Fileshare\Data;
use Spatie\LaravelData\Data;
class ResourceData extends Data
{
public function __construct(public string $name, public string $path, public string $parent)
{
}
public static function fromString(string $path): self
{
$dir = '/' . trim($path, '\\/');
return self::from([
'path' => $dir,
'name' => pathinfo($dir, PATHINFO_BASENAME),
'parent' => pathinfo($dir, PATHINFO_DIRNAME),
]);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Fileshare;
use App\Fileshare\Models\Fileshare;
use App\Fileshare\Resources\FileshareResource;
use App\Setting\LocalSettings;
class FileshareSettings extends LocalSettings
{
public static function group(): string
{
return 'fileshare';
}
public static function title(): string
{
return 'Datei-Verbindungen';
}
/**
* @inheritdoc
*/
public function viewData(): array
{
return [
'data' => FileshareResource::collection(Fileshare::paginate(15))
];
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace App\Fileshare\Models;
use App\Fileshare\ConnectionTypes\ConnectionType;
use Database\Factories\Fileshare\Models\FileshareFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Fileshare extends Model
{
/** @use HasFactory<FileshareFactory> */
use HasFactory;
public $guarded = [];
public $casts = [
'type' => ConnectionType::class,
];
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Fileshare\Resources;
use App\Fileshare\ConnectionTypes\ConnectionType;
use App\Fileshare\Models\Fileshare;
use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Fileshare
*/
class FileshareResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'name' => $this->name,
'is_active' => $this->type->check(),
'type' => get_class($this->type),
'config' => $this->type->toArray(),
'id' => $this->id,
'type_human' => $this->type::title(),
'links' => [
'update' => route('fileshare.update', ['fileshare' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'default' => [
'name' => '',
'type' => null,
'config' => null,
],
'types' => ConnectionType::forSelect(),
'links' => [
'store' => route('fileshare.store'),
]
];
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\FormSettings;
use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction;
class ClearFrontendCacheAction
{
use AsAction;
public function handle(): void
{
Http::get(app(FormSettings::class)->clearCacheUrl);
}
}

View File

@ -1,71 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use Illuminate\Database\Eloquent\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\TableDocument\SheetData;
use Zoomyboy\TableDocument\TableDocumentData;
class CreateExcelDocumentAction
{
use AsAction;
public Form $form;
/**
* @param Collection<int, Participant> $participants
*/
public function handle(Form $form, Collection $participants): string
{
$this->form = $form;
return file_get_contents($this->allSheet($participants)->compile($this->tempPath()));
}
/**
* @param Collection<int, Participant> $participants
*/
private function allSheet(Collection $participants): TableDocumentData
{
$document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]);
$headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray();
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => 'Alle',
]));
if ($this->form->export->groupBy) {
$groups = $participants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
foreach ($groups as $name => $participants) {
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $participants
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
->toArray(),
'name' => $name,
]));
}
$document->addSheet(SheetData::from([
'header' => ['Wert', 'Anzahl'],
'data' => $groups->map(fn ($participants, $name) => [$name, (string) count($participants)])->toArray(),
'name' => 'Statistik',
]));
}
return $document;
}
private function tempPath(): string
{
return sys_get_temp_dir() . '/' . str()->uuid()->toString();
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Illuminate\Support\Facades\Storage;
use League\Csv\Writer;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ExportAction
{
use AsAction;
public function handle(Form $form): string
{
return CreateExcelDocumentAction::run($form, $form->participants);
}
public function asController(Form $form, ActionRequest $request): StreamedResponse
{
$contents = $this->handle($form);
$filename = 'tn-' . $form->slug . '.xlsx';
Storage::disk('temp')->put($filename, $contents);
return Storage::disk('temp')->download($filename);
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Group;
use Lorisleiva\Actions\Concerns\AsAction;
class ExportSyncAction
{
use AsAction;
public Form $form;
public function handle(Form $form): void
{
if (!$form->export->root) {
return;
}
$storage = $form->export->root->getStorage();
$storage->put($form->export->root->resource . '/Anmeldungen ' . $form->name . '.xlsx', CreateExcelDocumentAction::run($form, $form->participants));
if ($form->export->toGroupField) {
foreach ($form->participants->groupBy(fn ($participant) => $participant->data[$form->export->toGroupField]) as $groupId => $participants) {
$group = Group::find($groupId);
if (!$group?->fileshare) {
continue;
}
$group->fileshare->getStorage()->put($group->fileshare->resource . '/Anmeldungen ' . $form->name . '.xlsx', CreateExcelDocumentAction::run($form, $participants));
}
}
}
public function asJob(int $formId): void
{
$this->handle(Form::find($formId));
}
}

View File

@ -27,7 +27,7 @@ class FormApiListAction
{ {
return FormApiResource::collection($this->handle( return FormApiResource::collection($this->handle(
$request->input('filter', ''), $request->input('filter', ''),
$request->input('perPage', 9999) $request->input('perPage', 10)
)); ));
} }
} }

View File

@ -14,8 +14,6 @@ class FormDestroyAction
{ {
$form->delete(); $form->delete();
ClearFrontendCacheAction::run();
Succeeded::message('Veranstaltung gelöscht.')->dispatch(); Succeeded::message('Veranstaltung gelöscht.')->dispatch();
} }
} }

View File

@ -28,14 +28,10 @@ class FormStoreAction
'to' => 'required|date', 'to' => 'required|date',
'registration_from' => 'present|nullable|date', 'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date', 'registration_until' => 'present|nullable|date',
'mail_top' => 'array',
'mail_bottom' => 'array',
'header_image' => 'required|exclude', 'header_image' => 'required|exclude',
'mailattachments' => 'present|array|exclude', 'mailattachments' => 'present|array|exclude',
'is_active' => 'boolean',
'is_private' => 'boolean',
'export' => 'nullable|array',
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
]; ];
} }
@ -47,7 +43,6 @@ class FormStoreAction
return tap(Form::create($attributes), function ($form) { return tap(Form::create($attributes), function ($form) {
$form->setDeferredUploads(request()->input('header_image')); $form->setDeferredUploads(request()->input('header_image'));
$form->setDeferredUploads(request()->input('mailattachments')); $form->setDeferredUploads(request()->input('mailattachments'));
ClearFrontendCacheAction::run();
}); });
} }

View File

@ -3,7 +3,6 @@
namespace App\Form\Actions; namespace App\Form\Actions;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Lib\Editor\Condition;
use App\Lib\Events\Succeeded; use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -30,12 +29,8 @@ class FormUpdateAction
'to' => 'required|date', 'to' => 'required|date',
'registration_from' => 'present|nullable|date', 'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date', 'registration_until' => 'present|nullable|date',
'is_active' => 'boolean', 'mail_top' => 'array',
'is_private' => 'boolean', 'mail_bottom' => 'array',
'export' => 'nullable|array',
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
]; ];
} }
@ -45,9 +40,6 @@ class FormUpdateAction
public function handle(Form $form, array $attributes): Form public function handle(Form $form, array $attributes): Form
{ {
$form->update($attributes); $form->update($attributes);
ClearFrontendCacheAction::run();
return $form; return $form;
} }

View File

@ -22,10 +22,10 @@ class FormUpdateMetaAction
return [ return [
'sorting' => 'array', 'sorting' => 'array',
'sorting.by' => 'required|string', 'sorting.0' => 'required|string',
'sorting.direction' => 'required|boolean', 'sorting.1' => 'required|string|in:asc,desc',
'active_columns' => 'array', 'active_columns' => 'array',
'active_columns.*' => ['string', Rule::in([...$form->getFields()->pluck('key')->toArray(), 'created_at', 'prevention'])] 'active_columns.*' => ['string', Rule::in([...$form->getFields()->pluck('key')->toArray(), 'created_at'])]
]; ];
} }

View File

@ -18,7 +18,6 @@ trait HasValidation
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'config' => 'array', 'config' => 'array',
'config.sections.*.name' => 'required', 'config.sections.*.name' => 'required',
'config.sections.*.intro' => 'nullable|string',
'config.sections.*.fields' => 'array', 'config.sections.*.fields' => 'array',
'config.sections.*.fields.*.name' => 'required|string', 'config.sections.*.fields.*.name' => 'required|string',
'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))], 'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))],
@ -28,8 +27,6 @@ trait HasValidation
'config.sections.*.fields.*.columns.mobile' => 'required|numeric|gt:0|lte:2', '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.tablet' => 'required|numeric|gt:0|lte:4',
'config.sections.*.fields.*.columns.desktop' => 'required|numeric|gt:0|lte:6', 'config.sections.*.fields.*.columns.desktop' => 'required|numeric|gt:0|lte:6',
'mail_top' => 'array',
'mail_bottom' => 'array',
]; ];
} }

View File

@ -1,27 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Participant;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantAssignAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'member_id' => 'required|exists:members,id',
];
}
public function handle(Participant $participant, ActionRequest $request): void
{
$participant->update(['member_id' => $request->input('member_id')]);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Participant;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantFieldsAction
{
use AsAction;
public function handle(Participant $participant): JsonResponse
{
return response()->json([
'data' => [
'id' => $participant->id,
'config' => $participant->getConfig(),
]
]);
}
}

View File

@ -7,7 +7,7 @@ use App\Form\Models\Participant;
use App\Form\Resources\ParticipantResource; use App\Form\Resources\ParticipantResource;
use App\Form\Scopes\ParticipantFilterScope; use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Laravel\Scout\Builder; use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantIndexAction class ParticipantIndexAction
@ -15,24 +15,17 @@ class ParticipantIndexAction
use AsAction; use AsAction;
/** /**
* @return Builder<Participant> * @return LengthAwarePaginator<Participant>
*/ */
protected function getQuery(Form $form, ParticipantFilterScope $filter): Builder public function handle(Form $form, ParticipantFilterScope $filter): LengthAwarePaginator
{ {
return $filter->setForm($form)->getQuery() return $form->participants()->withFilter($filter)->with('form')->paginate(15);
->query(fn ($q) => $q->withCount('children')->with('form'));
} }
public function asController(Form $form, ?int $parent = null): AnonymousResourceCollection public function asController(Form $form): AnonymousResourceCollection
{ {
$filter = ParticipantFilterScope::fromRequest(request()->input('filter', ''))->parent($parent); $filter = ParticipantFilterScope::fromRequest(request()->input('filter'));
return ParticipantResource::collection($this->handle($form, $filter))
$data = match ($parent) { ->additional(['meta' => ParticipantResource::meta($form)]);
null => $this->getQuery($form, $filter)->paginate(15), // initial all elements - paginate
-1 => $this->getQuery($form, $filter)->paginate(15), // initial root elements - parinate
default => $this->getQuery($form, $filter)->get(), // specific parent element - show all
};
return ParticipantResource::collection($data)->additional(['meta' => ParticipantResource::meta($form)]);
} }
} }

View File

@ -1,56 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantStoreAction
{
use AsAction;
/**
* @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 handle(Form $form, ActionRequest $request): JsonResponse
{
$form->participants()->create(['data' => $request->validated()]);
ExportSyncAction::dispatch($form->id);
Succeeded::message('Teilnehmer*in erstellt.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,55 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Participant;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantUpdateAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Participant */
$participant = request()->route('participant');
return $participant->form->getRegistrationRules();
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
/** @var Participant */
$participant = request()->route('participant');
return $participant->form->getRegistrationAttributes();
}
/**
* @return array<string, mixed>
*/
public function getValidationMessages(): array
{
/** @var Participant */
$participant = request()->route('participant');
return $participant->form->getRegistrationMessages();
}
public function handle(Participant $participant, ActionRequest $request): JsonResponse
{
$participant->update(['data' => [...$participant->data, ...$request->validated()]]);
ExportSyncAction::dispatch($participant->form->id);
Succeeded::message('Teilnehmer*in bearbeitet.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,49 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Editor\FormConditionResolver;
use App\Form\Models\Participant;
use App\Prevention\Mails\PreventionRememberMail;
use App\Prevention\PreventionSettings;
use Illuminate\Support\Facades\Mail;
use Lorisleiva\Actions\Concerns\AsAction;
class PreventionRememberAction
{
use AsAction;
public string $commandSignature = 'prevention:remember';
public function handle(): void
{
$query = Participant::whereHas(
'form',
fn ($form) => $form
->where('needs_prevention', true)
->where('from', '>=', now())
)
->where(
fn ($q) => $q
->where('last_remembered_at', '<=', now()->subWeeks(2))
->orWhereNull('last_remembered_at')
);
foreach ($query->get() as $participant) {
if (!app(FormConditionResolver::class)->forParticipant($participant)->filterCondition($participant->form->prevention_conditions)) {
continue;
}
if ($participant->getFields()->getMailRecipient() === null || count($participant->preventions()) === 0) {
continue;
}
$body = app(PreventionSettings::class)->refresh()->formmail
->placeholder('formname', $participant->form->name)
->append($participant->form->prevention_text);
Mail::send(new PreventionRememberMail($participant, $body));
$participant->update(['last_remembered_at' => now()]);
}
}
}

View File

@ -7,7 +7,6 @@ use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Member\Member; use App\Member\Member;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -20,24 +19,19 @@ class RegisterAction
*/ */
public function handle(Form $form, array $input): Participant public function handle(Form $form, array $input): Participant
{ {
if (!$form->canRegister()) {
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
}
$memberQuery = FieldCollection::fromRequest($form, $input) $memberQuery = FieldCollection::fromRequest($form, $input)
->withNamiType() ->withNamiType()
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery()); ->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
$member = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first() : null; $mglnr = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first()->mitgliedsnr : null;
$participant = $form->participants()->create([ $participant = $form->participants()->create([
'data' => $input, 'data' => $input,
'member_id' => $member?->id, 'mitgliedsnr' => $mglnr,
]); ]);
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input)); $form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
$participant->sendConfirmationMail(); $participant->sendConfirmationMail();
ExportSyncAction::dispatch($form->id);
return $participant; return $participant;
} }

View File

@ -1,31 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Lorisleiva\Actions\Concerns\AsAction;
class UpdateParticipantSearchIndexAction
{
use AsAction;
public function handle(Form $form): void
{
if (config('scout.driver') !== 'meilisearch') {
return;
}
$form->searchableUsing()->updateIndexSettings(
$form->participantsSearchableAs(),
[
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'],
'searchableAttributes' => $form->getFields()->searchables()->getKeys(),
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id', 'created_at'],
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id'],
'pagination' => [
'maxTotalHits' => 1000000,
]
]
);
}
}

View File

@ -6,7 +6,6 @@ use App\Form\Fields\Field;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\DataProperty;
class CollectionCast implements Cast class CollectionCast implements Cast
@ -21,9 +20,10 @@ class CollectionCast implements Cast
/** /**
* @param array<int, array<string, mixed>> $value * @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return Collection<int, Data> * @return Collection<int, Data>
*/ */
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed public function cast(DataProperty $property, mixed $value, array $context): mixed
{ {
return collect($value)->map(fn ($item) => $this->target::from($item)); return collect($value)->map(fn ($item) => $this->target::from($item));
} }

View File

@ -5,16 +5,16 @@ namespace App\Form\Casts;
use App\Form\Data\FieldCollection; use App\Form\Data\FieldCollection;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\DataProperty;
class FieldCollectionCast implements Cast class FieldCollectionCast implements Cast
{ {
/** /**
* @param array<int, array<string, string>> $value * @param array<int, array<string, mixed>> $value
* @param array<string, mixed> $context
* @return FieldCollection * @return FieldCollection
*/ */
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed 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()); return new FieldCollection(collect($value)->map(fn ($value) => Field::classFromType($value['type'])::from($value))->all());
} }

View File

@ -1,9 +0,0 @@
<?php
namespace App\Form\Contracts;
interface Filterable
{
/** @param mixed $value */
public function filter($value): string;
}

View File

@ -1,19 +0,0 @@
<?php
namespace App\Form\Data;
use App\Fileshare\Data\FileshareResourceData;
use App\Form\Fields\Field;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class ExportData extends Data
{
public function __construct(public ?FileshareResourceData $root = null, public ?string $groupBy = null, public ?string $toGroupField = null)
{
}
}

View File

@ -2,7 +2,6 @@
namespace App\Form\Data; namespace App\Form\Data;
use App\Form\Contracts\Filterable;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use App\Form\Fields\NamiField; use App\Form\Fields\NamiField;
@ -36,11 +35,6 @@ class FieldCollection extends Collection
return $this->filter(fn ($field) => !is_a($field, NamiField::class)); return $this->filter(fn ($field) => !is_a($field, NamiField::class));
} }
public function hasNamiField(): bool
{
return $this->first(fn ($field) => is_a($field, NamiField::class)) !== null;
}
/** /**
* @return stdClass * @return stdClass
*/ */
@ -98,47 +92,8 @@ class FieldCollection extends Collection
return $attributes->toArray(); return $attributes->toArray();
} }
/**
* @return array<int, string>
*/
public function names(): array
{
return $this->map(fn ($field) => $field->name)->toArray();
}
/**
* @return array<int, string>
*/
public function presentValues(): array
{
return $this->map(fn ($field) => $field->presentRaw())->toArray();
}
private function findBySpecialType(SpecialType $specialType): ?Field private function findBySpecialType(SpecialType $specialType): ?Field
{ {
return $this->first(fn ($field) => $field->specialType === $specialType); return $this->first(fn ($field) => $field->specialType === $specialType);
} }
public function searchables(): self
{
return $this;
}
public function sortables(): self
{
return $this;
}
public function filterables(): self
{
return $this->filter(fn ($field) => $field instanceof Filterable);
}
/**
* @return array<int, string>
*/
public function getKeys(): array
{
return $this->map(fn ($field) => $field->key)->toArray();
}
} }

View File

@ -15,8 +15,7 @@ class SectionData extends Data
public string $name, public string $name,
#[WithCast(FieldCollectionCast::class)] #[WithCast(FieldCollectionCast::class)]
#[WithTransformer(FieldCollectionTransformer::class)] #[WithTransformer(FieldCollectionTransformer::class)]
public FieldCollection $fields, public FieldCollection $fields
public ?string $intro
) { ) {
} }
} }

View File

@ -3,7 +3,6 @@
namespace App\Form\Editor; namespace App\Form\Editor;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\Editor\Condition;
use App\Lib\Editor\ConditionResolver; use App\Lib\Editor\ConditionResolver;
class FormConditionResolver extends ConditionResolver class FormConditionResolver extends ConditionResolver
@ -21,24 +20,24 @@ class FormConditionResolver extends ConditionResolver
/** /**
* @inheritdoc * @inheritdoc
*/ */
public function filterCondition(Condition $condition): bool public function filterCondition(string $mode, array $ifs): bool
{ {
if (!$condition->hasStatements()) { if (count($ifs) === 0) {
return true; return true;
} }
foreach ($condition->ifs as $if) { foreach ($ifs as $if) {
$field = $this->participant->getFields()->findByKey($if->field); $field = $this->participant->getFields()->findByKey($if['field']);
$matches = $field->matches($if->comparator, $if->value); $matches = $field->matches($if['comparator'], $if['value']);
if ($matches && $condition->isAny()) { if ($matches && $mode === 'any') {
return true; return true;
} }
if (!$matches && $condition->isAll()) { if (!$matches && $mode === 'all') {
return false; return false;
} }
} }
if ($condition->isAny()) { if ($mode === 'any') {
return false; return false;
} }

View File

@ -2,7 +2,6 @@
namespace App\Form\Enums; namespace App\Form\Enums;
use App\Form\Models\Form;
use App\Group\Enums\Level; use App\Group\Enums\Level;
use App\Member\Member; use App\Member\Member;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -11,7 +10,6 @@ enum NamiType: string
{ {
case FIRSTNAME = 'Vorname'; case FIRSTNAME = 'Vorname';
case LASTNAME = 'Nachname'; case LASTNAME = 'Nachname';
case NICKNAME = 'Spitzname';
case BIRTHDAY = 'Geburtstag'; case BIRTHDAY = 'Geburtstag';
case REGION = 'Bezirk'; case REGION = 'Bezirk';
case STAMM = 'Stamm'; case STAMM = 'Stamm';
@ -19,10 +17,6 @@ enum NamiType: string
case ADDRESS = 'Adresse'; case ADDRESS = 'Adresse';
case ZIP = 'PLZ'; case ZIP = 'PLZ';
case LOCATION = 'Ort'; case LOCATION = 'Ort';
case GENDER = 'Geschlecht';
case MOBILEPHONE = 'Handynummer';
case AGE = 'Alter (zum Zeitpunkt der Anmeldung)';
case AGEEVENT = 'Alter (zum Zeitpunkt der Veranstaltung)';
/** /**
* @return array<int, array{name: string, id: string}> * @return array<int, array{name: string, id: string}>
@ -34,7 +28,7 @@ enum NamiType: string
->toArray(); ->toArray();
} }
public function getMemberAttribute(Member $member, Form $form): ?string public function getMemberAttribute(Member $member): ?string
{ {
return match ($this) { return match ($this) {
static::FIRSTNAME => $member->firstname, static::FIRSTNAME => $member->firstname,
@ -46,11 +40,6 @@ enum NamiType: string
static::ADDRESS => $member->address, static::ADDRESS => $member->address,
static::ZIP => $member->zip, static::ZIP => $member->zip,
static::LOCATION => $member->location, static::LOCATION => $member->location,
static::NICKNAME => $member->nickname,
static::GENDER => $member->gender?->name,
static::AGE => intVal($member->birthday->diffInYears(now())),
static::AGEEVENT => $member->birthday->diffInYears($form->from),
static::MOBILEPHONE => $member->mobile_phone,
}; };
} }
@ -64,6 +53,7 @@ enum NamiType: string
static::FIRSTNAME => $query->where('firstname', $value), static::FIRSTNAME => $query->where('firstname', $value),
static::LASTNAME => $query->where('lastname', $value), static::LASTNAME => $query->where('lastname', $value),
static::BIRTHDAY => $query->where('birthday', $value), static::BIRTHDAY => $query->where('birthday', $value),
static::EMAIL => $query->where('email', $value),
default => $query, default => $query,
}; };
} }

View File

@ -2,7 +2,6 @@
namespace App\Form\Fields; namespace App\Form\Fields;
use App\Form\Contracts\Filterable;
use App\Form\Matchers\BooleanMatcher; use App\Form\Matchers\BooleanMatcher;
use App\Form\Matchers\Matcher; use App\Form\Matchers\Matcher;
use App\Form\Models\Form; use App\Form\Models\Form;
@ -10,8 +9,9 @@ use App\Form\Models\Participant;
use App\Form\Presenters\BooleanPresenter; use App\Form\Presenters\BooleanPresenter;
use App\Form\Presenters\Presenter; use App\Form\Presenters\Presenter;
use Faker\Generator; use Faker\Generator;
use Illuminate\Validation\Rule;
class CheckboxField extends Field implements Filterable class CheckboxField extends Field
{ {
public bool $required; public bool $required;
public string $description; public string $description;
@ -86,11 +86,4 @@ class CheckboxField extends Field implements Filterable
{ {
return app(BooleanMatcher::class); return app(BooleanMatcher::class);
} }
public function filter($value): string
{
$asString = $value ? 'true' : 'false';
return "{$this->key} = $asString";
}
} }

View File

@ -13,8 +13,6 @@ class CheckboxesField extends Field
{ {
/** @var array<int, string> */ /** @var array<int, string> */
public array $options; public array $options;
public ?int $min;
public ?int $max;
public static function name(): string public static function name(): string
{ {
@ -25,8 +23,6 @@ class CheckboxesField extends Field
{ {
return [ return [
['key' => 'options', 'default' => [], 'rules' => ['options' => 'array', 'options.*' => 'required|string'], 'label' => 'Optionen'], ['key' => 'options', 'default' => [], 'rules' => ['options' => 'array', 'options.*' => 'required|string'], 'label' => 'Optionen'],
['key' => 'min', 'default' => null, 'rules' => ['min' => 'present'], 'label' => 'minimale Anzahl'],
['key' => 'max', 'default' => null, 'rules' => ['max' => 'present'], 'label' => 'maximale Anzahl'],
]; ];
} }
@ -39,8 +35,6 @@ class CheckboxesField extends Field
{ {
return [ return [
'options' => $faker->words(4), 'options' => $faker->words(4),
'min' => null,
'max' => null,
]; ];
} }
@ -49,18 +43,8 @@ class CheckboxesField extends Field
*/ */
public function getRegistrationRules(Form $form): array public function getRegistrationRules(Form $form): array
{ {
$globalRules = ['array'];
if ($this->min > 0) {
$globalRules[] = 'min:' . $this->min;
}
if ($this->max > 0) {
$globalRules[] = 'max:' . $this->max;
}
return [ return [
$this->key => $globalRules, $this->key => 'array',
$this->key . '.*' => ['string', Rule::in($this->options)], $this->key . '.*' => ['string', Rule::in($this->options)],
]; ];
} }

View File

@ -2,7 +2,6 @@
namespace App\Form\Fields; namespace App\Form\Fields;
use App\Form\Contracts\Filterable;
use App\Form\Matchers\Matcher; use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher; use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form; use App\Form\Models\Form;
@ -10,7 +9,7 @@ use App\Form\Models\Participant;
use Faker\Generator; use Faker\Generator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class DropdownField extends Field implements Filterable class DropdownField extends Field
{ {
public bool $required; public bool $required;
/** @var array<int, string> */ /** @var array<int, string> */
@ -88,14 +87,4 @@ class DropdownField extends Field implements Filterable
{ {
return app(SingleValueMatcher::class); return app(SingleValueMatcher::class);
} }
/** @inheritdoc */
public function filter($value): string
{
if (is_null($value)) {
return "{$this->key} IS NULL";
}
return $this->key . ' = \'' . $value . '\'';
}
} }

View File

@ -11,7 +11,6 @@ use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Form\Presenters\DefaultPresenter; use App\Form\Presenters\DefaultPresenter;
use App\Form\Presenters\Presenter; use App\Form\Presenters\Presenter;
use App\Lib\Editor\Comparator;
use Faker\Generator; use Faker\Generator;
use Spatie\LaravelData\Data; use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
@ -30,7 +29,6 @@ abstract class Field extends Data
public bool $forMembers; public bool $forMembers;
public ?SpecialType $specialType = null; public ?SpecialType $specialType = null;
public ?string $hint; public ?string $hint;
public ?string $intro;
/** @var mixed */ /** @var mixed */
public $value; public $value;
@ -80,9 +78,6 @@ abstract class Field extends Data
->toArray(); ->toArray();
} }
/**
* @return class-string<Field>
*/
public static function classFromType(string $type): ?string public static function classFromType(string $type): ?string
{ {
/** @var class-string<Field> */ /** @var class-string<Field> */
@ -105,7 +100,10 @@ abstract class Field extends Data
]; ];
} }
public function presentRaw(): string /**
* @return mixed
*/
public function presentRaw()
{ {
return $this->getPresenter()->present($this->value); return $this->getPresenter()->present($this->value);
} }
@ -155,7 +153,6 @@ abstract class Field extends Data
'for_members' => true, 'for_members' => true,
'special_type' => null, 'special_type' => null,
'hint' => '', 'hint' => '',
'intro' => '',
...collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['default']])->toArray(), ...collect(static::meta())->mapWithKeys(fn ($meta) => [$meta['key'] => $meta['default']])->toArray(),
], ],
]; ];
@ -171,7 +168,7 @@ abstract class Field extends Data
return $this->key . '_display'; return $this->key . '_display';
} }
public function matches(Comparator $comparator, mixed $value): bool public function matches(string $comparator, mixed $value): bool
{ {
return $this->getMatcher()->setValue($this->value)->matches($comparator, $value); return $this->getMatcher()->setValue($this->value)->matches($comparator, $value);
} }
@ -180,10 +177,4 @@ abstract class Field extends Data
{ {
return app(SingleValueMatcher::class); return app(SingleValueMatcher::class);
} }
/** @param mixed $value */
public function filter($value): string
{
return '';
}
} }

View File

@ -65,7 +65,7 @@ class GroupField extends Field
$rules[] = Rule::in(Group::find($this->parentGroup)->children()->pluck('id')->push(-1)); $rules[] = Rule::in(Group::find($this->parentGroup)->children()->pluck('id')->push(-1));
} }
if ($this->parentField && request()->input($this->parentField) && request()->input($this->parentField) !== -1) { if ($this->parentField && request()->input($this->parentField)) {
$rules[] = Rule::in(Group::find(request()->input($this->parentField))->children()->pluck('id')->push(-1)); $rules[] = Rule::in(Group::find(request()->input($this->parentField))->children()->pluck('id')->push(-1));
} }

View File

@ -42,18 +42,19 @@ class NamiField extends Field
public function getRegistrationRules(Form $form): array public function getRegistrationRules(Form $form): array
{ {
$rules = [$this->key => 'present|array']; $rules = [$this->key => 'present|array'];
$inputMembers = request($this->key);
foreach ($form->getFields()->noNamiField()->forMembers() as $field) { $c = $form->getFields()->forMembers()->noNamiType()->noNamiField()
foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) { ->map(fn ($field) => $field->getRegistrationRules($form))
foreach ($inputMembers as $memberIndex => $inputMember) { ->toArray();
$rules[$this->key . '.' . $memberIndex . '.' . $ruleKey] = !$inputMember['id'] || $field->namiType === null ? $rule : '';
} foreach ($c as $field) {
foreach ($field as $ruleKey => $rule) {
$rules[$this->key . '.*.' . $ruleKey] = $rule;
} }
} }
return [ return [
$this->key . '.*.id' => ['nullable', 'numeric', 'exists:members,mitgliedsnr'], $this->key . '.*.id' => ['required', 'numeric', 'exists:members,mitgliedsnr'],
...$rules, ...$rules,
]; ];
} }
@ -70,9 +71,12 @@ class NamiField extends Field
return []; return [];
} }
foreach ($form->getFields()->noNamiField()->forMembers() as $field) { $c = $form->getFields()->noNamiField()->forMembers();
foreach ($c as $field) {
foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) { foreach ($field->getRegistrationRules($form) as $ruleKey => $rule) {
foreach ($inputMembers as $memberIndex => $inputMember) { foreach ($inputMembers as $memberIndex => $inputMember) {
$message = $field->name . ' für ein Mitglied'; $message = $field->name . ' für ein Mitglied';
$rules = array_merge( $rules = array_merge(
$rules, $rules,
@ -107,16 +111,16 @@ class NamiField extends Field
public function afterRegistration(Form $form, Participant $participant, array $input): void public function afterRegistration(Form $form, Participant $participant, array $input): void
{ {
foreach ($input[$this->key] as $memberData) { foreach ($input[$this->key] as $memberData) {
$member = $memberData['id'] ? Member::firstWhere(['mitgliedsnr' => $memberData['id']]) : null; $member = Member::firstWhere(['mitgliedsnr' => $memberData['id']]);
$data = []; $data = [];
foreach (FieldCollection::fromRequest($form, $memberData) as $field) { foreach (FieldCollection::fromRequest($form, $memberData) as $field) {
$data[$field->key] = $field->namiType === null || $member === null $data[$field->key] = $field->namiType === null
? $field->value ? $field->value
: $field->namiType->getMemberAttribute($member, $form); : $field->namiType->getMemberAttribute($member);
} }
$data[$this->key] = []; $data[$this->key] = [];
$form->participants()->create(['data' => $data, 'member_id' => $member?->id, 'parent_id' => $participant->id]); $form->participants()->create(['data' => $data, 'mitgliedsnr' => $memberData['id'], 'parent_id' => $participant->id]);
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Form\Fields; namespace App\Form\Fields;
use App\Form\Contracts\Filterable;
use App\Form\Matchers\Matcher; use App\Form\Matchers\Matcher;
use App\Form\Matchers\SingleValueMatcher; use App\Form\Matchers\SingleValueMatcher;
use App\Form\Models\Form; use App\Form\Models\Form;
@ -10,7 +9,7 @@ use App\Form\Models\Participant;
use Faker\Generator; use Faker\Generator;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class RadioField extends Field implements Filterable class RadioField extends Field
{ {
public bool $required; public bool $required;
/** @var array<int, string> */ /** @var array<int, string> */
@ -88,13 +87,4 @@ class RadioField extends Field implements Filterable
{ {
return app(SingleValueMatcher::class); return app(SingleValueMatcher::class);
} }
public function filter($value): string
{
if (is_null($value)) {
return "{$this->key} IS NULL";
}
return $this->key . ' = \'' . $value . '\'';
}
} }

View File

@ -1,54 +0,0 @@
<?php
namespace App\Form;
use App\Form\Actions\SettingStoreAction;
use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest;
class FormSettings extends LocalSettings implements Storeable
{
public string $registerUrl;
public string $clearCacheUrl;
public static function group(): string
{
return 'form';
}
public static function title(): string
{
return 'Formulare';
}
/**
* @inheritdoc
*/
public function rules(): array
{
return [
'registerUrl' => 'present|string',
'clearCacheUrl' => 'present|string',
];
}
public function beforeSave(ActionRequest $request): void
{
}
/**
* @inheritdoc
*/
public function viewData(): array
{
return [
'data' => [
'data' => [
'registerUrl' => $this->registerUrl,
'clearCacheUrl' => $this->clearCacheUrl,
]
]
];
}
}

View File

@ -5,7 +5,6 @@ namespace App\Form\Mails;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver; use App\Form\Editor\FormConditionResolver;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\Editor\Condition;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment; use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
@ -19,9 +18,9 @@ class ConfirmRegistrationMail extends Mailable
public string $fullname; public string $fullname;
public FormConfigData $config; public FormConfigData $config;
/** @var array<int, mixed> */ /** @var array<string, mixed> */
public array $topText; public array $topText;
/** @var array<int, mixed> */ /** @var array<string, mixed> */
public array $bottomText; public array $bottomText;
/** /**
@ -72,7 +71,10 @@ class ConfirmRegistrationMail extends Mailable
$conditionResolver = app(FormConditionResolver::class)->forParticipant($this->participant); $conditionResolver = app(FormConditionResolver::class)->forParticipant($this->participant);
return $this->participant->form->getMedia('mailattachments') return $this->participant->form->getMedia('mailattachments')
->filter(fn ($media) => $conditionResolver->filterCondition(Condition::fromMedia($media))) ->filter(fn ($media) => $conditionResolver->filterCondition(
data_get($media->getCustomProperty('conditions'), 'mode', 'all'),
data_get($media->getCustomProperty('conditions'), 'ifs', []),
))
->map(fn ($media) => Attachment::fromStorageDisk($media->disk, $media->getPathRelativeToRoot())) ->map(fn ($media) => Attachment::fromStorageDisk($media->disk, $media->getPathRelativeToRoot()))
->all(); ->all();
} }

View File

@ -2,8 +2,6 @@
namespace App\Form\Matchers; namespace App\Form\Matchers;
use App\Lib\Editor\Comparator;
abstract class Matcher abstract class Matcher
{ {
@ -16,5 +14,5 @@ abstract class Matcher
return $this; return $this;
} }
abstract public function matches(Comparator $comparator, mixed $value): bool; abstract public function matches(string $comparator, mixed $value): bool;
} }

View File

@ -2,26 +2,24 @@
namespace App\Form\Matchers; namespace App\Form\Matchers;
use App\Lib\Editor\Comparator;
class SingleValueMatcher extends Matcher class SingleValueMatcher extends Matcher
{ {
public function matches(Comparator $comparator, mixed $value): bool public function matches(string $comparator, mixed $value): bool
{ {
if ($comparator === Comparator::EQUAL && $value === $this->value) { if ($comparator === 'isEqual' && $value === $this->value) {
return true; return true;
} }
if ($comparator === Comparator::NOTEQUAL && $value !== $this->value) { if ($comparator === 'isNotEqual' && $value !== $this->value) {
return true; return true;
} }
if ($comparator === Comparator::IN && in_array($this->value, $value)) { if ($comparator === 'isIn' && in_array($this->value, $value)) {
return true; return true;
} }
if ($comparator === Comparator::NOTIN && !in_array($this->value, $value)) { if ($comparator === 'isNotIn' && !in_array($this->value, $value)) {
return true; return true;
} }

View File

@ -2,29 +2,21 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\Actions\UpdateParticipantSearchIndexAction;
use App\Form\Data\ExportData;
use App\Form\Data\FieldCollection; use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Lib\Editor\Condition;
use App\Lib\Editor\EditorData;
use App\Lib\Sorting;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Database\Factories\Form\Models\FormFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use Spatie\Image\Enums\Fit; use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Zoomyboy\MedialibraryHelper\DefersUploads; use Zoomyboy\MedialibraryHelper\DefersUploads;
/** @todo replace editor content with EditorData cast */
class Form extends Model implements HasMedia class Form extends Model implements HasMedia
{ {
/** @use HasFactory<FormFactory> */
use HasFactory; use HasFactory;
use Sluggable; use Sluggable;
use InteractsWithMedia; use InteractsWithMedia;
@ -36,21 +28,14 @@ class Form extends Model implements HasMedia
public $casts = [ public $casts = [
'config' => FormConfigData::class, 'config' => FormConfigData::class,
'meta' => 'json', 'meta' => 'json',
'description' => EditorData::class, 'description' => 'json',
'mail_top' => EditorData::class, 'mail_top' => 'json',
'mail_bottom' => EditorData::class, 'mail_bottom' => 'json',
'is_active' => 'boolean',
'is_private' => 'boolean',
'export' => ExportData::class,
'needs_prevention' => 'boolean',
'prevention_text' => EditorData::class,
'prevention_conditions' => Condition::class,
'from' => 'datetime',
'to' => 'datetime',
'registration_from' => 'datetime',
'registration_until' => 'datetime',
]; ];
/** @var array<int, string> */
public $dates = ['from', 'to', 'registration_from', 'registration_until'];
/** /**
* @return SluggableConfig * @return SluggableConfig
*/ */
@ -76,9 +61,8 @@ class Form extends Model implements HasMedia
->singleFile() ->singleFile()
->maxWidth(fn () => 500) ->maxWidth(fn () => 500)
->forceFileName(fn (Form $model, string $name) => $model->slug) ->forceFileName(fn (Form $model, string $name) => $model->slug)
->convert(fn () => 'jpg')
->registerMediaConversions(function (Media $media) { ->registerMediaConversions(function (Media $media) {
$this->addMediaConversion('square')->fit(Fit::Crop, 400, 400); $this->addMediaConversion('square')->fit(Manipulations::FIT_CROP, 400, 400);
}); });
$this->addMediaCollection('mailattachments') $this->addMediaCollection('mailattachments')
->withDefaultProperties(fn () => [ ->withDefaultProperties(fn () => [
@ -149,8 +133,6 @@ class Form extends Model implements HasMedia
'from' => $this->from->timestamp, 'from' => $this->from->timestamp,
'to' => $this->to->timestamp, 'to' => $this->to->timestamp,
'name' => $this->name, 'name' => $this->name,
'is_active' => $this->is_active,
'is_private' => $this->is_private,
]; ];
} }
@ -160,7 +142,7 @@ class Form extends Model implements HasMedia
if (is_null(data_get($model->meta, 'active_columns'))) { if (is_null(data_get($model->meta, 'active_columns'))) {
$model->setAttribute('meta', [ $model->setAttribute('meta', [
'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null, 'active_columns' => $model->getFields()->count() ? $model->getFields()->take(4)->pluck('key')->toArray() : null,
'sorting' => Sorting::by('id'), 'sorting' => $model->getFields()->count() ? [$model->getFields()->first()->key, 'asc'] : null,
]); ]);
return; return;
} }
@ -168,37 +150,10 @@ class Form extends Model implements HasMedia
if (is_array(data_get($model->meta, 'active_columns'))) { if (is_array(data_get($model->meta, 'active_columns'))) {
$model->setAttribute('meta', [ $model->setAttribute('meta', [
...$model->meta, ...$model->meta,
'active_columns' => array_values(array_intersect([...$model->getFields()->pluck('key')->toArray(), 'created_at', 'prevention'], $model->meta['active_columns'])), 'active_columns' => array_values(array_intersect([...$model->getFields()->pluck('key')->toArray(), 'created_at'], $model->meta['active_columns'])),
]); ]);
return; return;
} }
}); });
static::saved(function ($model) {
UpdateParticipantSearchIndexAction::dispatch($model);
});
}
public function participantsSearchableAs(): string
{
return config('scout.prefix') . 'forms_' . $this->id . '_participants';
}
public function defaultSorting(): Sorting
{
return Sorting::from($this->meta['sorting']);
}
public function canRegister(): bool
{
if ($this->registration_from && $this->registration_from->gt(now())) {
return false;
}
if ($this->registration_until && $this->registration_until->lt(now())) {
return false;
}
return true;
} }
} }

View File

@ -3,8 +3,6 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Lib\Editor\EditorData;
use Database\Factories\Form\Models\FormtemplateFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -13,14 +11,11 @@ use Illuminate\Database\Eloquent\Model;
*/ */
class Formtemplate extends Model class Formtemplate extends Model
{ {
/** @use HasFactory<FormtemplateFactory> */
use HasFactory; use HasFactory;
public $guarded = []; public $guarded = [];
public $casts = [ public $casts = [
'config' => FormConfigData::class, 'config' => FormConfigData::class,
'mail_top' => EditorData::class,
'mail_bottom' => EditorData::class,
]; ];
} }

View File

@ -6,30 +6,21 @@ use App\Form\Data\FieldCollection;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Form\Mails\ConfirmRegistrationMail; use App\Form\Mails\ConfirmRegistrationMail;
use App\Form\Scopes\ParticipantFilterScope; use App\Form\Scopes\ParticipantFilterScope;
use App\Member\Member;
use App\Prevention\Contracts\Preventable;
use Database\Factories\Form\Models\ParticipantFactory;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Laravel\Scout\Searchable;
use stdClass;
class Participant extends Model implements Preventable class Participant extends Model
{ {
/** @use HasFactory<ParticipantFactory> */
use HasFactory; use HasFactory;
use Searchable;
public $guarded = []; public $guarded = [];
public $casts = [ public $casts = [
'data' => 'json', 'data' => 'json',
'last_remembered_at' => 'datetime',
]; ];
/** /**
@ -49,11 +40,12 @@ class Participant extends Model implements Preventable
} }
/** /**
* @return BelongsTo<Member, self> * @param Builder<self> $query
* @return Builder<self>
*/ */
public function member(): BelongsTo public function scopeWithFilter(Builder $query, ParticipantFilterScope $filter): Builder
{ {
return $this->belongsTo(Member::class); return $filter->apply($query);
} }
public function getFields(): FieldCollection public function getFields(): FieldCollection
@ -78,40 +70,6 @@ class Participant extends Model implements Preventable
return; return;
} }
Mail::to($this->getMailRecipient())->queue(new ConfirmRegistrationMail($this)); Mail::to($this->getFields()->getMailRecipient())->queue(new ConfirmRegistrationMail($this));
}
public function preventableLayout(): string
{
return 'mail.prevention.prevention-remember-participant';
}
/**
* @inheritdoc
*/
public function preventions(): array
{
return $this->member?->preventions($this->form->from) ?: [];
}
public function getMailRecipient(): stdClass
{
return $this->getFields()->getMailRecipient();
}
public function preventableSubject(): string
{
return 'Nachweise erforderlich für deine Anmeldung zu ' . $this->form->name;
}
public function searchableAs(): string
{
return $this->form->participantsSearchableAs();
}
/** @return array<string, mixed> */
public function toSearchableArray(): array
{
return [...$this->data, 'parent-id' => $this->parent_id, 'created_at' => $this->created_at->timestamp];
} }
} }

View File

@ -33,9 +33,6 @@ class FormApiResource extends JsonResource
'slug' => $this->slug, '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'), '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'), 'image' => $this->getMedia('headerImage')->first()->getFullUrl('square'),
'is_active' => $this->is_active,
'is_private' => $this->is_private,
'can_register' => $this->getModel()->canRegister(),
]; ];
} }

View File

@ -2,16 +2,13 @@
namespace App\Form\Resources; namespace App\Form\Resources;
use App\Form\Data\ExportData;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use App\Form\FormSettings;
use App\Form\Scopes\FormFilterScope; use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Formtemplate; use App\Form\Models\Formtemplate;
use App\Group; use App\Group;
use App\Lib\Editor\EditorData;
use App\Lib\HasMeta; use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@ -46,21 +43,11 @@ class FormResource extends JsonResource
'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'), 'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'),
'config' => $this->config, 'config' => $this->config,
'participants_count' => $this->participants_count, 'participants_count' => $this->participants_count,
'is_active' => $this->is_active,
'is_private' => $this->is_private,
'has_nami_field' => $this->getFields()->hasNamiField(),
'export' => $this->export,
'needs_prevention' => $this->needs_prevention,
'prevention_text' => $this->prevention_text,
'prevention_conditions' => $this->prevention_conditions,
'links' => [ 'links' => [
'participant_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => null]), 'participant_index' => route('form.participant.index', ['form' => $this->getModel()]),
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
'update' => route('form.update', ['form' => $this->getModel()]), 'update' => route('form.update', ['form' => $this->getModel()]),
'destroy' => route('form.destroy', ['form' => $this->getModel()]), 'destroy' => route('form.destroy', ['form' => $this->getModel()]),
'is_dirty' => route('form.is-dirty', ['form' => $this->getModel()]), 'is_dirty' => route('form.is-dirty', ['form' => $this->getModel()]),
'frontend' => str(app(FormSettings::class)->registerUrl)->replace('{slug}', $this->slug),
'export' => route('form.export', ['form' => $this->getModel()]),
] ]
]; ];
} }
@ -84,8 +71,6 @@ class FormResource extends JsonResource
'specialTypes' => SpecialType::forSelect(), 'specialTypes' => SpecialType::forSelect(),
'default' => [ 'default' => [
'description' => [], 'description' => [],
'is_active' => true,
'is_private' => false,
'name' => '', 'name' => '',
'excerpt' => '', 'excerpt' => '',
'from' => null, 'from' => null,
@ -97,10 +82,7 @@ class FormResource extends JsonResource
'config' => null, 'config' => null,
'header_image' => null, 'header_image' => null,
'mailattachments' => [], 'mailattachments' => [],
'prevention_text' => EditorData::default(),
'id' => null, 'id' => null,
'export' => ExportData::from([]),
'prevention_conditions' => ['mode' => 'all', 'ifs' => []],
], ],
'section_default' => [ 'section_default' => [
'name' => '', 'name' => '',

View File

@ -7,7 +7,6 @@ use App\Form\Enums\SpecialType;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use App\Form\Models\Formtemplate; use App\Form\Models\Formtemplate;
use App\Group; use App\Group;
use App\Lib\Editor\EditorData;
use App\Lib\HasMeta; use App\Lib\HasMeta;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@ -53,8 +52,6 @@ class FormtemplateResource extends JsonResource
], ],
'default' => [ 'default' => [
'name' => '', 'name' => '',
'mail_top' => EditorData::default(),
'mail_bottom' => EditorData::default(),
'config' => [ 'config' => [
'sections' => [], 'sections' => [],
] ]

View File

@ -4,8 +4,6 @@ namespace App\Form\Resources;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Form\Scopes\ParticipantFilterScope;
use App\Prevention\Enums\Prevention;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -24,18 +22,10 @@ class ParticipantResource extends JsonResource
{ {
return [ return [
...$this->getModel()->getFields()->present(), ...$this->getModel()->getFields()->present(),
'id' => $this->id,
'created_at' => $this->created_at->format('Y-m-d H:i:s'), 'created_at' => $this->created_at->format('Y-m-d H:i:s'),
'created_at_display' => $this->created_at->format('d.m.Y'), 'created_at_display' => $this->created_at->format('d.m.Y'),
'children_count' => $this->children_count,
'member_id' => $this->member_id,
'prevention_items' => $this->member ? Prevention::items($this->member->preventions()) : [],
'links' => [ 'links' => [
'assign' => route('participant.assign', ['participant' => $this->getModel()]),
'destroy' => route('participant.destroy', ['participant' => $this->getModel()]), 'destroy' => route('participant.destroy', ['participant' => $this->getModel()]),
'children' => route('form.participant.index', ['form' => $this->form, 'parent' => $this->id]),
'fields' => route('participant.fields', ['participant' => $this->getModel()]),
'update' => route('participant.update', ['participant' => $this->getModel()]),
] ]
]; ];
} }
@ -53,33 +43,16 @@ class ParticipantResource extends JsonResource
'id' => $field->key, 'id' => $field->key,
'display_attribute' => $field->getDisplayAttribute(), 'display_attribute' => $field->getDisplayAttribute(),
]); ]);
$filterData = $form->getFields()
->map(fn ($field) => [
...$field->toArray(),
'base_type' => class_basename($field),
]);
return [ return [
'filter' => ParticipantFilterScope::fromRequest(request()->input('filter', ''))->setForm($form),
'form_config' => $form->config,
'default_filter_value' => ParticipantFilterScope::$nan,
'filters' => $filterData,
'form_meta' => $form->meta, 'form_meta' => $form->meta,
'has_nami_field' => $form->getFields()->hasNamiField(),
'links' => [ 'links' => [
'update_form_meta' => route('form.update-meta', ['form' => $form]), 'update_form_meta' => route('form.update-meta', ['form' => $form]),
'store_participant' => route('form.participant.store', ['form' => $form]),
], ],
'columns' => $fieldData->push([ 'columns' => $fieldData->push([
'name' => 'Registriert am', 'name' => 'Registriert am',
'id' => 'created_at', 'id' => 'created_at',
'display_attribute' => 'created_at_display' 'display_attribute' => 'created_at_display'
])->push([ ])
'name' => 'Prävention',
'id' => 'prevention',
'display_attribute' => 'prevention_display'
]),
]; ];
} }
} }

View File

@ -19,7 +19,6 @@ class FormFilterScope extends ScoutFilter
public function __construct( public function __construct(
public ?string $search = '', public ?string $search = '',
public bool $past = false, public bool $past = false,
public bool $inactive = false,
) { ) {
} }
@ -36,10 +35,6 @@ class FormFilterScope extends ScoutFilter
$filters->push('to > ' . now()->timestamp); $filters->push('to > ' . now()->timestamp);
} }
if ($this->inactive === false) {
$filters->push('is_active = true');
}
$options['filter'] = $filters->implode(' AND '); $options['filter'] = $filters->implode(' AND ');
return $engine->search($query, $options); return $engine->search($query, $options);

View File

@ -2,91 +2,38 @@
namespace App\Form\Scopes; namespace App\Form\Scopes;
use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\ScoutFilter; use App\Lib\Filter;
use App\Lib\Sorting; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper;
/** /**
* @extends ScoutFilter<Participant> * @extends Filter<Participant>
*/ */
#[MapInputName(SnakeCaseMapper::class)] #[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
class ParticipantFilterScope extends ScoutFilter class ParticipantFilterScope extends Filter
{ {
public static string $nan = 'deeb3ef4-d185-44b1-a4bc-0a4e7addebc3d8900c6f-a344-4afb-b54e-065ed483a7ba';
private Form $form;
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $options
*/
public function __construct( public function __construct(
public array $data = [],
public string $search = '',
public array $options = [],
public ?int $parent = null, public ?int $parent = null,
public ?Sorting $sort = null
) { ) {
} }
public function getQuery(): Builder /**
* @inheritdoc
*/
public function apply(Builder $query): Builder
{ {
$this->search = $this->search ?: '';
return Participant::search($this->search, function ($engine, string $query, array $options) {
$filter = collect([]);
foreach ($this->form->getFields()->filterables() as $field) {
if ($this->data[$field->key] === static::$nan) {
continue;
}
$filter->push($field->filter($this->data[$field->key]));
}
if ($this->parent === -1) { if ($this->parent === -1) {
$filter->push('parent-id IS NULL'); $query = $query->whereNull('parent_id');
} }
if ($this->parent !== null && $this->parent !== -1) { if (!is_null($this->parent) && $this->parent > 0) {
$filter->push('parent-id = ' . $this->parent); $query = $query->where('parent_id', $this->parent);
} }
$options['filter'] = $filter->map(fn ($expression) => "($expression)")->implode(' AND '); return $query;
$options['sort'] = $this->sort->toMeilisearch();
return $engine->search($query, [...$this->options, ...$options]);
})->within($this->form->participantsSearchableAs());
}
public function setForm(Form $form): self
{
$this->form = $form;
if (is_null($this->sort)) {
$this->sort = $this->form->defaultSorting();
}
foreach ($form->getFields() as $field) {
if (!Arr::has($this->data, $field->key)) {
data_set($this->data, $field->key, static::$nan);
}
}
return $this;
}
public function parent(?int $parent): self
{
$this->parent = $parent;
return $this;
} }
} }

View File

@ -5,7 +5,6 @@ namespace App\Form\Transformers;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Transformation\TransformationContext;
use Spatie\LaravelData\Transformers\Transformer; use Spatie\LaravelData\Transformers\Transformer;
class CollectionTransformer implements Transformer class CollectionTransformer implements Transformer
@ -19,7 +18,7 @@ class CollectionTransformer implements Transformer
* @param Collection<int, Field> $value * @param Collection<int, Field> $value
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed public function transform(DataProperty $property, mixed $value): mixed
{ {
return $value->toArray(); return $value->toArray();
} }

View File

@ -5,7 +5,6 @@ namespace App\Form\Transformers;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Transformation\TransformationContext;
use Spatie\LaravelData\Transformers\Transformer; use Spatie\LaravelData\Transformers\Transformer;
class FieldCollectionTransformer implements Transformer class FieldCollectionTransformer implements Transformer
@ -15,7 +14,7 @@ class FieldCollectionTransformer implements Transformer
* @param Collection<int, Field> $value * @param Collection<int, Field> $value
* @return array<string, mixed> * @return array<string, mixed>
*/ */
public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed public function transform(DataProperty $property, mixed $value): mixed
{ {
return $value->map(fn ($field) => [ return $value->map(fn ($field) => [
...$field->toArray(), ...$field->toArray(),

View File

@ -3,15 +3,12 @@
namespace App; namespace App;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Database\Factories\GenderFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Gender extends Model class Gender extends Model
{ {
use HasNamiField; use HasNamiField;
/** @use HasFactory<GenderFactory> */
use HasFactory; use HasFactory;
public $fillable = ['name', 'nami_id']; public $fillable = ['name', 'nami_id'];
@ -25,15 +22,6 @@ class Gender extends Model
}; };
} }
public function getShortAttribute(): string
{
return match ($this->name) {
'Männlich' => 'm',
'Weiblich' => 'w',
default => ''
};
}
public static function fromString(string $title): self public static function fromString(string $title): self
{ {
return self::firstWhere('name', $title); return self::firstWhere('name', $title);

View File

@ -2,10 +2,8 @@
namespace App; namespace App;
use App\Fileshare\Data\FileshareResourceData;
use App\Group\Enums\Level; use App\Group\Enums\Level;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Database\Factories\GroupFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -13,24 +11,22 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Group extends Model class Group extends Model
{ {
/** @use HasFactory<GroupFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;
public $fillable = ['nami_id', 'name', 'inner_name', 'level', 'parent_id', 'fileshare']; public $fillable = ['nami_id', 'name', 'inner_name', 'level', 'parent_id'];
public $timestamps = false; public $timestamps = false;
public $casts = [ public $casts = [
'level' => Level::class, 'level' => Level::class
'fileshare' => FileshareResourceData::class,
]; ];
/** /**
* @return BelongsTo<self, self> * @return BelongsTo<static, self>
*/ */
public function parent(): BelongsTo public function parent(): BelongsTo
{ {
return $this->belongsTo(self::class, 'parent_id'); return $this->belongsTo(static::class, 'parent_id');
} }
/** /**
@ -38,7 +34,7 @@ class Group extends Model
*/ */
public function children(): HasMany public function children(): HasMany
{ {
return $this->hasMany(self::class, 'parent_id'); return $this->hasMany(static::class, 'parent_id');
} }
public static function booted(): void public static function booted(): void

View File

@ -6,7 +6,6 @@ use App\Group;
use App\Group\Resources\GroupResource; use App\Group\Resources\GroupResource;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class GroupApiIndexAction class GroupApiIndexAction
@ -21,12 +20,8 @@ class GroupApiIndexAction
return Group::get(); return Group::get();
} }
public function asController(ActionRequest $request, ?Group $group = null): AnonymousResourceCollection public function asController(?Group $group = null): AnonymousResourceCollection
{ {
return GroupResource::collection( return GroupResource::collection($group ? $group->children()->withCount('children')->get() : Group::where('parent_id', null)->withCount('children')->get());
$request->has('all')
? Group::with('children')->get()
: ($group ? $group->children()->withCount('children')->get() : Group::where('parent_id', null)->withCount('children')->get())
);
} }
} }

View File

@ -22,9 +22,6 @@ class GroupBulkstoreAction
'*.id' => 'required|integer|exists:groups,id', '*.id' => 'required|integer|exists:groups,id',
'*.inner_name' => 'required|string|max:255', '*.inner_name' => 'required|string|max:255',
'*.level' => ['required', 'string', Rule::in(Level::values())], '*.level' => ['required', 'string', Rule::in(Level::values())],
'*.fileshare' => 'present|nullable',
'*.fileshare.connection_id' => 'nullable|numeric|exists:fileshares,id',
'*.fileshare.resource' => 'nullable|string',
]; ];
} }
@ -34,7 +31,7 @@ class GroupBulkstoreAction
public function handle(array $groups): void public function handle(array $groups): void
{ {
foreach ($groups as $payload) { foreach ($groups as $payload) {
Group::find($payload['id'])->update(['level' => $payload['level'], 'inner_name' => $payload['inner_name'], 'fileshare' => $payload['fileshare']]); Group::find($payload['id'])->update(['level' => $payload['level'], 'inner_name' => $payload['inner_name']]);
} }
} }

Some files were not shown because too many files have changed in this diff Show More