Compare commits

..

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

930 changed files with 30165 additions and 48892 deletions

View File

@ -1,26 +0,0 @@
APP_NAME="Adrema"
APP_ENV=production
APP_KEY=YOUR_APP_KEY
APP_DEBUG=false
APP_URL=http://localhost:8000
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME=me
DB_PASSWORD=secret_db_password
MYSQL_PASSWORD=secret_db_password
MEILI_MASTER_KEY=secret_meilisearch_password
PUSHER_APP_HOST=socketi
WORKERS=5
USER_EMAIL=admin@example.com
USER_PASSWORD=admin

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,22 +0,0 @@
FROM composer:2.7.9 as composer
WORKDIR /app
COPY . /app
RUN composer install --ignore-platform-reqs --no-dev
RUN php artisan telescope:publish
RUN php artisan horizon:publish
FROM node:20.15.0-slim as node
WORKDIR /app
COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules
FROM nginx:1.21.6-alpine as nginx
WORKDIR /app
COPY --from=node /app /app
COPY --from=composer /app/public/vendor /app/public/vendor
COPY ./.docker/nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
VOLUME ["/app/public/storage"]
CMD ["nginx", "-g", "daemon off;"]

View File

@ -25,40 +25,12 @@ http {
root /app/public; root /app/public;
charset utf-8; charset utf-8;
index index.php; index index.php;
location /app/adremakey {
proxy_pass http://socketi:6001;
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 /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;
} }
location = /favicon.ico { access_log off; log_not_found off; } location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php; error_page 404 /index.php;
location = /.well-known/carddav {
return 301 $scheme://$host/dav;
}
location = /.well-known/caldav {
return 301 $scheme://$host/dav;
}
location ~ /\.ht { location ~ /\.ht {
deny all; deny all;
} }

View File

@ -1,27 +0,0 @@
FROM composer:2.7.9 as composer
WORKDIR /app
COPY . /app
RUN composer install --ignore-platform-reqs --no-dev
FROM node:20.15.0-slim as node
WORKDIR /app
COPY . /app
RUN npm install && npm run prod && npm run img && rm -R node_modules
FROM zoomyboy/adrema-base:latest as php
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=composer /app/vendor /app/vendor
USER www-data
RUN php artisan telescope:publish
RUN php artisan horizon:publish
USER root
COPY ./.docker/php /bin
VOLUME ["/app/packages/laravel-nami/.cookies", "/app/storage/app", "/app/resources/views/tex/invoice"]
EXPOSE 9000
CMD /bin/php-entrypoint

View File

@ -1,46 +0,0 @@
#!/bin/bash
set -e
function wait_for_db {
while true; do
echo "waiting for Database init"
php -r 'new PDO("mysql:host='$DB_HOST';dbname='$DB_DATABASE'", "'$DB_USERNAME'", "'$DB_PASSWORD'");' > /dev/null && return 0
sleep 1
done
}
mkdir -p /app/packages/laravel-nami/.cookies || true
mkdir -p /app/storage/app/public || true
chown -R www-data:www-data /app/packages/laravel-nami/.cookies
chown -R www-data:www-data /app/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
wait_for_db
su www-data -c 'php artisan horizon'
fi
if [ $1 == "app" ]; then
# --------------------------- ensure appkey is set ----------------------------
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
su www-data -c 'php artisan migrate --force'
php artisan scout:sync-index-settings
php-fpm -F -R -O
fi
if [ $1 == "schedule" ]; then
wait_for_db
while true; do
su www-data -c 'php artisan schedule:run -n'
sleep 60
done
fi

View File

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

View File

@ -1,3 +1,26 @@
# kind: pipeline
# type: ssh
# name: scoutrobot
#
# server:
# host: zoomyboy.de
# user: stammsilva
# ssh_key:
# from_secret: private_key
#
# clone:
# disable: true
#
# steps:
# - name: master
# commands:
# - /usr/local/bin/deploy_scoutrobot_master
# when:
# branch:
# - master
# event:
# - push
#
kind: pipeline kind: pipeline
type: docker type: docker
name: default name: default
@ -8,140 +31,31 @@ workspace:
steps: steps:
- name: submodules - name: submodules
image: alpine/git image: alpine/git
environment:
SSH_KEY:
from_secret: private_key
KNOWN_HOSTS:
from_secret: known_hosts
commands: commands:
- mkdir $HOME/.ssh
- echo "$SSH_KEY" > $HOME/.ssh/id_rsa
- echo "$KNOWN_HOSTS" > $HOME/.ssh/known_hosts
- cat $HOME/.ssh/known_hosts
- chmod 600 $HOME/.ssh/id_rsa
- git submodule update --init --recursive - git submodule update --init --recursive
- name: composer_dev - name: composer
image: composer:2.7.9 image: composer:2.2.7
commands:
- composer install --ignore-platform-reqs --dev
- name: mysql_healthcheck
image: mysql:oracle
commands:
- 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
image: node:20.15.0-slim
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
- name: tests
image: zoomyboy/adrema-base:latest
commands:
- touch .env
- php artisan migrate
- php artisan test
- rm -f .env
- vendor/bin/phpstan analyse
environment:
APP_NAME: Scoutrobot
APP_KEY:
from_secret: app_key
APP_ENV: local
APP_DEBUG: true
APP_URL: http://scoutrobot.test
LOG_CHANNEL: stack
DB_CONNECTION: mysql
DB_HOST: db
REDIS_HOST: redis
DB_PORT: 3306
DB_DATABASE: db
DB_USERNAME: db
DB_PASSWORD: db
BROADCAST_DRIVER: log
CACHE_DRIVER: file
QUEUE_CONNECTION: sync
SESSION_DRIVER: file
SESSION_LIFETIME: 120
MAIL_FROM_NAME: '${APP_NAME}'
PDFLATEX_BIN: /usr/bin/pdflatex
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
image: plugins/docker
settings:
dockerfile: ./.docker/php.Dockerfile
repo: zoomyboy/adrema-app
username: zoomyboy
password:
from_secret: docker_hub_token
tags:
- latest
- ${DRONE_TAG}
pull_image: true
when:
event: tag
- name: docker_webservice_push
image: plugins/docker
settings:
dockerfile: ./.docker/nginx.Dockerfile
repo: zoomyboy/adrema-webservice
username: zoomyboy
password:
from_secret: docker_hub_token
tags:
- latest
- ${DRONE_TAG}
pull_image: true
when:
event: tag
- name: deploy
image: zoomyboy/adrema-base:latest
environment:
SSH_KEY:
from_secret: deploy_private_key
commands:
- apt-get update > /dev/null
- apt-get install -y openssh-client
- mkdir $HOME/.ssh
- echo "UserKnownHostsFile=/dev/null" >> $HOME/.ssh/config
- echo "StrictHostKeyChecking=no" >> $HOME/.ssh/config
- echo "$SSH_KEY" > $HOME/.ssh/id_rsa
- chmod 600 $HOME/.ssh/id_rsa
- ./vendor/bin/envoy run deploy
when:
event: tag
- name: github push
image: alpine/git
environment:
SSH_KEY:
from_secret: github_private_key
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:
branch: master
event: push
- name: composer_no_dev
image: composer:2.7.9
commands: commands:
- composer install --ignore-platform-reqs --no-dev - composer install --ignore-platform-reqs --no-dev
- name: node
image: node:17.9.0-slim
commands:
- npm install && npm run prod && rm -R node_modules
- name: compress - name: compress
image: php:8.1.6 image: php:7.3
commands: commands:
- apt-get update -yqq - apt-get update -yqq
- apt-get install -yqq zip tar - apt-get install -yqq zip tar
@ -159,62 +73,6 @@ steps:
when: when:
event: tag event: tag
services:
- name: db
image: mariadb:10.6.5
environment:
MARIADB_DATABASE: db
MARIADB_USER: db
MARIADB_PASSWORD: db
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
- name: 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:
- push - push

