Compare commits

..

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

686 changed files with 12914 additions and 34187 deletions

View File

@ -3,6 +3,7 @@ APP_ENV=production
APP_KEY=YOUR_APP_KEY APP_KEY=YOUR_APP_KEY
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://localhost:8000 APP_URL=http://localhost:8000
APP_MODE=stamm
MAIL_MAILER=smtp MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io MAIL_HOST=smtp.mailtrap.io
@ -16,8 +17,6 @@ MAIL_FROM_NAME=me
DB_PASSWORD=secret_db_password DB_PASSWORD=secret_db_password
MYSQL_PASSWORD=secret_db_password MYSQL_PASSWORD=secret_db_password
MEILI_MASTER_KEY=secret_meilisearch_password
PUSHER_APP_HOST=socketi PUSHER_APP_HOST=socketi
WORKERS=5 WORKERS=5

View File

@ -0,0 +1,2 @@
\App\User::factory()->create();
\App\User::factory()->create();

View File

@ -1,14 +0,0 @@
FROM php:8.3.11-fpm as php
WORKDIR /app
RUN ls /app
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 --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 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-install gd
RUN usermod -s /bin/bash www-data
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: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
@ -15,8 +15,9 @@ WORKDIR /app
COPY --from=node /app /app COPY --from=node /app /app
COPY --from=composer /app/public/vendor /app/public/vendor COPY --from=composer /app/public/vendor /app/public/vendor
COPY ./.docker/nginx/nginx.conf /etc/nginx/nginx.conf COPY ./.docker/nginx/nginx.conf /etc/nginx/nginx.conf
RUN cd public && ln -s ../storage/app/public ./storage
EXPOSE 80 EXPOSE 80
VOLUME ["/app/public/storage"] VOLUME ["/app/storage/app"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@ -36,17 +36,6 @@ http {
proxy_read_timeout 60; proxy_read_timeout 60;
proxy_connect_timeout 60; proxy_connect_timeout 60;
} }
location /indexes/members/search {
proxy_pass http://meilisearch:7700/indexes/members/search;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
proxy_read_timeout 60;
proxy_connect_timeout 60;
}
location / { location / {
try_files $uri $uri/ /index.php?$query_string; try_files $uri $uri/ /index.php?$query_string;
} }

View File

@ -1,17 +1,24 @@
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
FROM zoomyboy/adrema-base:latest as php FROM php:8.1.6-fpm as php
WORKDIR /app
RUN apt-get update
RUN apt-get install -y rsync libcurl3-dev apt-utils zlib1g-dev libpng-dev libicu-dev libonig-dev unzip
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
RUN docker-php-ext-install pdo_mysql curl gd exif intl mbstring pcntl
RUN pecl install redis && docker-php-ext-enable redis
COPY --chown=www-data:www-data . /app COPY --chown=www-data:www-data . /app
COPY --chown=www-data:www-data --from=node /app/public /app/public COPY --chown=www-data:www-data --from=node /app/public /app/public
COPY --chown=www-data:www-data --from=composer /app/vendor /app/vendor COPY --chown=www-data:www-data --from=composer /app/vendor /app/vendor
RUN usermod -s /bin/bash www-data
USER www-data USER www-data
RUN php artisan telescope:publish RUN php artisan telescope:publish
@ -20,7 +27,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

@ -10,16 +10,11 @@ function wait_for_db {
done done
} }
mkdir -p /app/packages/laravel-nami/.cookies || true mkdir /app/packages/laravel-nami/.cookies || true
mkdir -p /app/storage/app/public || true mkdir /app/storage/app || 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,10 +22,13 @@ 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'
php artisan scout:sync-index-settings
php-fpm -F -R -O php-fpm -F -R -O
fi fi

View File

@ -1,17 +1,12 @@
**/node_modules node_modules
data data
**/vendor vendor
public/build public/build
public/vendor public/vendor
bootstrap/cache/services.php bootstrap/cache/services.php
bootstrap/cache/packages.php bootstrap/cache/packages.php
bootstrap/cache/routes.php bootstrap/cache/routes.php
packages/laravel-nami/.cookies packages/laravel-nami/.cookies
storage/logs/** app/storage/app
storage/temp/**
storage/debugbar/**
storage/app/tmp/**
cookies cookies
storage/logs/laravel.log storage/logs/laravel.log
.git
doc

View File

@ -12,38 +12,32 @@ 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
- name: mysql_healthcheck - name: mysql_healthcheck
image: mysql:oracle image: mariadb/server:10.3
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:17.9.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 && npm run img && npm run prod && rm -R node_modules
- name: tests - name: tests
image: zoomyboy/adrema-base:latest image: php:8.1.6
commands: commands:
- touch .env - apt-get update > /dev/null
- apt-get install -y rsync libcurl3-dev apt-utils zlib1g-dev libpng-dev libicu-dev libonig-dev unzip > /dev/null
- 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 > /dev/null
- docker-php-ext-install pdo_mysql curl gd exif intl mbstring pcntl > /dev/null
- pecl install redis && docker-php-ext-enable redis > /dev/null
- php artisan migrate - php artisan migrate
- php artisan test - php artisan test
- rm -f .env - rm -f .env
- vendor/bin/phpstan analyse - vendor/bin/phpstan analyse --memory-limit=2G
environment: environment:
APP_NAME: Scoutrobot APP_NAME: Scoutrobot
APP_KEY: APP_KEY:
@ -51,6 +45,7 @@ steps:
APP_ENV: local APP_ENV: local
APP_DEBUG: true APP_DEBUG: true
APP_URL: http://scoutrobot.test APP_URL: http://scoutrobot.test
APP_MODE: stamm
LOG_CHANNEL: stack LOG_CHANNEL: stack
DB_CONNECTION: mysql DB_CONNECTION: mysql
DB_HOST: db DB_HOST: db
@ -67,10 +62,6 @@ steps:
MAIL_FROM_NAME: '${APP_NAME}' MAIL_FROM_NAME: '${APP_NAME}'
PDFLATEX_BIN: /usr/bin/pdflatex PDFLATEX_BIN: /usr/bin/pdflatex
XELATEX_BIN: /usr/bin/xelatex XELATEX_BIN: /usr/bin/xelatex
SCOUT_DRIVER: database
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 +94,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
@ -121,22 +112,13 @@ steps:
- name: github push - name: github push
image: alpine/git image: alpine/git
environment:
SSH_KEY:
from_secret: github_private_key
commands: commands:
- mkdir $HOME/.ssh
- git config core.sshCommand "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
- echo "$SSH_KEY" > $HOME/.ssh/id_rsa
- chmod 600 $HOME/.ssh/id_rsa
- git remote add gh git@github.com:zoomyboy/adrema.git
- git push -f gh HEAD:master
when: when:
branch: master branch: master
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
@ -161,59 +143,14 @@ steps:
services: services:
- name: db - name: db
image: mariadb:10.6.5 image: mariadb/server:10.3
environment: environment:
MARIADB_DATABASE: db MARIADB_DATABASE: db
MARIADB_USER: db MARIADB_USER: db
MARIADB_PASSWORD: db MARIADB_PASSWORD: db
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes MARIADB_ALLOW_EMPTY_PASSWORD: yes
- name: redis - name: redis
image: redis image: redis
- name: meilisearch
image: getmeili/meilisearch:v1.6
commands:
- 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:

View File

@ -3,26 +3,11 @@
"browser": true, "browser": true,
"es2021": true "es2021": true
}, },
"extends": [ "extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
"eslint:recommended",
"plugin:vue/vue3-recommended",
"prettier"
],
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module" "sourceType": "module"
}, },
"plugins": [ "plugins": ["vue"],
"vue" "rules": {}
],
"overrides": [
{
"files": [
"*.vue"
],
"rules": {
"vue/multi-word-component-names": "off"
}
}
]
} }

54
.gitignore vendored
View File

@ -1,43 +1,27 @@
node_modules/ /node_modules
/public/build
/public/sprite.svg
/public/hot
/public/vendor
/storage/*.key
/vendor
.env
.env.backup
.env.testing
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log npm-debug.log
yarn-error.log yarn-error.log
/bootstrap/compiled.php tags
/app/storage/ /storage/temp
/public/storage
/public/hot
/public/build
/public/vendor
storage/*.key
/storage/media-library/*
/vendor/
Homestead.yaml
Homestead.json
.vagrant/
.phpunit.result.cache
/storage/temp/
/storage/debugbar/
/tests/Fileshare/oc_tmp/*
# User data files
/data/
/.app.env
.config/psysh
# Development files
/docker-compose.yml
/.env
/.env.backup
/.env.testing
# Editor config files
/.vscode/
# Temporary files # Temporary files
*.swp *.swp
*.swo *.swo
*.swm *.swm
/resources/img/sprite.svg resources/img/sprite.svg
/public/sprite.svg
/.php-cs-fixer.cache /.php-cs-fixer.cache
/groups.sql /data
/.phpunit.cache .app.env
cookies

16
.gitmodules vendored
View File

@ -1,19 +1,9 @@
[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
[submodule "packages/tex"] [submodule "packages/tex"]
path = packages/tex path = packages/tex
url = https://git.zoomyboy.de/pille/tex url = https://git.zoomyboy.de/pille/tex
[submodule "packages/adrema-form"]
path = packages/adrema-form
url = https://git.zoomyboy.de/silva/adrema-form.git
[submodule "packages/medialibrary-helper"]
path = packages/medialibrary-helper
url = https://git.zoomyboy.de/zoomyboy/medialibrary-helper.git
branch = version2
[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

@ -1,4 +1,4 @@
@servers(['docker' => ['stammsilva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']]) @servers(['docker' => ['stammsilva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de']])
@task('deploy', ['on' => 'docker']) @task('deploy', ['on' => 'docker'])
cd $ADREMA_PATH cd $ADREMA_PATH

131
README.md
View File

@ -1,6 +1,6 @@
# Adrema # Adrema
**Schön, dass du den Weg hierhin gefunden hast!** __Schön, dass du den Weg hierhin gefunden hast!__
Da du diese Seite besuchst, gehörst du sicherlich zu den Leuten, die möglichst einfach die Daten ihrer Mitglieder pfelgen wollen. Das ist offiziell in der DPSG nur mit NaMi möglich. Da du diese Seite besuchst, gehörst du sicherlich zu den Leuten, die möglichst einfach die Daten ihrer Mitglieder pfelgen wollen. Das ist offiziell in der DPSG nur mit NaMi möglich.
@ -12,115 +12,78 @@ AdReMa kann von jedem und jeder genutzt werden, die einen NaMi-Account besitzt u
## Was kann ich mit AdReMa machen? ## Was kann ich mit AdReMa machen?
- Basisdaten von Mitgliedern anzeigen und bearbeiten * Basisdaten von Mitgliedern anzeigen und bearbeiten
- Einfacher Filter nach Gruppierung, Tätigkeit, etc * Einfacher Filter nach Gruppierung, Tätigkeit, etc
- Detailansichten mit allen zugehörigen Daten * Detailansichten mit allen zugehörigen Daten
- Führungszeugnisse und Präventionssulungen nachhalten * Führungszeugnisse und Präventionssulungen nachhalten
- Beitragszahlungen eintragen * Beitragszahlungen eintragen
- Automatische Rechunungserstellung * Automatisches Rechunungssystem
- Eigenen Beitragssatz hinterlegen (z.B. interner Stammes-Jahresbeitrag) * Eigene Beiträge hinterlegen (z.B. interner Stammes-Jahresbeitrag)
- Generieren von Zuschusslisten (aktuell RdP NRW, Bdkj Hessen, Stadt Solingen, Stadt Remscheid, Stadt Frankfurt a. M.) * Generieren von Zuschusslisten (aktuell RdP NRW)
- Einpflegen von internen Tätigkeiten, die nicht in NaMi vorhanden sind (um z.B. stammes-interne AGs / AKs zu verwalten) * Einpflegen von internen Tätigkeiten, die nicht in NaMi vorhanden sind (um z.B. stammes-interne AGs / AKs zu verwalten)
- Automatisches Erstellen und Managen von E-Mail-Verteilern mittels Mailman 3.0 * Automatisches Erstellen und managen von E-Mail-Verteilern mittels Mailman 3.0
- eFz-Bescheinigung abrufen für alle Leitenden (das kann in NaMi nur jede\*r für sich selbst) * eFz-Bescheinigung abrufen für alle Leitenden (das kann normalerweise nur jede*r einzelne für sich selbst)
- Ausbildungen eintragen (WBK-Bausteine) * Ausbildungen eintragen (WBK-Bausteine)
- Abrufen von Kontakten ins eigene Telefonbuch (mittels CardDAV) * Abrufen von Kontakten ins eigene Telefonbuch (mittels CardDAV)
Ziel dieses Projektes ist es, viele Dinge, die man normalerweise manuell zu tun hat so gut es geht zu automatisieren oder zumindest zu vereinfachen. So kann man sich als Leitende\*r / Vorstand auf die wichtigeren Dinge konzentrieren wie Gruppenstunden, Lager, Leiterrunden, etc. Ziel dieses Projektes ist es, viele Dinge, die man normalerweise manuell zu tun hat so gut es geht zu automatisieren oder zumindest zu vereinfachen. So kann man sich als Leitende*r / Vorstand auf die wichtigeren Dinge konzentrieren wie Gruppenstunden, Lager, Leiterrunden, etc.
Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles Design") Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles Design")
## Installation des Produktivsystems # Installation
1. Herunterladen der Beispiel Docker-Compose ## App Key generieren
```cmd Kopiere .app.env.example nach .app.env
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/docker-compose.prod.yml -o docker-compose.yml
```
2. Herunterladen der Beispiel Environmentvariablen-Datei ```
cp .app.env.example .app.env
```
```cmd Services starten:
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/.app.env.example -o .app.env
```
3. In der `.app.env` notwendige Einstellungen vornehmen: ```
docker compose up
```
- `APP_URL`: Hier sollte die URL (mit HTTPS) stehen, unter der Adrema erreichbar sein soll (z.B. `https://adrema.stamm-bipi.de`) Es wird die ein App Key generiert: ``Keinen APP KEY gefunden. Key wird generiert: base64:..........``
- Mail-Server Einstellungen `MAIL_PORT`, `MAIL_HOST`, `MAIL_USERNAME`, `MAIL_PASSWORD` und `MAIL_ENCRYPTION` anpassen
- `MAIL_FROM_NAME`: Der Name, der als Absender von E-Mails gesetzt wird (z.B. `Stamm Bipi Service`)
- `MAIL_FROM_ADDRESS`: Die dazu gehörige E-Mail-Adresse, die natürlich für antworten erreichbar sein sollte (z.B. `vorstand@stamm-bipi.de`)
- `DB_PASSWORD` und `MYSQL_PASSWORD`: Mit dem selben sicheren Passwort für die Datenbank versehen
- `USER_EMAIL` und `USER_PASSWORD`: Einstellen des standard Adrema Logins
4. Container zur Gennerierung des App-Key starten Kopiere diesen App key und setze in in .app.env als APP_KEY ein (APP_KEY=base64:........).
```cmd ## Einstellungen
docker compose up php
```
Nach einiger zeit wird ein App-Key generiert: Passe in der .app.env dann folgende Einstellungen an:
```cmd ### APP_URL
Keinen APP KEY gefunden. Key wird generiert: base64:xxx
```
Container herunterfahren und entfernen Hier sollte die URL (mit HTTPS) stehen, unter der Adrema erreichbar sein soll (z.B. https://adrema.stamm-bipi.de)
```cmd ### Mail
docker compose down
```
5. Der generierte App-Key muss als Environmentvariable (`APP_KEY`) mit in den Docker-Container gegeben werden. Kopiere den App-Key in die Datei `.app.env` Setze nun die Einstellungen für den Mail-Versand ein. Du solltest mindestens MAIL_PORT, MAIL_HOST, MAIL_USERNAME, MAIL_PASSWORD und MAIL_ENCRYPTION setzen.
```env MAIL_FROM_NAME ist der Name, der als Absender von E-Mails gesetzt wird. z.B. "Stamm Bipi Service".
APP_KEY=base64:xxx
```
6. Alle Container starten MAIL_FROM_ADDRESS die dazu gehörige E-Mail-Adresse, die natürlich erreichbar sein sollte (z.B. "vorstand@stamm-bipi.de").
```cmd ### DB Passwort
docker compose up -d
```
7. Nach kurzer Zeit ist AdReMa über <http://localhost:8000> erreichbar und es kann sich mit dem zuvor festgelegten Login eingeloggt werden Setze die beiden letzten Variablen (da wo "secret_db_password" steht) auf ein generiertes sicheres Passwort. Bei beiden Variablen muss der gleiche Wert eingestellt werden (also so wie vorher, nur sicherer :D )
### Individuelle anpassungen ## Starten
#### Rechnungswesen Führe nun den DB Container aus, um eine erste Version der Datenbank zu erstellen.
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. ```
docker-compose up db -d
```
## Nutzen des Entwicklungssystmes Nun kannst du auf localhost:8000 die App öffnen, einen LB verwenden, den Port mit CLI Optionen ändern, etc.
1. Klonen des Reposetories ## Standard Login
```cmd Beim ersten Starten wird ein Benutzer mit folgenden Zugangsdaten erstellt:
git clone https://git.zoomyboy.de/silva/adrema.git
```
2. Kopieren der Beispiel Docker-Compose für das Entwickeln und nach Wünschen anpassen * E-Mail-Adresse: admin@example.com
* Passwort: admin
```cmd
cp docker-compose.dev.yml docker-compose.yml
```
3. Kopieren der Beispiel Environmentvariablen-Datei
```cmd
cp .app.env.example .app.env
```
4. Submodule aktuallisieren
```cmd
git submodule update --init
```
5. Container erstellen
```cmd
docker compose build
```
6. Mit Schritt 3 und den folgenden der [Installation des Produktivsystems](#installation-des-produktivsystems) fortfahren

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

@ -2,16 +2,16 @@
namespace App; namespace App;
use App\Http\Views\ActivityFilterScope;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Database\Factories\ActivityFactory; 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\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;
@ -33,6 +33,16 @@ class Activity extends Model
]; ];
} }
/**
* @param Builder<self> $query
*
* @return Builder<self>
*/
public function scopeWithFilter(Builder $query, ActivityFilterScope $filter): Builder
{
return $filter->apply($query);
}
/** /**
* @return BelongsToMany<Subactivity> * @return BelongsToMany<Subactivity>
*/ */