55
.env.example Normal file
View File

@ -0,0 +1,55 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_MODE=
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_TEST_DATABASE=laraveltest
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
XELATEX=
PDFLATEX=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
NAMI_MGLNR=-1
NAMI_GROUP=-1
NAMI_PASSWORD=password
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

View File

@ -5,8 +5,7 @@
}, },
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:vue/vue3-recommended", "plugin:vue/essential"
"prettier"
], ],
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",
@ -15,14 +14,6 @@
"plugins": [ "plugins": [
"vue" "vue"
], ],
"overrides": [
{
"files": [
"*.vue"
],
"rules": { "rules": {
"vue/multi-word-component-names": "off"
} }
} }
]
}

52
.gitignore vendored
View File

@ -1,43 +1,27 @@
node_modules/ /node_modules
/public/hot
/public/storage
/public/js
/public/css
/public/fonts
/public/img
/storage/*.key
/vendor
.env
.env.backup
.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/
/public/storage
/public/hot
/public/build
/public/vendor /public/vendor
storage/*.key /storage/temp
/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 /public/mix-manifest.json
/public/sprite.svg resources/img/sprite.svg
/.php-cs-fixer.cache /.php-cs-fixer.cache
/groups.sql
/.phpunit.cache

20
.gitmodules vendored
View File

@ -1,19 +1,9 @@
[submodule "packages/silvaletter"]
path = packages/silvaletter
url = 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 = git@github.com:DPSGKoeln/laravel-nami.git
[submodule "packages/tex"] [submodule "packages/tex"]
path = packages/tex path = packages/tex
url = https://git.zoomyboy.de/pille/tex url = git@zoomyboy.de:pille/tex.git
[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,5 +1,5 @@
{ {
"printWidth": 200, "printWidth": 120,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 4,
"quoteProps": "consistent", "quoteProps": "consistent",

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.

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM composer:2.2.7 as composer
WORKDIR /app
COPY . /app
RUN composer install --ignore-platform-reqs --no-dev
FROM node:17.9.0-slim as node
WORKDIR /app
COPY . /app
RUN npm install && npm run prod && rm -R node_modules
FROM php:8.1.6-fpm as php
RUN apt-get update && apt-get install -y libcurl3-dev apt-utils zlib1g-dev libpng-dev libicu-dev libonig-dev texlive
RUN docker-php-ext-install pdo_mysql curl gd intl mbstring
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=composer /app/vendor /app/vendor
EXPOSE 9000
CMD ["php-fpm", "-F", "-R", "-O"]

View File

@ -1,8 +0,0 @@
@servers(['docker' => ['stammsilva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg-koeln@dpsg-koeln.de']])
@task('deploy', ['on' => 'docker'])
cd $ADREMA_PATH
docker compose down
docker compose pull
docker compose up -d
@endtask

128
README.md
View File

@ -1,126 +1,12 @@
# Adrema # DPSG Köln Adrema
**Schön, dass du den Weg hierhin gefunden hast!** ## Installation
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. Init and update git submodules to get laravel-nami Package
Die AdReMa (= "AddRessManagement") macht das auch, nur einfacher, schöner und intuitiver als es NaMi tut.
![Mitglieder-Übersicht](https://git.zoomyboy.de/silva/adrema/raw/branch/master/doc/page/assets/img/member.jpg)
AdReMa kann von jedem und jeder genutzt werden, die einen NaMi-Account besitzt und Schreibrechte hat (i.d.R. sind das Stammesvorstände, e.V.-Mitglieder und andere, die Mitgliederdaten und deren Abrechungen und Beiträge pflegen müssen).
## Was kann ich mit AdReMa machen?
- Basisdaten von Mitgliedern anzeigen und bearbeiten
- Einfacher Filter nach Gruppierung, Tätigkeit, etc
- Detailansichten mit allen zugehörigen Daten
- Führungszeugnisse und Präventionssulungen nachhalten
- Beitragszahlungen eintragen
- Automatische Rechunungserstellung
- Eigenen Beitragssatz hinterlegen (z.B. interner Stammes-Jahresbeitrag)
- Generieren von Zuschusslisten (aktuell RdP NRW, Bdkj Hessen, Stadt Solingen, Stadt Remscheid, Stadt Frankfurt a. M.)
- 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
- eFz-Bescheinigung abrufen für alle Leitenden (das kann in NaMi nur jede\*r für sich selbst)
- Ausbildungen eintragen (WBK-Bausteine)
- 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.
Außerdem ist AdReMa auch problemlos auf Handys und Tablets bedienbar ("mobiles Design")
## Installation des Produktivsystems
1. Herunterladen der Beispiel Docker-Compose
```cmd
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/docker-compose.prod.yml -o docker-compose.yml
``` ```
composer install --no-dev
2. Herunterladen der Beispiel Environmentvariablen-Datei npm ci
npm run prod
```cmd php artisan serve
curl https://git.zoomyboy.de/silva/adrema/raw/branch/master/.app.env.example -o .app.env
``` ```
3. In der `.app.env` notwendige Einstellungen vornehmen:
- `APP_URL`: Hier sollte die URL (mit HTTPS) stehen, unter der Adrema erreichbar sein soll (z.B. `https://adrema.stamm-bipi.de`)
- 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
```cmd
docker compose up php
```
Nach einiger zeit wird ein App-Key generiert:
```cmd
Keinen APP KEY gefunden. Key wird generiert: base64:xxx
```
Container herunterfahren und entfernen
```cmd
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`
```env
APP_KEY=base64:xxx
```
6. Alle Container starten
```cmd
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
### Individuelle anpassungen
#### Rechnungswesen
Bei dem Setup wird im Daten-Verzeichniss ein Ordner `./data/setup` angelegt. Hier kann das Logo des Stammes in den Briefkopf eingefügt werden. Zusätzlich kann der Text der Rechnung und der Zahlungserinnerung angepasst werden, dafür ist ein grundlegendes Verständnis für `.tex` Datein erforderlich.
## Nutzen des Entwicklungssystmes
1. Klonen des Reposetories
```cmd
git clone https://git.zoomyboy.de/silva/adrema.git
```
2. Kopieren der Beispiel Docker-Compose für das Entwickeln und nach Wünschen anpassen
```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

@ -1,37 +0,0 @@
<?php
namespace App\Actions;
use App\Course\Models\Course;
use App\Member\Member;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Data\Course as NamiCourse;
class InsertCoursesAction
{
use AsAction;
/**
* @param Collection<int, NamiCourse> $courses
*/
public function handle(Member $member, Collection $courses): void
{
if (!$member->hasNami) {
return;
}
foreach ($courses as $course) {
$member->courses()->updateOrCreate(['nami_id' => $course->id], [
'course_id' => Course::nami($course->courseId)->id,
'organizer' => $course->organizer,
'nami_id' => $course->id,
'completed_at' => $course->completedAt,
'event_name' => $course->eventName,
]);
}
$courseIds = $courses->map(fn ($course) => $course->id)->toArray();
$member->courses()->whereNotIn('nami_id', $courseIds)->whereNotNull('nami_id')->delete();
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Actions;
use App\Confession;
use App\Country;
use App\Fee;
use App\Gender;
use App\Group;
use App\Member\Member;
use App\Nationality;
use App\Payment\Subscription;
use App\Region;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Data\Member as NamiMember;
class InsertMemberAction
{
use AsAction;
public function handle(NamiMember $member): Member
{
$region = Region::firstWhere('nami_id', $member->regionId ?: -1);
return Member::updateOrCreate(['nami_id' => $member->id], [
'firstname' => $member->firstname,
'lastname' => $member->lastname,
'joined_at' => $member->joinedAt,
'birthday' => $member->birthday,
'send_newspaper' => $member->sendNewspaper,
'address' => $member->address,
'zip' => $member->zip,
'location' => $member->location,
'nickname' => $member->nickname,
'other_country' => $member->otherCountry,
'further_address' => $member->furtherAddress,
'main_phone' => $member->mainPhone,
'mobile_phone' => $member->mobilePhone,
'work_phone' => $member->workPhone,
'fax' => $member->fax,
'email' => $member->email,
'email_parents' => $member->emailParents,
'nami_id' => $member->id,
'group_id' => Group::firstOrCreate(['nami_id' => $member->groupId], ['nami_id' => $member->groupId, 'name' => $member->groupName])->id,
'gender_id' => optional(Gender::firstWhere('nami_id', $member->genderId ?: -1))->id,
'confession_id' => optional(Confession::firstWhere('nami_id', $member->confessionId ?: -1))->id,
'region_id' => $region && !$region->is_null ? $region->id : null,
'country_id' => optional(Country::where('nami_id', $member->countryId)->first())->id,
'subscription_id' => $this->getSubscription($member)?->id,
'nationality_id' => Nationality::where('nami_id', $member->nationalityId)->firstOrFail()->id,
'mitgliedsnr' => $member->memberId,
'version' => $member->version,
'keepdata' => $member->keepdata,
]);
}
public function getSubscription(NamiMember $member): ?Subscription
{
if (is_null($member->feeId)) {
return null;
}
$fee = Fee::nami($member->feeId);
if (is_null($fee)) {
$feeName = $member->feeName ?: 'Default';
$fee = Fee::create(['name' => $feeName, 'nami_id' => $member->feeId]);
$subscription = $fee->subscriptions()->create(['name' => $feeName]);
$subscription->children()->create(['name' => $feeName, 'amount' => 1000]);
}
return $fee->subscriptions()->first();
}
}

View File

@ -1,113 +0,0 @@
<?php
namespace App\Actions;
use App\Activity;
use App\Group;
use App\Initialize\ActivityCreator;
use App\Member\Member;
use App\Member\Membership;
use App\Setting\NamiSettings;
use App\Subactivity;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Api;
use Zoomyboy\LaravelNami\Data\MembershipEntry as NamiMembershipEntry;
class InsertMembershipsAction
{
use AsAction;
/**
* @param Collection<int, NamiMembershipEntry> $memberships
*/
public function handle(Member $member, Collection $memberships): void
{
if (!$member->hasNami) {
return;
}
foreach ($memberships as $membership) {
$existingMembership = Membership::where('nami_id', $membership->id)->first();
$group = Group::where('name', $membership->group)->whereNotNull('nami_id')->first();
if (!$group) {
continue;
}
if (null !== $this->overviewStrategy($member, $group, $membership)) {
continue;
}
$this->singleStrategy($member, $group, $membership);
}
$membershipIds = $memberships->map(fn ($membership) => $membership->id)->toArray();
$member->memberships()->whereNotIn('nami_id', $membershipIds)->whereNotNull('nami_id')->delete();
}
private function overviewStrategy(Member $member, Group $group, NamiMembershipEntry $membership): ?Membership
{
$activity = 1 === preg_match('/\(([0-9]+)\)/', $membership->activity, $activityMatches)
? Activity::where('nami_id', (int) $activityMatches[1])->first()
: null;
if (!$activity) {
return null;
}
if (null !== $membership->subactivity) {
$subactivity = Subactivity::remote()->where('name', $membership->subactivity)->first();
if (!$subactivity) {
return null;
}
} else {
$subactivity = null;
}
return $member->memberships()->updateOrCreate(['nami_id' => $membership->id], [
'nami_id' => $membership->id,
'from' => $membership->startsAt,
'to' => $membership->endsAt,
'group_id' => $group->id,
'activity_id' => $activity->id,
'subactivity_id' => $subactivity?->id,
]);
}
private function singleStrategy(Member $member, Group $group, NamiMembershipEntry $membershipEntry): ?Membership
{
$membership = $this->api()->membership($member->nami_id, $membershipEntry->id);
app(ActivityCreator::class)->createFor($this->api(), $membership->group());
$activity = Activity::nami($membership->activityId);
if (!$activity) {
return null;
}
if (null !== $membership->subactivityId) {
$subactivity = Subactivity::nami($membership->subactivityId);
if (!$subactivity) {
return null;
}
} else {
$subactivity = null;
}
return $member->memberships()->updateOrCreate(['nami_id' => $membership->id], [
'nami_id' => $membership->id,
'from' => $membership->startsAt,
'to' => $membership->endsAt,
'group_id' => $group->id,
'activity_id' => $activity->id,
'subactivity_id' => $subactivity?->id,
]);
}
private function api(): Api
{
return app(NamiSettings::class)->login();
}
}

View File

@ -0,0 +1,193 @@
<?php
namespace App\Actions;
use App\Activity;
use App\Confession;
use App\Country;
use App\Course\Models\Course;
use App\Fee;
use App\Gender;
use App\Group;
use App\Initialize\ActivityCreator;
use App\Member\Member;
use App\Member\Membership;
use App\Nationality;
use App\Region;
use App\Subactivity;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Zoomyboy\LaravelNami\Api;
use Zoomyboy\LaravelNami\Data\MembershipEntry;
use Zoomyboy\LaravelNami\Exceptions\RightException;
use Zoomyboy\LaravelNami\Member as NamiMember;
use Zoomyboy\LaravelNami\NamiException;
class MemberPullAction
{
private NamiMember $member;
private Api $api;
public function api(Api $api): self
{
$this->api = $api;
return $this;
}
public function member(int $groupId, int $memberId): self
{
$this->member = NamiMember::fromNami($this->api->member($groupId, $memberId));
return $this;
}
public function execute(): void
{
if (!$this->member->joined_at) {
return;
}
try {
$region = Region::firstWhere('nami_id', $this->member->region_id ?: -1);
$m = Member::updateOrCreate(['nami_id' => $this->member->id], [
'firstname' => $this->member->firstname,
'lastname' => $this->member->lastname,
'joined_at' => $this->member->joined_at,
'birthday' => $this->member->birthday,
'send_newspaper' => $this->member->send_newspaper,
'address' => $this->member->address,
'zip' => $this->member->zip,
'location' => $this->member->location,
'nickname' => $this->member->nickname,
'other_country' => $this->member->other_country,
'further_address' => $this->member->further_address,
'main_phone' => $this->member->main_phone,
'mobile_phone' => $this->member->mobile_phone,
'work_phone' => $this->member->work_phone,
'fax' => $this->member->fax,
'email' => $this->member->email,
'email_parents' => $this->member->email_parents,
'nami_id' => $this->member->id,
'group_id' => Group::firstOrCreate(['nami_id' => $this->member->group_id], ['nami_id' => $this->member->group_id, 'name' => $this->member->group_name])->id,
'gender_id' => optional(Gender::firstWhere('nami_id', $this->member->gender_id ?: -1))->id,
'confession_id' => optional(Confession::firstWhere('nami_id', $this->member->confession_id ?: -1))->id,
'region_id' => $region && !$region->is_null ? $region->id : null,
'country_id' => optional(Country::where('nami_id', $this->member->country_id)->first())->id,
'subscription_id' => $this->getSubscriptionId($this->member),
'nationality_id' => Nationality::where('nami_id', $this->member->nationality_id)->firstOrFail()->id,
'version' => $this->member->version,
]);
try {
foreach ($this->api->coursesFor($this->member->id) as $course) {
$m->courses()->updateOrCreate(['nami_id' => $course->id], [
'course_id' => Course::where('nami_id', $course->courseId)->firstOrFail()->id,
'organizer' => $course->organizer,
'event_name' => $course->eventName,
'completed_at' => $course->completedAt,
'nami_id' => $course->id,
]);
}
} catch (RightException $e) {
}
try {
foreach ($this->api->membershipsOf($this->member->id) as $membership) {
$existingMembership = Membership::where('nami_id', $membership->id)->first();
if (null !== $membership->endsAt && !$existingMembership) {
continue;
}
if (null !== $membership->endsAt && $existingMembership) {
$existingMembership->delete();
continue;
}
try {
[$activityId, $subactivityId, $groupId] = $this->fetchMembership($membership);
} catch (RightException $e) {
continue;
}
if (is_null($activityId)) {
continue;
}
$m->memberships()->updateOrCreate(['nami_id' => $membership->id], [
'nami_id' => $membership->id,
'from' => $membership->startsAt,
'group_id' => $groupId,
'activity_id' => $activityId,
'subactivity_id' => $subactivityId,
]);
}
} catch (RightException $e) {
}
} catch (ModelNotFoundException $e) {
dd($e->getMessage(), $this->member);
}
}
private function fetchMembership(MembershipEntry $membershipEntry): array
{
if ($this->shouldSyncMembership($membershipEntry)) {
$membership = $this->api->membership($this->member->id, $membershipEntry->id);
if (is_null($membership)) {
return [null, null, null];
}
app(ActivityCreator::class)->createFor($this->api, $membership->groupId);
$group = Group::firstOrCreate(['nami_id' => $membership->groupId], [
'nami_id' => $membership->groupId,
'name' => $membership->group,
]);
try {
$activityId = Activity::where('nami_id', $membership->activityId)->firstOrFail()->id;
$subactivityId = $membership->subactivityId
? Subactivity::where('nami_id', $membership->subactivityId)->firstOrFail()->id
: null;
return [$activityId, $subactivityId, $group->id];
} catch (ModelNotFoundException $e) {
return [null, null, null];
}
}
if (null === $membershipEntry->subactivity) {
$subactivityId = null;
} else {
$subactivityId = Subactivity::where('name', $membershipEntry->subactivity)->firstOrFail()->id;
}
preg_match('/\(([0-9]+)\)$/', $membershipEntry->activity, $activityMatches);
$activityId = Activity::where('nami_id', $activityMatches[1])->firstOrFail()->id;
$groupId = Group::where('name', $membershipEntry->group)->firstOrFail()->id;
return [$activityId, $subactivityId, $groupId];
}
private function shouldSyncMembership(MembershipEntry $membershipEntry): bool
{
if (!Group::where('name', $membershipEntry->group)->exists()) {
return true;
}
if (1 !== preg_match('/\(([0-9]+)\)/', $membershipEntry->activity, $activityMatches)) {
throw new NamiException("ID in taetigkeit string not found: {$membershipEntry->activity}");
}
if (!Activity::where('nami_id', (int) $activityMatches[1])->exists()) {
return true;
}
if (null === $membershipEntry->subactivity) {
return false;
}
return !Subactivity::where('name', $membershipEntry->subactivity)->exists();
}
public function getSubscriptionId(NamiMember $member): ?int
{
$fee = Fee::firstWhere('nami_id', $member->fee_id ?: -1);
if (is_null($fee)) {
return null;
}
return optional($fee->subscriptions()->first())->id;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions;
use App\Member\Member;
use App\Nami\Api\CoursesOfAction;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Api;
class PullCoursesAction
{
use AsAction;
public function handle(Member $member): void
{
if (!$member->hasNami) {
return;
}
InsertCoursesAction::run($member, CoursesOfAction::run($this->api(), $member->nami_id));
}
private function api(): Api
{
return app(NamiSettings::class)->login();
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Actions;
use App\Member\Member;
use App\Nami\Api\MemberAction;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Api;
class PullMemberAction
{
use AsAction;
public function handle(int $groupId, int $memberId): Member
{
return InsertMemberAction::run(MemberAction::run($this->api(), $groupId, $memberId));
}
private function api(): Api
{
return app(NamiSettings::class)->login();
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions;
use App\Member\Member;
use App\Nami\Api\MembershipsOfAction;
use App\Setting\NamiSettings;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Api;
class PullMembershipsAction
{
use AsAction;
public function handle(Member $member): void
{
if (!$member->hasNami) {
return;
}
InsertMembershipsAction::run($member, MembershipsOfAction::run($this->api(), $member->nami_id));
}
private function api(): Api
{
return app(NamiSettings::class)->login();
}
}

View File

@ -4,14 +4,12 @@ namespace App;
use App\Nami\HasNamiField; use App\Nami\HasNamiField;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Database\Factories\ActivityFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Activity extends Model class Activity extends Model
{ {
/** @use HasFactory<ActivityFactory> */
use HasFactory; use HasFactory;
use HasNamiField; use HasNamiField;
use Sluggable; use Sluggable;
@ -21,7 +19,6 @@ class Activity extends Model
public $casts = [ public $casts = [
'nami_id' => 'integer', 'nami_id' => 'integer',
'is_filterable' => 'boolean',
]; ];
public function sluggable(): array public function sluggable(): array
@ -33,9 +30,6 @@ class Activity extends Model
]; ];
} }
/**
* @return BelongsToMany<Subactivity>
*/
public function subactivities(): BelongsToMany public function subactivities(): BelongsToMany
{ {
return $this->belongsToMany(Subactivity::class); return $this->belongsToMany(Subactivity::class);

View File

@ -1,45 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use Illuminate\Http\RedirectResponse;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ActivityStoreAction
{
use AsAction;
/**
* @param array<string, mixed> $payload
*/
public function handle(array $payload): Activity
{
$activity = Activity::create($payload);
$activity->subactivities()->sync($payload['subactivities']);
return $activity;
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'name' => 'required|max:255',
'is_filterable' => 'present|boolean',
'subactivities' => 'present|array',
'subactivities.*' => 'integer',
];
}
public function asController(ActionRequest $request): RedirectResponse
{
$this->handle($request->validated());
return redirect()->route('activity.index');
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use App\Member\Membership;
use App\Subactivity;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
/**
* @template Payload of array{name: string, subactivities: array<int, int>}
*/
class ActivityUpdateAction
{
use AsAction;
/**
* @param Payload $payload
*/
public function handle(Activity $activity, array $payload): void
{
DB::transaction(function () use ($activity, $payload) {
$activity->update($payload);
$activity->subactivities()->sync($payload['subactivities']);
});
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'name' => 'required|max:255',
'is_filterable' => 'present|boolean',
'subactivities' => 'present|array',
'subactivities.*' => 'integer',
];
}
public function asController(ActionRequest $request, Activity $activity): RedirectResponse
{
if ($activity->hasNami) {
$this->validateNami($activity, $request->validated());
}
$removingSubactivities = $activity->subactivities()->whereNotIn('id', $request->validated('subactivities'))->pluck('id');
if ($removingSubactivities->first(fn ($subactivity) => Membership::where(['activity_id' => $activity->id, 'subactivity_id' => $subactivity])->exists())) {
throw ValidationException::withMessages(['subactivities' => 'Untergliederung hat noch Mitglieder.']);
}
$this->handle($activity, $request->validated());
return redirect()->route('activity.index');
}
/**
* @todo handle this with a model event on the pivot model
*
* @param Payload $payload
*/
private function validateNami(Activity $activity, array $payload): void
{
if ($activity->name !== $payload['name']) {
throw ValidationException::withMessages(['nami_id' => 'Aktivität ist in NaMi. Update des Namens nicht möglich.']);
}
$removingSubactivities = $activity->subactivities()->whereNotIn('id', $payload['subactivities'])->pluck('id');
if ($removingSubactivities->first(fn ($subactivity) => Subactivity::find($subactivity)->hasNami)) {
throw ValidationException::withMessages(['nami_id' => 'Untertätigkeit kann nicht entfernt werden.']);
}
$addingSubactivities = collect($payload['subactivities'])->filter(fn ($subactivityId) => $activity->subactivities->doesntContain($subactivityId));
if ($addingSubactivities->first(fn ($subactivity) => Subactivity::find($subactivity)->hasNami)) {
throw ValidationException::withMessages(['nami_id' => 'Untertätigkeit kann nicht hinzugefügt werden.']);
}
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity\Resources\ActivityResource;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class CreateAction
{
use AsAction;
public function handle(): Response
{
session()->put('menu', 'activity');
session()->put('title', 'Tätigkeit erstellen');
return Inertia::render('activity/VForm', [
'meta' => ActivityResource::meta(),
'data' => [
'name' => '',
'is_filterable' => false,
'subactivities' => [],
],
]);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use App\Member\Membership;
use Illuminate\Http\RedirectResponse;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\Concerns\AsAction;
class DestroyAction
{
use AsAction;
public function handle(Activity $activity): void
{
$activity->subactivities()->sync([]);
$activity->delete();
}
public function asController(Activity $activity): RedirectResponse
{
if (Membership::where('activity_id', $activity->id)->count()) {
throw ValidationException::withMessages(['activity' => 'Tätigkeit besitzt noch Mitglieder.']);
}
if ($activity->hasNami) {
throw ValidationException::withMessages(['activity' => 'Tätigkeit ist in NaMi.']);
}
$this->handle($activity);
return redirect()->route('activity.index');
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use App\Activity\Resources\ActivityResource;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction;
class EditAction
{
use AsAction;
public function handle(Activity $activity): Response
{
return Inertia::render('activity/VForm', [
'meta' => ActivityResource::meta(),
'data' => new ActivityResource($activity->load('subactivities')),
]);
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace App\Activity\Actions;
use App\Activity;
use App\Activity\Resources\ActivityResource;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class IndexAction
{
use AsAction;
public function handle(): AnonymousResourceCollection
{
return ActivityResource::collection(Activity::paginate(20));
}
public function asController(ActionRequest $request): Response
{
session()->put('menu', 'activity');
session()->put('title', 'Tätigkeiten');
return Inertia::render('activity/VIndex', [
'data' => $this->handle(),
]);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Activity\Api;
use App\Resources\SubactivityResource;
use App\Subactivity;
use Illuminate\Http\JsonResponse;
use Lorisleiva\Actions\Concerns\AsAction;
class SubactivityShowAction
{
use AsAction;
public function handle(): void
{
return;
}
public function asController(Subactivity $subactivity): JsonResponse
{
return response()->json([
'data' => new SubactivityResource($subactivity),
'meta' => SubactivityResource::meta(),
]);
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Activity\Api;
use App\Subactivity;
use DB;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Arr;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SubactivityStoreAction
{
use AsAction;
/**
* @param array<string, string|array<int, int>> $payload
*/
public function handle(array $payload): Subactivity
{
return DB::transaction(function () use ($payload) {
$subactivity = Subactivity::create(Arr::except($payload, 'activities'));
$subactivity->activities()->sync($payload['activities']);
return $subactivity;
});
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'name' => 'required|unique:subactivities,name',
'activities' => 'present|array|min:1',
'activities.*' => 'integer',
'is_filterable' => 'present|boolean',
];
}
/**
* @return array<string, string>
*/
public function getValidationAttributes(): array
{
return [
'activities' => 'Tätigkeiten',
];
}
public function asController(ActionRequest $request): JsonResponse
{
return response()->json($this->handle($request->validated()));
}
}

View File

@ -1,100 +0,0 @@
<?php
namespace App\Activity\Api;
use App\Activity;
use App\Member\Membership;
use App\Subactivity;
use DB;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class SubactivityUpdateAction
{
use AsAction;
/**
* @param array<string, string|array<int, int>> $payload
*/
public function handle(Subactivity $subactivity, array $payload): Subactivity
{
return DB::transaction(function () use ($subactivity, $payload) {
$subactivity->update(Arr::except($payload, 'activities'));
if (null !== data_get($payload, 'activities')) {
$subactivity->activities()->sync($payload['activities']);
}
return $subactivity;
});
}
/**
* @return array<string, string|array<int, mixed>>
*/
public function rules(): array
{
/** @var Subactivity */
$subactivity = request()->route('subactivity');
return [
'name' => ['required', 'string', 'max:255', Rule::unique('subactivities', 'name')->ignore($subactivity->id)],
'activities' => ['present', 'array', 'min:1'],
'activities.*' => 'integer',
'is_filterable' => 'present|boolean',
];
}
/**
* @return array<string, string>
*/
public function getValidationAttributes(): array
{
return [
'activities' => 'Tätigkeiten',
];
}
public function asController(ActionRequest $request, Subactivity $subactivity): JsonResponse
{
if ($subactivity->hasNami) {
$this->validateNami($subactivity, $request->validated());
}
$removingActivities = $subactivity->activities()->whereNotIn('id', $request->validated('activities'))->pluck('id');
if ($removingActivities->first(fn ($activity) => Membership::where(['activity_id' => $activity, 'subactivity_id' => $subactivity->id])->exists())) {
throw ValidationException::withMessages(['activities' => 'Tätigkeit hat noch Mitglieder.']);
}
return response()->json($this->handle($subactivity, $request->validated()));
}
/**
* @todo handle this with a model event on the pivot model
*
* @param array{name: string, activities: array<int, int>} $payload
*/
private function validateNami(Subactivity $subactivity, array $payload): void
{
if ($subactivity->name !== $payload['name']) {
throw ValidationException::withMessages(['name' => 'Untertätigkeit ist in NaMi. Update des Namens nicht möglich.']);
}
$removingActivities = $subactivity->activities()->whereNotIn('id', $payload['activities'])->pluck('id');
if ($removingActivities->first(fn ($activity) => Activity::find($activity)->hasNami)) {
throw ValidationException::withMessages(['activities' => 'Tätigkeit kann nicht entfernt werden.']);
}
$addingActivities = collect($payload['activities'])->filter(fn ($activityId) => $subactivity->activities->doesntContain($activityId));
if ($addingActivities->first(fn ($activity) => Activity::find($activity)->hasNami)) {
throw ValidationException::withMessages(['activities' => 'Tätigkeit kann nicht hinzugefügt werden.']);
}
}
}

View File

@ -1,60 +0,0 @@
<?php
namespace App\Activity\Resources;
use App\Activity;
use App\Lib\HasMeta;
use App\Resources\SubactivityResource;
use App\Subactivity;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Activity
*/
class ActivityResource extends JsonResource
{
use HasMeta;
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*
* @return array{id: int, name: string}
*/
public function toArray($request)
{
return [
'name' => $this->name,
'id' => $this->id,
'subactivities' => $this->subactivities->pluck('id')->toArray(),
'is_filterable' => $this->is_filterable,
'links' => [
'edit' => route('activity.edit', ['activity' => $this->getModel()]),
'update' => route('activity.update', ['activity' => $this->getModel()]),
'destroy' => route('activity.destroy', ['activity' => $this->getModel()]),
],
'subactivity_model' => [
'activities' => [$this->id],
'is_age_group' => false,
'is_filterable' => false,
'name' => '',
],
];
}
/**
* @return array<string, mixed>
*/
public static function meta(): array
{
return [
'subactivities' => SubactivityResource::collectionWithoutMeta(Subactivity::get()),
'links' => [
'index' => route('activity.index'),
'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

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Actions\MemberPullAction;
use App\Setting\NamiSettings;
use Illuminate\Console\Command;
use Zoomyboy\LaravelNami\Member as NamiMember;
class MemberResyncCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'member:resync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*
* @return int
*/
public function handle(NamiSettings $settings)
{
$api = $settings->login();
$api->search([])->each(
fn (NamiMember $member) => app(MemberPullAction::class)->api($api)->member($member->group_id, $member->id)->execute()
);
return 0;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Initialize\Initializer;
use Illuminate\Console\Command;
use Zoomyboy\LaravelNami\NamiException;
class NamiInitializeCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'nami:initialize {--mglnr=} {--password=} {--group_id=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initializes nami';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
app(Initializer::class)->run();
} catch (NamiException $e) {
$e->outputToConsole($this);
return 1;
}
return 0;
}
}

View File

@ -2,10 +2,7 @@
namespace App\Console; namespace App\Console;
use App\Actions\DbMaintainAction; use App\Letter\Actions\LetterSendAction;
use App\Form\Actions\PreventionRememberAction;
use App\Initialize\InitializeMembers;
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;
@ -14,13 +11,10 @@ class Kernel extends ConsoleKernel
/** /**
* The Artisan commands provided by your application. * The Artisan commands provided by your application.
* *
* @var array<int, class-string> * @var array
*/ */
protected $commands = [ protected $commands = [
InvoiceSendAction::class, LetterSendAction::class,
InitializeMembers::class,
DbMaintainAction::class,
PreventionRememberAction::class,
]; ];
/** /**
@ -30,10 +24,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
$schedule->command(DbMaintainAction::class)->daily(); // $schedule->command('inspire')->hourly();
$schedule->command(InitializeMembers::class)->dailyAt('03:00');
$schedule->command(PreventionRememberAction::class)->dailyAt('11:00');
$schedule->command(InvoiceSendAction::class)->dailyAt('10:00');
} }
/** /**

View File

@ -4,6 +4,8 @@ namespace App\Contribution\Actions;
use App\Contribution\ContributionFactory; use App\Contribution\ContributionFactory;
use App\Country; use App\Country;
use App\Member\Member;
use App\Member\MemberResource;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -18,10 +20,9 @@ class FormAction
public function handle(): array public function handle(): array
{ {
return [ return [
'countries' => Country::select('name', 'id')->get(), 'allMembers' => MemberResource::collection(Member::slangOrdered()->get()),
'data' => [ 'countries' => Country::pluck('name', 'id'),
'country' => Country::firstWhere('name', 'Deutschland')->id, 'defaultCountry' => Country::firstWhere('name', 'Deutschland')->id,
],
'compilers' => app(ContributionFactory::class)->compilerSelect(), 'compilers' => app(ContributionFactory::class)->compilerSelect(),
]; ];
} }

View File

@ -1,54 +0,0 @@
<?php
namespace App\Contribution\Actions;
use App\Contribution\ContributionFactory;
use App\Contribution\Documents\ContributionDocument;
use App\Rules\JsonBase64Rule;
use Illuminate\Support\Facades\Validator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class GenerateAction
{
use AsAction;
/**
* @param class-string<ContributionDocument> $document
* @param array<string, mixed> $payload
*/
public function handle(string $document, array $payload): BaseCompiler
{
return Tex::compile($document::fromRequest($payload));
}
public function asController(ActionRequest $request): BaseCompiler
{
$payload = $this->payload($request);
$type = data_get($payload, 'type');
ValidateAction::validateType($type);
Validator::make($payload, app(ContributionFactory::class)->rules($type))->validate();
return $this->handle($type, $payload);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'payload' => [new JsonBase64Rule()],
];
}
/**
* @return array<string, string>
*/
private function payload(ActionRequest $request): array
{
return json_decode(rawurldecode(base64_decode($request->input('payload', ''))), true);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Contribution\Actions;
use App\Contribution\Documents\ContributionDocument;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class GenerateApiAction
{
use AsAction;
/**
* @param class-string<ContributionDocument> $document
* @param array<string, mixed> $payload
*/
public function handle(string $document, array $payload): BaseCompiler
{
return Tex::compile($document::fromApiRequest($payload));
}
public function asController(ActionRequest $request): BaseCompiler
{
ValidateAction::validateType($request->input('type'));
return $this->handle($request->input('type'), $request->input());
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [];
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace App\Contribution\Actions;
use App\Contribution\ContributionFactory;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidateAction
{
use AsAction;
public function asController(): JsonResponse
{
return response()->json(['valid' => true]);
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return app(ContributionFactory::class)->rules(request()->type);
}
public function prepareForValidation(ActionRequest $request): void
{
static::validateType($request->input('type'));
}
public static function validateType(?string $type = null): void
{
Validator::make(['type' => $type], app(ContributionFactory::class)->typeRule())->validate();
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Contribution;
use App\Contribution\Documents\SolingenDocument;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Zoomyboy\Tex\BaseCompiler;
use Zoomyboy\Tex\Tex;
class ContributionController extends Controller
{
public function generate(Request $request): BaseCompiler
{
/** @var class-string<SolingenDocument> */
$type = $request->query('type');
return Tex::compile($type::fromRequest($request));
}
}

View File

@ -2,15 +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\Validation\Rule;
class ContributionFactory class ContributionFactory
{ {
@ -18,45 +13,19 @@ 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 array<int, array{id: string, name: string}>
*/ */
public function compilerSelect(): Collection public function compilerSelect(): array
{ {
return collect($this->documents)->map(fn ($document) => [ return collect($this->documents)->map(fn ($document) => [
'title' => $document::buttonName(), 'title' => $document::getName(),
'class' => $document, 'class' => $document,
]); ])->toArray();
}
/**
* @return array<string, mixed>
*/
public function typeRule(): array
{
return [
'type' => ['required', Rule::in($this->documents)],
];
}
/**
* @param class-string<ContributionDocument> $type
*
* @return array<string, mixed>
*/
public function rules(string $type): array
{
return [
...$type::globalRules(),
...$type::rules(),
];
} }
} }

View File

@ -1,94 +0,0 @@
<?php
namespace App\Contribution\Data;
use App\Gender;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Data;
class MemberData extends Data
{
public function __construct(
public string $firstname,
public string $lastname,
public string $address,
public string $zip,
public string $location,
public Carbon $birthday,
public ?Gender $gender,
public bool $isLeader
) {
}
/**
* @param array<int, int> $ids
*
* @return Collection<int, static>
*/
public static function fromModels(array $ids): Collection
{
return Member::whereIn('id', $ids)->orderByRaw('lastname, firstname')->get()->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([
...$member->toArray(),
'birthday' => $member->birthday->toAtomString(),
'isLeader' => $member->isLeader(),
'gender' => $member->gender,
]))->toBase();
}
/**
* @param array<int, ContributionMemberData> $data
*
* @return Collection<int, static>
*/
public static function fromApi(array $data): Collection
{
return collect($data)->map(fn ($member) => self::factory()->withoutMagicalCreation()->from([
...$member,
'birthday' => Carbon::parse($member['birthday'])->toAtomString(),
'gender' => Gender::fromString($member['gender']),
'isLeader' => $member['is_leader'],
]));
}
public function fullname(): string
{
return $this->firstname . ' ' . $this->lastname;
}
public function separatedName(): string
{
return $this->lastname . ', ' . $this->firstname;
}
public function fullAddress(): string
{
return $this->address . ', ' . $this->zip . ' ' . $this->location;
}
public function city(): string
{
return $this->zip . ' ' . $this->location;
}
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,136 +0,0 @@
<?php
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Contribution\Traits\HasPdfBackground;
use App\Country;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class BdkjHesse extends ContributionDocument
{
use HasPdfBackground;
/**
* @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);
}
public function dateFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function dateUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
/**
* {@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(20),
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(20),
eventName: $request['eventName'],
);
}
public function countryName(): string
{
return $this->country->name;
}
public function durationDays(): int
{
return intVal(Carbon::parse($this->dateUntil)->diffInDays(Carbon::parse($this->dateFrom))) + 1;
}
/**
* @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
{
return $member->separatedName();
}
public function memberCity(MemberData $member): string
{
return $member->city();
}
public function memberGender(MemberData $member): string
{
if (!$member->gender) {
return '';
}
return strtolower(substr($member->gender->name, 0, 1));
}
public function memberBirthYear(MemberData $member): string
{
return $member->birthYear();
}
public static function getName(): string
{
return 'BDKJ Hessen';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'country' => 'required|integer|exists:countries,id',
'zipLocation' => 'required|string',
];
}
}

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

@ -1,89 +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\Member\Member;
use Illuminate\Support\Collection;
class CityRemscheidDocument extends ContributionDocument
{
use HasPdfBackground;
use FormatsDates;
/**
* @param Collection<int, Collection<int, Member>> $leaders
* @param Collection<int, Collection<int, Member>> $children
*/
public function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
public Collection $leaders,
public Collection $children,
public ?string $filename = '',
public string $type = 'F',
public string $eventName = '',
) {
$this->setEventName($eventName);
}
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): self
{
[$leaders, $children] = MemberData::fromModels($request['members'])->partition(fn ($member) => $member->isLeader);
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): self
{
$members = MemberData::fromApi($request['member_data']);
[$leaders, $children] = $members->partition(fn ($member) => $member->isLeader);
return new self(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
country: Country::where('id', $request['country'])->firstOrFail(),
leaders: $leaders->values()->toBase()->chunk(6),
children: $children->values()->toBase()->chunk(20),
eventName: $request['eventName'],
);
}
public static function getName(): string
{
return 'Remscheid';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string',
'country' => 'required|integer|exists:countries,id',
];
}
}

View File

@ -1,112 +0,0 @@
<?php
namespace App\Contribution\Documents;
use App\Contribution\Data\MemberData;
use App\Invoice\InvoiceSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Zoomyboy\Tex\Engine;
class CitySolingenDocument extends ContributionDocument
{
public string $fromName;
/**
* @param Collection<int, MemberData> $members
*/
final private function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public Collection $members,
public string $eventName,
public string $type = 'F',
) {
$this->setEventName($eventName);
$this->fromName = app(InvoiceSettings::class)->from_long;
}
/**
* {@inheritdoc}
*/
public static function fromRequest(array $request): static
{
return new static(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
members: MemberData::fromModels($request['members']),
eventName: $request['eventName'],
);
}
/**
* {@inheritdoc}
*/
public static function fromApiRequest(array $request): static
{
return new static(
dateFrom: $request['dateFrom'],
dateUntil: $request['dateUntil'],
zipLocation: $request['zipLocation'],
members: MemberData::fromApi($request['member_data']),
eventName: $request['eventName'],
);
}
/**
* @return Collection<int, Collection<int, MemberData>>
*/
public function memberModels(): Collection
{
return $this->members->chunk(14);
}
public function niceEventFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceEventUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function checkboxes(): string
{
$output = '';
$firstRow = collect(['B' => 'Jugendbildungsmaßnahme', 'G' => 'Gruppenleiter/innenschulung', 'FK' => 'Ferienkolonie', 'F' => 'Freizeitnaßnahme'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox') . '{' . $item . '}';
})->implode(' & ') . ' \\\\';
$secondRow = collect(['I' => 'Int. Jugendbegegnung', 'P' => 'politische Jugendbildung', 'PR' => 'Projekte'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox') . '{' . $item . '}';
})->implode(' & ') . ' & \\emptycheckbox \\\\';
return $firstRow . "\n" . $secondRow;
}
public function getEngine(): Engine
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Stadt Solingen';
}
/**
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
'dateFrom' => 'required|string|date_format:Y-m-d',
'dateUntil' => 'required|string|date_format:Y-m-d',
'zipLocation' => 'required|string',
];
}
}

View File

@ -3,63 +3,8 @@
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;
/**
* @param ContributionRequestArray $request
*/
abstract public static function fromRequest(array $request): self;
/**
* @param ContributionApiRequestArray $request
*/
abstract public static function fromApiRequest(array $request): self;
/**
* @return array<string, mixed>
*/
abstract public static function rules(): array;
/**
* @return array<string, mixed>
*/
public static function globalRules(): array
{
return [
'eventName' => 'required|string',
'members' => 'present|array|min:1',
'members.*' => 'integer|exists:members,id',
];
}
public static function buttonName(): string
{
return 'Für ' . static::getName() . ' erstellen';;
}
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

@ -0,0 +1,110 @@
<?php
namespace App\Contribution\Documents;
use App\Country;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class DvDocument extends ContributionDocument
{
public function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
/* @var Collection<int, Collection<int, Member>> */
public Collection $members,
public ?string $filename = '',
public string $type = 'F',
) {
}
public function dateRange(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y')
.' - '
.Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public static function fromRequest(Request $request): self
{
return new self(
dateFrom: $request->dateFrom,
dateUntil: $request->dateUntil,
zipLocation: $request->zipLocation,
country: Country::where('id', $request->country)->firstOrFail(),
members: Member::whereIn('id', $request->members)->orderByRaw('lastname, firstname')->get()->chunk(17),
);
}
public function countryName(): string
{
return $this->country->name;
}
public function memberShort(Member $member): string
{
return $member->isLeader() ? 'L' : '';
}
public function memberName(Member $member): string
{
return $member->lastname.', '.$member->firstname;
}
public function memberAddress(Member $member): string
{
return $member->fullAddress;
}
public function memberGender(Member $member): string
{
if (!$member->gender) {
return '';
}
return strtolower(substr($member->gender->name, 0, 1));
}
public function memberAge(Member $member): string
{
return (string) $member->getAge();
}
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
{
return 'Für DV erstellen';
}
}

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

@ -0,0 +1,84 @@
<?php
namespace App\Contribution\Documents;
use App\Country;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class RemscheidDocument extends ContributionDocument
{
public function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
public ?Country $country,
/* @var Collection<int, Collection<int, Member>> */
public Collection $leaders,
/* @var Collection<int, Collection<int, Member>> */
public Collection $children,
public ?string $filename = '',
public string $type = 'F',
) {
}
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 static function fromRequest(Request $request): self
{
[$leaders, $children] = Member::whereIn('id', $request->members)->orderByRaw('lastname, firstname')->get()->partition(fn ($member) => $member->isLeader());
return new self(
dateFrom: $request->dateFrom,
dateUntil: $request->dateUntil,
zipLocation: $request->zipLocation,
country: Country::where('id', $request->country)->firstOrFail(),
leaders: $leaders->values()->chunk(6),
children: $children->values()->chunk(20),
);
}
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
{
return 'Für Remscheid erstellen';
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Contribution\Documents;
use App\Member\Member;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Zoomyboy\Tex\Engine;
use Zoomyboy\Tex\Template;
class SolingenDocument extends ContributionDocument
{
final private function __construct(
public string $dateFrom,
public string $dateUntil,
public string $zipLocation,
/** @var array<int, int> */
public array $members,
public string $eventName,
public string $type = 'F',
) {
}
public static function fromRequest(Request $request): static
{
return new static(
dateFrom: $request->dateFrom,
dateUntil: $request->dateUntil,
zipLocation: $request->zipLocation,
members: $request->members,
eventName: $request->eventName,
);
}
/**
* @return Collection<Collection<Member>>
*/
public function memberModels(): Collection
{
return Member::whereIn('id', $this->members)->orderByRaw('lastname, firstname')->get()->chunk(14);
}
public function niceEventFrom(): string
{
return Carbon::parse($this->dateFrom)->format('d.m.Y');
}
public function niceEventUntil(): string
{
return Carbon::parse($this->dateUntil)->format('d.m.Y');
}
public function template(): Template
{
return Template::make('tex.templates.contribution');
}
public function checkboxes(): string
{
$output = '';
$firstRow = collect(['B' => 'Jugendbildungsmaßnahme', 'G' => 'Gruppenleiter/innenschulung', 'FK' => 'Ferienkolonie', 'F' => 'Freizeitnaßnahme'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox').'{'.$item.'}';
})->implode(' & ').' \\\\';
$secondRow = collect(['I' => 'Int. Jugendbegegnung', 'P' => 'politische Jugendbildung', 'PR' => 'Projekte'])->map(function ($item, $key) {
return ($this->type === $key ? '\\checkedcheckbox' : '\\checkbox').'{'.$item.'}';
})->implode(' & ').' & \\emptycheckbox \\\\';
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
{
return Engine::PDFLATEX;
}
public static function getName(): string
{
return 'Für Stadt Solingen erstellen';
}
}

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

@ -2,16 +2,12 @@
namespace App\Course\Models; namespace App\Course\Models;
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;
public $timestamps = false; public $timestamps = false;
public $fillable = ['name', 'nami_id']; public $fillable = ['name', 'nami_id'];
@ -24,12 +20,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,33 +2,18 @@
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> */
public $guarded = []; public $guarded = [];
/**
* @return BelongsTo<Course, self>
*/
public function course(): BelongsTo public function course(): BelongsTo
{ {
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

@ -14,7 +14,7 @@ class CourseResource extends JsonResource
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* *
* @return array<string, mixed> * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/ */
public function toArray($request) public function toArray($request)
{ {

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
namespace App\Efz; namespace App\Efz;
use App\Dashboard\Blocks\Block; use App\Home\Blocks\Block;
use App\Member\Member; use App\Member\Member;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -17,9 +17,8 @@ class EfzPendingBlock extends Block
return $query->where('efz', '<=', now()->subYears(5)->endOfYear()) return $query->where('efz', '<=', now()->subYears(5)->endOfYear())
->orWhereNull('efz'); ->orWhereNull('efz');
}) })
->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

@ -1,10 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class MemberNotInNamiException extends Exception
{
//
}

View File

@ -2,28 +2,18 @@
namespace App; namespace App;
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;
/** @var array<int, string> */
public $fillable = ['name', 'nami_id']; public $fillable = ['name', 'nami_id'];
/** @var bool */
public $timestamps = false; public $timestamps = false;
/**
* @return HasMany<Subscription>
*/
public function subscriptions(): HasMany public function subscriptions(): HasMany
{ {
return $this->hasMany(Subscription::class); return $this->hasMany(Subscription::class);

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();
}
}

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