View File

@ -4,6 +4,7 @@ namespace App\Activity\Actions;
use App\Activity; use App\Activity;
use App\Activity\Resources\ActivityResource; use App\Activity\Resources\ActivityResource;
use App\Http\Views\ActivityFilterScope;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
@ -14,18 +15,19 @@ class IndexAction
{ {
use AsAction; use AsAction;
public function handle(): AnonymousResourceCollection public function handle(ActivityFilterScope $filter): AnonymousResourceCollection
{ {
return ActivityResource::collection(Activity::paginate(20)); return ActivityResource::collection(Activity::withFilter($filter)->paginate(20));
} }
public function asController(ActionRequest $request): Response public function asController(ActionRequest $request): Response
{ {
session()->put('menu', 'activity'); session()->put('menu', 'activity');
session()->put('title', 'Tätigkeiten'); session()->put('title', 'Tätigkeiten');
$filter = ActivityFilterScope::fromRequest($request->input('filter'));
return Inertia::render('activity/VIndex', [ return Inertia::render('activity/VIndex', [
'data' => $this->handle(), 'data' => $this->handle($filter),
]); ]);
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Activity\Resources; namespace App\Activity\Resources;
use App\Activity; use App\Activity;
use App\Http\Views\ActivityFilterScope;
use App\Lib\HasMeta; use App\Lib\HasMeta;
use App\Resources\SubactivityResource; use App\Resources\SubactivityResource;
use App\Subactivity; use App\Subactivity;
@ -50,10 +51,10 @@ class ActivityResource extends JsonResource
{ {
return [ return [
'subactivities' => SubactivityResource::collectionWithoutMeta(Subactivity::get()), 'subactivities' => SubactivityResource::collectionWithoutMeta(Subactivity::get()),
'filter' => ActivityFilterScope::fromRequest(request()->input('filter')),
'links' => [ 'links' => [
'index' => route('activity.index'), 'index' => route('activity.index'),
'create' => route('activity.create'), 'create' => route('activity.create'),
'membership_masslist' => route('membership.masslist.index'),
], ],
]; ];
} }

View File

@ -1,26 +0,0 @@
<?php
namespace App\Auth;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;
class ResetPassword extends BaseResetPassword
{
/**
* Get the reset password notification mail message for the given URL.
*
* @param string $url
* @return MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject(Lang::get('Passwort zurücksetzen | Adrema'))
->line(Lang::get('Du erhälst diese E-Mail, weil du eine Anfrage zum zurücksetzen deines Account-Passworts gestellt hast.'))
->action(Lang::get('Passwort zurücksetzen'), $url)
->line(Lang::get('Dieser Link wird in :count Minuten ablaufen.', ['count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]))
->line(Lang::get('Wenn du die Anfrage nicht selbst gestellt hast, ist keine weitere Aktion erforderlich.'));
}
}

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

@ -49,6 +49,6 @@ class GenerateAction
*/ */
private function payload(ActionRequest $request): array private function payload(ActionRequest $request): array
{ {
return json_decode(rawurldecode(base64_decode($request->input('payload', ''))), true); return json_decode(base64_decode($request->input('payload', '')), true);
} }
} }

View File

@ -2,13 +2,10 @@
namespace App\Contribution; namespace App\Contribution;
use App\Contribution\Documents\BdkjHesse;
use App\Contribution\Documents\ContributionDocument; use App\Contribution\Documents\ContributionDocument;
use App\Contribution\Documents\RdpNrwDocument; use App\Contribution\Documents\DvDocument;
use App\Contribution\Documents\CityRemscheidDocument; use App\Contribution\Documents\RemscheidDocument;
use App\Contribution\Documents\CitySolingenDocument; use App\Contribution\Documents\SolingenDocument;
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;
@ -18,21 +15,18 @@ class ContributionFactory
* @var array<int, class-string<ContributionDocument>> * @var array<int, class-string<ContributionDocument>>
*/ */
private array $documents = [ private array $documents = [
RdpNrwDocument::class, DvDocument::class,
CitySolingenDocument::class, SolingenDocument::class,
CityRemscheidDocument::class, RemscheidDocument::class,
CityFrankfurtMainDocument::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']),
@ -54,41 +54,21 @@ class MemberData extends Data
public function fullname(): string public function fullname(): string
{ {
return $this->firstname . ' ' . $this->lastname; return $this->firstname.' '.$this->lastname;
} }
public function separatedName(): string public function separatedName(): string
{ {
return $this->lastname . ', ' . $this->firstname; return $this->lastname.', '.$this->firstname;
} }
public function fullAddress(): string public function fullAddress(): string
{ {
return $this->address . ', ' . $this->zip . ' ' . $this->location; return $this->address.', '.$this->zip.' '.$this->location;
} }
public function city(): string public function age(): string
{ {
return $this->zip . ' ' . $this->location; return (string) $this->birthday->diffInYears(now()) ?: '';
}
public function age(): int
{
return intval($this->birthday->diffInYears(now()));
}
public function birthYear(): string
{
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

@ -1,93 +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 App\Invoice\InvoiceSettings;
use Illuminate\Support\Collection;
class CityFrankfurtMainDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
public string $fromName;
/**
* @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 $eventName,
public ?string $filename = '',
public string $type = 'F',
) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long;
}
/**
* {@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(15),
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(15),
eventName: $request['eventName'],
);
}
public function countryName(): string
{
return $this->country->name;
}
public function pages(): int
{
return count($this->members);
}
public static function getName(): string
{
return 'Frankfurt';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
];
}
}

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\HasPdfBackground;
use App\Country; use App\Country;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class BdkjHesse extends ContributionDocument class DvDocument extends ContributionDocument
{ {
use HasPdfBackground;
/** /**
* @param Collection<int, Collection<int, MemberData>> $members * @param Collection<int, Collection<int, MemberData>> $members
*/ */
@ -22,21 +20,16 @@ class BdkjHesse extends ContributionDocument
public string $zipLocation, public string $zipLocation,
public ?Country $country, public ?Country $country,
public Collection $members, public Collection $members,
public string $eventName,
public ?string $filename = '', public ?string $filename = '',
public string $type = 'F', public string $type = 'F',
) { ) {
$this->setEventName($eventName);
} }
public function dateFrom(): string public function dateRange(): string
{ {
return Carbon::parse($this->dateFrom)->format('d.m.Y'); return Carbon::parse($this->dateFrom)->format('d.m.Y')
} .' - '
.Carbon::parse($this->dateUntil)->format('d.m.Y');
public function dateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
} }
/** /**
@ -49,8 +42,7 @@ class BdkjHesse extends ContributionDocument
dateUntil: $request['dateUntil'], dateUntil: $request['dateUntil'],
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(20), members: MemberData::fromModels($request['members'])->chunk(17),
eventName: $request['eventName'],
); );
} }
@ -64,8 +56,7 @@ class BdkjHesse extends ContributionDocument
dateUntil: $request['dateUntil'], dateUntil: $request['dateUntil'],
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(20), members: MemberData::fromApi($request['member_data'])->chunk(17),
eventName: $request['eventName'],
); );
} }
@ -74,22 +65,9 @@ class BdkjHesse extends ContributionDocument
return $this->country->name; return $this->country->name;
} }
public function durationDays(): int public function memberShort(MemberData $member): string
{ {
return intVal(Carbon::parse($this->dateUntil)->diffInDays(Carbon::parse($this->dateFrom))) + 1; return $member->isLeader ? 'L' : '';
}
/**
* @param Collection<int, MemberData> $chunk
*/
public function membersDays(Collection $chunk): int
{
return $this->durationDays() * $chunk->count();
}
public function pages(): int
{
return count($this->members);
} }
public function memberName(MemberData $member): string public function memberName(MemberData $member): string
@ -97,9 +75,9 @@ class BdkjHesse extends ContributionDocument
return $member->separatedName(); return $member->separatedName();
} }
public function memberCity(MemberData $member): string public function memberAddress(MemberData $member): string
{ {
return $member->city(); return $member->fullAddress();
} }
public function memberGender(MemberData $member): string public function memberGender(MemberData $member): string
@ -111,14 +89,41 @@ class BdkjHesse extends ContributionDocument
return strtolower(substr($member->gender->name, 0, 1)); return strtolower(substr($member->gender->name, 0, 1));
} }
public function memberBirthYear(MemberData $member): string public function memberAge(MemberData $member): string
{ {
return $member->birthYear(); return $member->age();
}
public function basename(): string
{
return 'zuschuesse-dv';
}
public function view(): string
{
return 'tex.zuschuss-dv';
}
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 DV erstellen';
} }
/** /**
@ -131,6 +136,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

@ -1,84 +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 RdpNrwDocument 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(17),
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(17),
eventName: $request['eventName'],
);
}
public function countryName(): string
{
return $this->country->name;
}
public static function getName(): string
{
return 'RdP NRW';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
];
}
}

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 RemscheidDocument 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.zuschuss-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

@ -3,15 +3,14 @@
namespace App\Contribution\Documents; namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData; use App\Contribution\Data\MemberData;
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 SolingenDocument extends ContributionDocument
{ {
public string $fromName;
/** /**
* @param Collection<int, MemberData> $members * @param Collection<int, MemberData> $members
*/ */
@ -23,8 +22,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;
} }
/** /**
@ -73,19 +70,34 @@ 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 = '';
$firstRow = collect(['B' => 'Jugendbildungsmaßnahme', 'G' => 'Gruppenleiter/innenschulung', 'FK' => 'Ferienkolonie', 'F' => 'Freizeitnaßnahme'])->map(function ($item, $key) { $firstRow = collect(['B' => 'Jugendbildungsmaßnahme', 'G' => 'Gruppenleiter/innenschulung', 'FK' => 'Ferienkolonie', 'F' => 'Freizeitnaßnahme'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox') . '{' . $item . '}'; return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox').'{'.$item.'}';
})->implode(' & ') . ' \\\\'; })->implode(' & ').' \\\\';
$secondRow = collect(['I' => 'Int. Jugendbegegnung', 'P' => 'politische Jugendbildung', 'PR' => 'Projekte'])->map(function ($item, $key) { $secondRow = collect(['I' => 'Int. Jugendbegegnung', 'P' => 'politische Jugendbildung', 'PR' => 'Projekte'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox') . '{' . $item . '}'; return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox').'{'.$item.'}';
})->implode(' & ') . ' & \\emptycheckbox \\\\'; })->implode(' & ').' & \\emptycheckbox \\\\';
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.zuschuss-stadt';
} }
public function getEngine(): Engine public function getEngine(): Engine
@ -95,7 +107,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 +119,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

@ -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

@ -1,46 +0,0 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\CourseMember;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Setting\NamiSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseDestroyAction
{
use AsAction;
use TracksJob;
public function handle(int $courseId): void
{
$course = CourseMember::find($courseId);
app(NamiSettings::class)->login()->deleteCourse($course->member->nami_id, $course->nami_id);
$course->delete();
}
public function asController(CourseMember $course): JsonResponse
{
$this->startJob($course->id, $course->member->fullname);
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$memberFullname = $parameters[1];
return $jobState
->before('Ausbildung für ' . $memberFullname . ' wird gelöscht')
->after('Ausbildung für ' . $memberFullname . ' gelöscht')
->failed('Fehler beim Löschen der Ausbildung für ' . $memberFullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\CourseMember;
use App\Course\Resources\CourseMemberResource;
use App\Member\Member;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseIndexAction
{
use AsAction;
/**
* @return Collection<int, CourseMember>
*/
public function handle(Member $member): Collection
{
return $member->courses()->with('course')->get();
}
public function asController(Member $member): AnonymousResourceCollection
{
return CourseMemberResource::collection($this->handle($member))
->additional([
'meta' => CourseMemberResource::memberMeta($member),
]);
}
}

View File

@ -1,72 +0,0 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\Course;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseStoreAction
{
use AsAction;
use TracksJob;
/**
* @param array<string, mixed> $attributes
*/
public function handle(Member $member, array $attributes): void
{
$course = Course::where('id', $attributes['course_id'])->firstOrFail();
$payload = collect($attributes)->only(['event_name', 'completed_at', 'organizer'])->merge([
'course_id' => $course->nami_id,
])->toArray();
$namiId = app(NamiSettings::class)->login()->createCourse($member->nami_id, $payload);
$member->courses()->create([
...$attributes,
'nami_id' => $namiId,
]);
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function asController(Member $member, ActionRequest $request): JsonResponse
{
$this->startJob($member, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0];
return $jobState
->before('Ausbildung für ' . $member->fullname . ' wird gespeichert')
->after('Ausbildung für ' . $member->fullname . ' gespeichert')
->failed('Fehler beim Erstellen der Ausbildung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace App\Course\Actions;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use App\Setting\NamiSettings;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class CourseUpdateAction
{
use AsAction;
use TracksJob;
/**
* @param array<string, string> $attributes
*/
public function handle(CourseMember $course, array $attributes): void
{
app(NamiSettings::class)->login()->updateCourse(
$course->member->nami_id,
$course->nami_id,
[
...$attributes,
'course_id' => Course::find($attributes['course_id'])->nami_id,
]
);
$course->update($attributes);
}
/**
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function asController(CourseMember $course, ActionRequest $request): JsonResponse
{
$this->startJob($course, $request->validated());
return response()->json([]);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
$member = $parameters[0]->member;
return $jobState
->before('Ausbildung für ' . $member->fullname . ' wird gespeichert')
->after('Ausbildung für ' . $member->fullname . ' gespeichert')
->failed('Fehler beim Erstellen der Ausbildung für ' . $member->fullname)
->shouldReload(JobChannels::make()->add('member')->add('course'));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Course\Controllers;
use App\Course\Models\CourseMember;
use App\Course\Requests\DestroyRequest;
use App\Course\Requests\StoreRequest;
use App\Course\Requests\UpdateRequest;
use App\Http\Controllers\Controller;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Http\RedirectResponse;
class CourseController extends Controller
{
public function store(Member $member, StoreRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $settings);
return redirect()->back()->success('Ausbildung erstellt');
}
public function update(Member $member, CourseMember $course, UpdateRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $course, $settings);
return redirect()->back()->success('Ausbildung aktualisiert');
}
public function destroy(Member $member, CourseMember $course, DestroyRequest $request, NamiSettings $settings): RedirectResponse
{
$request->persist($member, $course, $settings);
return redirect()->back()->success('Ausbildung gelöscht');
}
}

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;
@ -24,12 +22,4 @@ class Course extends Model
->trim() ->trim()
->replaceMatches('/ - .*/', ''); ->replaceMatches('/ - .*/', '');
} }
/**
* @return array<int, array{id: int, name: string}>
*/
public static function forSelect(): array
{
return static::select('name', 'id')->get()->toArray();
}
} }

View File

@ -2,15 +2,12 @@
namespace App\Course\Models; namespace App\Course\Models;
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> */
@ -23,12 +20,4 @@ class CourseMember extends Model
{ {
return $this->belongsTo(Course::class); return $this->belongsTo(Course::class);
} }
/**
* @return BelongsTo<Member, self>
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
} }

View File

@ -0,0 +1,38 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\CourseMember;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class DestroyRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [];
}
public function persist(Member $member, CourseMember $course, NamiSettings $settings): void
{
$settings->login()->deleteCourse($member->nami_id, $course->nami_id);
$course->delete();
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\Course;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function persist(Member $member, NamiSettings $settings): void
{
$course = Course::where('id', $this->input('course_id'))->firstOrFail();
$payload = collect($this->input())->only(['event_name', 'completed_at', 'organizer'])->merge([
'course_id' => $course->nami_id,
])->toArray();
$namiId = $settings->login()->createCourse($member->nami_id, $payload);
$member->courses()->create($this->safe()->collect()->put('nami_id', $namiId)->toArray());
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Course\Requests;
use App\Course\Models\Course;
use App\Course\Models\CourseMember;
use App\Member\Member;
use App\Setting\NamiSettings;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, string>
*/
public function rules()
{
return [
'organizer' => 'required|max:255',
'event_name' => 'required|max:255',
'completed_at' => 'required|date',
'course_id' => 'required|exists:courses,id',
];
}
public function persist(Member $member, CourseMember $course, NamiSettings $settings): void
{
$settings->login()->updateCourse(
$member->nami_id,
$course->nami_id,
$this->safe()->merge(['course_id' => Course::find($this->input('course_id'))->nami_id])->toArray()
);
$course->update($this->safe()->toArray());
}
}

View File

@ -2,8 +2,6 @@
namespace App\Course\Resources; namespace App\Course\Resources;
use App\Course\Models\Course;
use App\Member\Member;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@ -30,29 +28,6 @@ class CourseMemberResource extends JsonResource
'course_name' => $this->course->name, 'course_name' => $this->course->name,
'course_id' => $this->course->id, 'course_id' => $this->course->id,
'course' => new CourseResource($this->whenLoaded('course')), 'course' => new CourseResource($this->whenLoaded('course')),
'links' => [
'update' => route('course.update', ['course' => $this->getModel()]),
'destroy' => route('course.destroy', ['course' => $this->getModel()]),
]
];
}
/**
* @return array<string, mixed>
*/
public static function memberMeta(Member $member): array
{
return [
'default' => [
'event_name' => '',
'completed_at' => null,
'course_id' => null,
'organizer' => ''
],
'courses' => Course::forSelect(),
'links' => [
'store' => route('member.course.store', ['member' => $member]),
]
]; ];
} }
} }

View File

@ -4,10 +4,10 @@ namespace App\Dashboard;
use App\Dashboard\Blocks\Block; use App\Dashboard\Blocks\Block;
use App\Efz\EfzPendingBlock; use App\Efz\EfzPendingBlock;
use App\Invoice\MemberPaymentBlock;
use App\Member\PsPendingBlock; use App\Member\PsPendingBlock;
use App\Membership\AgeGroupCountBlock; use App\Membership\AgeGroupCountBlock;
use App\Membership\TestersBlock; use App\Membership\TestersBlock;
use App\Payment\MemberPaymentBlock;
class DashboardFactory class DashboardFactory
{ {

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,13 +248,13 @@ class AddressBookBackend extends AbstractBackend
} }
/** /**
* @return AddressBookCard * @return M
*/ */
private function cardMeta(Member $member): array private function cardMeta(Member $member): array
{ {
return [ return [
'lastmodified' => $member->updated_at->timestamp, 'lastmodified' => $member->updated_at->timestamp,
'etag' => '"' . $member->etag . '"', 'etag' => '"'.$member->etag.'"',
'uri' => $member->slug, 'uri' => $member->slug,
'id' => $member->id, 'id' => $member->id,
'size' => strlen($member->toVcard()->serialize()), 'size' => strlen($member->toVcard()->serialize()),

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)
{ {
@ -156,7 +155,7 @@ class Principal implements PrincipalBackendInterface
return null; return null;
} }
return ['addressbooks/' . $matches[1]]; return ['addressbooks/'.$matches[1]];
} }
/** /**
@ -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,8 +175,8 @@ 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

@ -1,33 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form;
use App\Form\Resources\FormApiResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Pagination\LengthAwarePaginator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormApiListAction
{
use AsAction;
/**
* @param string $filter
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter, int $perPage): LengthAwarePaginator
{
return FormFilterScope::fromRequest($filter)->getQuery()->paginate($perPage);
}
public function asController(ActionRequest $request): AnonymousResourceCollection
{
return FormApiResource::collection($this->handle(
$request->input('filter', ''),
$request->input('perPage', 9999)
));
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Lorisleiva\Actions\Concerns\AsAction;
class FormDestroyAction
{
use AsAction;
public function asController(Form $form): void
{
$form->delete();
ClearFrontendCacheAction::run();
Succeeded::message('Veranstaltung gelöscht.')->dispatch();
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Scopes\FormFilterScope;
use App\Form\Models\Form;
use App\Form\Resources\FormResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormIndexAction
{
use AsAction;
/**
* @return LengthAwarePaginator<Form>
*/
public function handle(string $filter): LengthAwarePaginator
{
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
}
public function asController(ActionRequest $request): Response
{
session()->put('menu', 'form');
session()->put('title', 'Veranstaltungen');
return Inertia::render('form/Index', [
'data' => FormResource::collection($this->handle($request->input('filter', ''))),
]);
}
}

View File

@ -1,75 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\ActionRequest;
class FormStoreAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
'description.time' => 'required|integer',
'description.blocks' => 'required|array',
'description.version' => 'required|string',
'excerpt' => 'required|string|max:130',
'from' => 'required|date',
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'header_image' => 'required|exclude',
'mailattachments' => 'present|array|exclude',
'is_active' => 'boolean',
'is_private' => 'boolean',
'export' => 'nullable|array',
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(array $attributes): Form
{
return tap(Form::create($attributes), function ($form) {
$form->setDeferredUploads(request()->input('header_image'));
$form->setDeferredUploads(request()->input('mailattachments'));
ClearFrontendCacheAction::run();
});
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
'from' => 'Start',
'to' => 'Ende',
'header_image' => 'Bild',
'description.blocks' => 'Beschreibung',
];
}
public function asController(ActionRequest $request): JsonResponse
{
$this->handle($request->validated());
Succeeded::message('Veranstaltung gespeichert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Lib\Editor\Condition;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\ActionRequest;
class FormUpdateAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
'description' => 'required|array',
'description.time' => 'required|integer',
'description.blocks' => 'required|array',
'description.version' => 'required|string',
'excerpt' => 'required|string|max:130',
'from' => 'required|date',
'to' => 'required|date',
'registration_from' => 'present|nullable|date',
'registration_until' => 'present|nullable|date',
'is_active' => 'boolean',
'is_private' => 'boolean',
'export' => 'nullable|array',
'needs_prevention' => 'present|boolean',
'prevention_text' => 'array',
'prevention_conditions' => 'array',
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(Form $form, array $attributes): Form
{
$form->update($attributes);
ClearFrontendCacheAction::run();
return $form;
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
'from' => 'Start',
'to' => 'Ende',
'description.blocks' => 'Beschreibung',
];
}
public function asController(Form $form, ActionRequest $request): JsonResponse
{
$this->handle($form, $request->validated());
Succeeded::message('Veranstaltung aktualisiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,46 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Illuminate\Validation\Rule;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\HttpFoundation\JsonResponse;
class FormUpdateMetaAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Form */
$form = request()->route('form');
return [
'sorting' => 'array',
'sorting.by' => 'required|string',
'sorting.direction' => 'required|boolean',
'active_columns' => 'array',
'active_columns.*' => ['string', Rule::in([...$form->getFields()->pluck('key')->toArray(), 'created_at', 'prevention'])]
];
}
/**
* @param array<string, mixed> $input
*/
public function handle(Form $form, array $input): void
{
$form->update(['meta' => $input]);
}
public function asController(Form $form, ActionRequest $request): JsonResponse
{
$this->handle($form, $request->validated());
return response()->json($form->fresh()->meta);
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateDestroyAction
{
use AsAction;
public function handle(Formtemplate $formtemplate): void
{
$formtemplate->delete();
}
public function asController(Formtemplate $formtemplate): JsonResponse
{
$this->handle($formtemplate);
Succeeded::message('Vorlage gelöscht.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,33 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Form\Resources\FormtemplateResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateIndexAction
{
use AsAction;
/**
* @return LengthAwarePaginator<Formtemplate>
*/
public function handle(): LengthAwarePaginator
{
return Formtemplate::paginate(15);
}
public function asController(): Response
{
session()->put('menu', 'form');
session()->put('title', 'Formular-Vorlagen');
return Inertia::render('formtemplate/Index', [
'data' => FormtemplateResource::collection($this->handle()),
]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateStoreAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
];
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(array $attributes): Formtemplate
{
return Formtemplate::create($attributes);
}
public function asController(ActionRequest $request): JsonResponse
{
$this->handle($request->validated());
Succeeded::message('Vorlage gespeichert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Formtemplate;
use App\Lib\Events\Succeeded;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormtemplateUpdateAction
{
use AsAction;
use HasValidation;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
...$this->globalRules(),
];
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
return [
...$this->globalValidationAttributes(),
];
}
/**
* @param array<string, mixed> $attributes
*/
public function handle(Formtemplate $formtemplate, array $attributes): void
{
$formtemplate->update($attributes);
}
public function asController(Formtemplate $formtemplate, ActionRequest $request): JsonResponse
{
$this->handle($formtemplate, $request->validated());
Succeeded::message('Vorlage aktualisiert.')->dispatch();
return response()->json([]);
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace App\Form\Actions;
use Illuminate\Validation\Rule;
use App\Form\Fields\Field;
use Illuminate\Validation\Validator;
use Lorisleiva\Actions\ActionRequest;
trait HasValidation
{
/**
* @return array<string, mixed>
*/
public function globalRules(): array
{
return [
'name' => 'required|string|max:255',
'config' => 'array',
'config.sections.*.name' => 'required',
'config.sections.*.intro' => 'nullable|string',
'config.sections.*.fields' => 'array',
'config.sections.*.fields.*.name' => 'required|string',
'config.sections.*.fields.*.type' => ['required', 'string', Rule::in(array_column(Field::asMeta(), 'id'))],
'config.sections.*.fields.*.key' => ['required', 'string', 'regex:/^[a-zA-Z_]*$/'],
'config.sections.*.fields.*.columns' => 'required|array',
'config.sections.*.fields.*.*' => '',
'config.sections.*.fields.*.columns.mobile' => 'required|numeric|gt:0|lte:2',
'config.sections.*.fields.*.columns.tablet' => 'required|numeric|gt:0|lte:4',
'config.sections.*.fields.*.columns.desktop' => 'required|numeric|gt:0|lte:6',
'mail_top' => 'array',
'mail_bottom' => 'array',
];
}
/**
* @return array<string, mixed>
*/
public function globalValidationAttributes(): array
{
return [
'config.sections.*.name' => 'Sektionsname',
'config.sections.*.fields.*.name' => 'Feldname',
'config.sections.*.fields.*.type' => 'Feldtyp',
'config.sections.*.fields.*.key' => 'Feldkey',
];
}
public function withValidator(Validator $validator, ActionRequest $request): void
{
if (!$validator->passes()) {
return;
}
foreach ($request->input('config.sections') as $sindex => $section) {
foreach (data_get($section, 'fields') as $findex => $field) {
$fieldClass = Field::classFromType($field['type']);
if (!$fieldClass) {
continue;
}
foreach ($fieldClass::metaRules() as $fieldName => $rules) {
$validator->addRules(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $rules]);
}
foreach ($fieldClass::metaAttributes() as $fieldName => $attribute) {
$validator->addCustomAttributes(["config.sections.{$sindex}.fields.{$findex}.{$fieldName}" => $attribute]);
}
}
}
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class IsDirtyAction
{
use AsAction;
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'config' => 'array|present',
];
}
public function handle(Form $form, ActionRequest $request): JsonResponse
{
$form->config = $request->input('config');
return response()->json([
'result' => $form->isDirty('config'),
]);
}
}

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,36 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Participant;
use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantDestroyAction
{
use AsAction;
use TracksJob;
public function handle(int $participantId): void
{
Participant::findOrFail($participantId)->delete();
}
public function asController(Participant $participant): void
{
$this->startJob($participant->id);
}
/**
* @param mixed $parameters
*/
public function jobState(WithJobState $jobState, ...$parameters): WithJobState
{
return $jobState
->after('Teilnehmer gelöscht.')
->failed('Löschen von Teilnehmer fehlgeschlagen.')
->shouldReload(JobChannels::make()->add('participant'));
}
}

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

@ -1,38 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Form\Resources\ParticipantResource;
use App\Form\Scopes\ParticipantFilterScope;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Laravel\Scout\Builder;
use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantIndexAction
{
use AsAction;
/**
* @return Builder<Participant>
*/
protected function getQuery(Form $form, ParticipantFilterScope $filter): Builder
{
return $filter->setForm($form)->getQuery()
->query(fn ($q) => $q->withCount('children')->with('form'));
}
public function asController(Form $form, ?int $parent = null): AnonymousResourceCollection
{
$filter = ParticipantFilterScope::fromRequest(request()->input('filter', ''))->parent($parent);
$data = match ($parent) {
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

@ -1,84 +0,0 @@
<?php
namespace App\Form\Actions;
use App\Form\Data\FieldCollection;
use App\Form\Models\Form;
use App\Form\Models\Participant;
use App\Member\Member;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class RegisterAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*/
public function handle(Form $form, array $input): Participant
{
if (!$form->canRegister()) {
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
}
$memberQuery = FieldCollection::fromRequest($form, $input)
->withNamiType()
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
$member = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first() : null;
$participant = $form->participants()->create([
'data' => $input,
'member_id' => $member?->id,
]);
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
$participant->sendConfirmationMail();
ExportSyncAction::dispatch($form->id);
return $participant;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationRules();
}
/**
* @return array<string, mixed>
*/
public function getValidationAttributes(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationAttributes();
}
/**
* @return array<string, mixed>
*/
public function getValidationMessages(): array
{
/** @var Form */
$form = request()->route('form');
return $form->getRegistrationMessages();
}
public function asController(ActionRequest $request, Form $form): JsonResponse
{
$participant = $this->handle($form, $request->validated());
return response()->json($participant);
}
}

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

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

View File

@ -1,21 +0,0 @@
<?php
namespace App\Form\Casts;
use App\Form\Data\FieldCollection;
use App\Form\Fields\Field;
use Spatie\LaravelData\Casts\Cast;
use Spatie\LaravelData\Support\Creation\CreationContext;
use Spatie\LaravelData\Support\DataProperty;
class FieldCollectionCast implements Cast
{
/**
* @param array<int, array<string, string>> $value
* @return FieldCollection
*/
public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed
{
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,16 +0,0 @@
<?php
namespace App\Form\Data;
use Spatie\LaravelData\Data;
class ColumnData extends Data
{
public function __construct(
public int $mobile,
public int $tablet,
public int $desktop,
) {
}
}

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

@ -1,144 +0,0 @@
<?php
namespace App\Form\Data;
use App\Form\Contracts\Filterable;
use App\Form\Enums\SpecialType;
use App\Form\Fields\Field;
use App\Form\Fields\NamiField;
use App\Form\Models\Form;
use Illuminate\Support\Collection;
use stdClass;
/**
* @extends Collection<int, Field>
*/
class FieldCollection extends Collection
{
public function forMembers(): self
{
return $this->filter(fn ($field) => $field->forMembers === true);
}
public function withNamiType(): self
{
return $this->filter(fn ($field) => $field->namiType !== null);
}
public function noNamiType(): self
{
return $this->filter(fn ($field) => $field->namiType === null);
}
public function noNamiField(): self
{
return $this->filter(fn ($field) => !is_a($field, NamiField::class));
}
public function hasNamiField(): bool
{
return $this->first(fn ($field) => is_a($field, NamiField::class)) !== null;
}
/**
* @return stdClass
*/
public function getMailRecipient(): ?stdClass
{
$email = $this->findBySpecialType(SpecialType::EMAIL)?->value;
return $this->getFullname() && $email
? (object) [
'name' => $this->getFullname(),
"email" => $email,
] : null;
}
public function getFullname(): ?string
{
$firstname = $this->findBySpecialType(SpecialType::FIRSTNAME)?->value;
$lastname = $this->findBySpecialType(SpecialType::LASTNAME)?->value;
return $firstname && $lastname ? "$firstname $lastname" : null;
}
/**
* @param array<string, mixed> $input
*/
public static function fromRequest(Form $form, array $input): self
{
return $form->getFields()->map(function ($field) use ($input) {
$field->value = array_key_exists($field->key, $input) ? $input[$field->key] : $field->default();
return $field;
});
}
public function find(Field $givenField): ?Field
{
return $this->findByKey($givenField->key);
}
public function findByKey(string $key): ?Field
{
return $this->first(fn ($field) => $field->key === $key);
}
/**
* @return array<string, mixed>
*/
public function present(): array
{
$attributes = collect([]);
foreach ($this as $field) {
$attributes = $attributes->merge($field->present());
}
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
{
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

@ -1,32 +0,0 @@
<?php
namespace App\Form\Data;
use App\Form\Casts\CollectionCast;
use App\Form\Transformers\CollectionTransformer;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Attributes\WithCast;
use Spatie\LaravelData\Attributes\WithTransformer;
class FormConfigData extends Data
{
/**
* @param Collection<int, SectionData> $sections
*/
public function __construct(
#[WithCast(CollectionCast::class, target: SectionData::class)]
#[WithTransformer(CollectionTransformer::class, target: SectionData::class)]
public Collection $sections
) {
}
public function fields(): FieldCollection
{
return $this->sections->reduce(
fn ($carry, $current) => $carry->merge($current->fields->all()),
new FieldCollection([])
);
}
}

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