Compare commits

...

4 Commits

Author SHA1 Message Date
philipp lang f1c55bedce Add options for search layers
continuous-integration/drone/push Build is passing Details
2023-05-18 01:13:28 +02:00
philipp lang 50dc714f18 Add doc for installation
continuous-integration/drone/push Build is passing Details
2023-05-17 02:32:40 +02:00
Philipp Lang b120e5a039 add build.sh 2023-05-17 00:30:39 +02:00
Philipp Lang 0a64f8ef0c add doc 2023-05-17 00:30:39 +02:00
33 changed files with 739 additions and 31 deletions

View File

@ -11,7 +11,7 @@ class RedirectIfNotInitializedMiddleware
/**
* @var array<int, string>
*/
public array $dontRedirect = ['initialize.form', 'initialize.store', 'nami.login-check', 'nami.search'];
public array $dontRedirect = ['initialize.form', 'initialize.store', 'nami.login-check', 'nami.search', 'nami.get-search-layer'];
/**
* Handle an incoming request.

View File

@ -0,0 +1,49 @@
<?php
namespace App\Initialize\Actions;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
use Zoomyboy\LaravelNami\Data\SearchLayerOption;
use Zoomyboy\LaravelNami\Enum\SearchLayer;
use Zoomyboy\LaravelNami\Nami;
class NamiGetSearchLayerAction
{
use AsAction;
/**
* @param array<string, mixed> $input
*
* @return Collection<int, SearchLayerOption>
*/
public function handle(array $input): Collection
{
return Nami::login((int) $input['mglnr'], $input['password'])->searchLayerOptions(
SearchLayer::from($input['layer'] ?: 0),
$input['parent'] ?: null
);
}
/**
* @return array<string, string>
*/
public function rules(): array
{
return [
'mglnr' => 'required|numeric|min:0',
'password' => 'required|string',
'parent' => 'present',
'layer' => 'required|numeric',
];
}
public function asController(ActionRequest $request): JsonResponse
{
$response = $this->handle($request->validated());
return response()->json($response);
}
}

5
doc/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
_site
.sass-cache
.jekyll-cache
.jekyll-metadata
vendor

25
doc/404.html Normal file
View File

@ -0,0 +1,25 @@
---
permalink: /404.html
layout: default
---
<style type="text/css" media="screen">
.container {
margin: 10px auto;
max-width: 600px;
text-align: center;
}
h1 {
margin: 30px 0;
font-size: 4em;
line-height: 1;
letter-spacing: -1px;
}
</style>
<div class="container">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
<p>The requested page could not be found.</p>
</div>

35
doc/Gemfile Normal file
View File

@ -0,0 +1,35 @@
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 4.2.2"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "just-the-docs"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
gem "webrick", "~> 1.8"

84
doc/Gemfile.lock Normal file
View File

@ -0,0 +1,84 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
colorator (1.1.0)
concurrent-ruby (1.2.2)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
eventmachine (1.2.7)
ffi (1.15.5)
forwardable-extended (2.6.0)
http_parser.rb (0.8.0)
i18n (1.13.0)
concurrent-ruby (~> 1.0)
jekyll (4.2.2)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 1.0)
jekyll-sass-converter (~> 2.0)
jekyll-watch (~> 2.0)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (~> 0.4.0)
pathutil (~> 0.9)
rouge (~> 3.0)
safe_yaml (~> 1.0)
terminal-table (~> 2.0)
jekyll-feed (0.17.0)
jekyll (>= 3.7, < 5.0)
jekyll-sass-converter (2.2.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
just-the-docs (0.5.1)
jekyll (>= 3.8.5)
jekyll-seo-tag (>= 2.0)
rake (>= 12.3.1)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.4)
listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (5.0.1)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.30.0)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
unicode-display_width (1.8.0)
webrick (1.8.1)
PLATFORMS
x86_64-linux-musl
DEPENDENCIES
http_parser.rb (~> 0.6.0)
jekyll (~> 4.2.2)
jekyll-feed (~> 0.12)
just-the-docs
tzinfo (~> 1.2)
tzinfo-data
wdm (~> 0.1.1)
webrick (~> 1.8)
BUNDLED WITH
2.3.25

76
doc/_config.yml Normal file
View File

@ -0,0 +1,76 @@
# Welcome to Jekyll!
#
# This config file is meant for settings that affect your whole blog, values
# which you are expected to set up once and rarely edit after that. If you find
# yourself editing this file very often, consider using Jekyll's data files
# feature for the data you need to update frequently.
#
# For technical reasons, this file is *NOT* reloaded automatically when you use
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
#
# If you need help with YAML syntax, here are some quick references for you:
# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
# https://learnxinyminutes.com/docs/yaml/
#
# Site settings
# These are used to personalize your new site. If you look in the HTML files,
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
# You can create any custom variable you would like, and they will be accessible
# in the templates via {{ site.myvariable }}.
title: Adrema
email: philipp@zoomyboy.de
timezone: Europe/Berlin
description: >- # this means to ignore newlines until "baseurl:"
Frontend für NaMi
baseurl: '' # the subpath of your site, e.g. /blog
url: '' # the base hostname & protocol for your site, e.g. http://example.com
twitter_username:
github_username: zoomyboy
source: page
# Aux links for the upper right navigation
aux_links:
'GitHub':
- '//github.com/zoomyboy/adrema'
aux_links_new_tab: true
# Build settings
livereload: true
theme: just-the-docs
color_scheme: dark
plugins:
- jekyll-feed
callouts:
block:
color: grey-lt
warning:
color: yellow
title: Warnung
info:
color: blue
title: Info
defaults:
- scope:
path: 'assets/img'
values:
image: true
# Exclude from processing.
# The following items will not be processed, by default.
# Any item listed under the `exclude:` key here will be automatically added to
# the internal "default list".
#
# Excluded items can be processed by explicitly listing the directories or
# their entries' file path in the `include:` list.
#
# exclude:
# - .sass-cache/
# - .jekyll-cache/
# - gemfiles/
# - Gemfile
# - Gemfile.lock
# - node_modules/
# - vendor/bundle/
# - vendor/cache/
# - vendor/gems/
# - vendor/ruby/

View File

@ -0,0 +1,29 @@
---
layout: post
title: "Welcome to Jekyll!"
date: 2023-05-09 04:45:58 -0500
categories: jekyll update
---
Youll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated.
Jekyll requires blog post files to be named according to the following format:
`YEAR-MONTH-DAY-title.MARKUP`
Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works.
Jekyll also offers powerful support for code snippets:
{% highlight ruby %}
def print_hi(name)
puts "Hi, #{name}"
end
print_hi('Tom')
#=> prints 'Hi, Tom' to STDOUT.
{% endhighlight %}
Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekylls GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk].
[jekyll-docs]: https://jekyllrb.com/docs/home
[jekyll-gh]: https://github.com/jekyll/jekyll
[jekyll-talk]: https://talk.jekyllrb.com/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

1
doc/build.sh Executable file
View File

@ -0,0 +1 @@
docker run -p 35729:35729 -p 4000:4000 --rm -v "$PWD:/srv/jekyll" jekyll/minimal:latest sh -c 'chown -R jekyll /usr/gem && jekyll serve --incremental'

View File

@ -0,0 +1,4 @@
<figure style="display: flex; flex-direction: column; justify-content: center;">
<img src="/assets/img/{{ include.img }}.jpg" style="max-width: 90%; margin: 0 auto;"/>
<figcaption style="text-align: center; font-size: 0.8rem; margin-top: 0.4rem;">{{ include.caption }}</figcaption>
</figure>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,47 @@
---
layout: page
title: Grundeinstellungen
parent: Erste Schritte
nav_order: 2
---
Dieses Dokument beschreibt die ersten Grundeinstellungen nach der Installation von Adrema.
## Anmeldung in NaMi
Als erstes solltest du deine Zugangsdaten zu NaMi (deine Mitgliedsnummer und dein Passwort) eingeben, um dich einmalig anzumelden. Deine Zugangsdaten werden in Adrema gespeichert und müssen nur bei der Ersteinrichtung einmalig angegeben werden.
{: .info }
Der NaMi Benutzer sollte Schreibrechte auf der gewünschten Gruppierung haben. Grundsätzlich lässt sich Adrema auch mit NaMi-Accounts nutzen, die nur Leserechte haben. Dann kann man aber keine NaMi-Daten aktualisieren, bzw nur Adrema-interne Änderungen vornehmen (was langweilig ist :D )
{% include imgcap.html img='init-login' caption="Login in NaMi" %}
## Suchparameter definieren
Nun solltest du Parameter für die NaMi-Suche definieren, die beim täglichen Abruf angewendet werden.
Dabei ist es notwendig, zuerst die Diözesan-Gruppierungsnummer anzugeben. Wenn du diese nicht weißt, gehe in NaMi auf den Reiter "Suche" und wähle bei "1. Ebene (Diözese)" deine Gruppierung aus. Dort erscheint dann die Gruppierungsnummer (eine 6-Stellige Zahl). Diese ist hier einzugeben.
Danach kannst du mit der Bezirks-Ebene weiter verfahren (analog "2. Ebene (Bezirk)" in NaMi).
Du kannst beim Mitglieds-Status auswählen, ob du nur aktive Mitglieder, nur inaktive Mitglieder oder beides ("kein") abrufen willst.
Du bekommst im unteren Bereich eine Vorschau eingeblendet, welche Mitglieder abgerufen werden würden. Dies ist lediglich eine Vorschau eines Live-Abrufs aus NaMi - es handelt sich also nicht um einen vollständigen Adrema-Datensatz, da wichtige Infos wie z.B. Geburtsdaten in diesem Prozess noch nicht abgerufen werden.
{% include imgcap.html img='init-members' caption="Suchparameter" %}
Wenn du mit der Vorschau zufrieden bist, klicke auf "weiter".
## Standard-Gruppierungsnummer
Für einige Prozesse benötigt Adrema die Standard-Gruppierungsnummer. Dies ist i.d.R. die Gruppierungsnummer deiner lokalen Gruppierung die du verwalten willst (z.B. dein Stamm).
{% include imgcap.html img='init-default-groupid' caption="Standard-Gruppierungsnummer" %}
## 4. Initialisierung starten
Danach führt Adrema im Hintergrund selbstständig einen ersten Abgleich durch. Dies kann je nach Datenmenge einige Minuten bis Stunden dauern.
{% include imgcap.html img='init-confirm' caption="Einrichtung abschließen" %}
Du wirst danach ins Dashboard weitergeleitet. Nach und nach wird sich die Mitgliederliste dann mit den Mitgliedern füllen, solange bis alles abgerufen ist.

View File

@ -0,0 +1,11 @@
---
layout: page
title: Erste Schritte
has_children: true
nav_order: 1
---
# Erste Schritte
Hier findest du Hinweise zur Grundeinrichtung von Adrema.

View File

@ -0,0 +1,65 @@
---
layout: page
title: Installation
parent: Erste Schritte
nav_order: 1
---
# Installation
Adrema ist eine Web-Applikation, die auf einem Webserver installiert werden kann. Die Installation mit Docker wird empfohlen, da hier bereits alle notwendigen Dienste mit installiert werden.
{: .warning }
Für die Installation sind Grundkenntnisse im Umgang mit Docker und / oder Server-Umgebungen erforderlich. Wenn du hier Hilfe benötigst, [kontaktiere uns]({% link kontakt.md %}).
## Mindestanforderungen
Die Mindestanforderungen sind größtenteils die Anforderungen vom [Laravel Framework](https://laravel.com/docs/10.x/deployment#server-requirements). Diese (plus einige Extra-Anforderungen) sind hier kurz ausgeführt:
{: .block-title }
> Anforderungen
>
> PHP >= 8.1
> Ctype PHP Extension
> cURL PHP Extension
> DOM PHP Extension
> Fileinfo PHP Extension
> Filter PHP Extension
> Hash PHP Extension
> Mbstring PHP Extension
> OpenSSL PHP Extension
> PCRE PHP Extension
> PDO PHP Extension
> Session PHP Extension
> Tokenizer PHP Extension
> XML PHP Extensionnother paragraph
> Texlive mit fonts-extra (pdflatex & xelatex)
> rsync
## Installation mit Docker
```bash
git submodule update --init # Submodules updaten
cp .app.env.example .app.env # Example env erstellen:
docker-compose build # Container bauen
docker-compose run php php artisan key:generate --show # Key generieren
# Ersetze nun "YOUR_APP_KEY" in .app.env mit dem generierten Key (base64:qzX....).
# Führe nun den DB Container aus, um eine erste Version der Datenbank zu erstellen.
docker-compose up db -d
docker-compose run php php artisan migrate --seed # Migrations ausführen
docker-compose stop # Alles stoppen, dann alles neu starten
docker-compose up -d
```
Nun kannst du auf localhost:8000 die App öffnen, einen LoadBalancer wie nginx verwenden, den Port mit CLI Optionen ändern, etc.
## Standard Login
Wenn du die Seeder ausführst (``--seed``, siehe oben), wird ein Benutzer mit folgenden Zugangsdaten erstellt:
* E-Mail-Adresse: admin@example.com
* Passwort: admin

18
doc/page/index.md Normal file
View File

@ -0,0 +1,18 @@
---
layout: home
nav_order: 0
---
# Willkommen bei Adrema
Adrema ist eine Applikation, die die Verwaltung von Mitgliedern in der DPSG vereinfachen soll.
{% include imgcap.html img='member' caption="Mitglieder-Übersicht" %}
Insbesondere soll dabei möglichst auf eine direkte Interaktion mit NaMi verzichtet werden.
Sämtliche Änderungen die du in Adrema an deinen Mitgliedern vornimmst werden automatisch in NaMi übernommen und dort aktualisiert.
Darüber hinaus findet täglich um 01:00 Uhr ein Abgleich mit NaMi statt. Dies sorgt dafür, dass Änderungen, die z.B. jemand anderes (beispilsweise eine übergeordnete Ebene wie ein Bezirk oder eine Diözese) ebenfalls bei dir in Adrema erscheinen.

17
doc/page/kontakt.md Normal file
View File

@ -0,0 +1,17 @@
---
layout: page
title: Kontakt
nav_order: 5
---
# Kontakt
Wenn du Hilfe bei der Einrichtung brauchst, Rückfragen, Verbesserungsvorschläge oder etwas ähnliches hast, dann kontaktiere mich einfach.
## E-Mail
[philipp@zoomyboy.de](mailto:philipp@zoomyboy.de)
## Matrix
@philipp:zoomyboy.de

34
doc/page/version.md Normal file
View File

@ -0,0 +1,34 @@
---
layout: page
title: Versionierung
nav_order: 4
---
# Versionierung
## Ein Beispiel
> Eine Stammesvorsitzende - nennen wir sie Petra - öffnet ein Mitglied in NaMi, um eine Änderung der Adresse vorzunehmen
>
> Währenddessen ändert jemand anderes - nennen wir ihn Bob - die Telefonnummer des gleichen Mitglieds
>
> Petra speichert nun das Mitglied mit der __neuen Adresse__ ab, obwohl bei ihr im "bearbeiten-Formular" noch die __alte Telefonnummer__ steht.
>
> Resultat: Die Änderung der Adresse (Petras Änderung) wurde übernommen. Die Änderung von Bob (die Änderung der Telefonnummer) wurde aber überschrieben.
> Das Mitglied hat also nun die neue Adresse, aber noch die alte Telefonnummer.
Da auch übergeordnete Ebenen auf die Mitglieder der DPSG Zugriff haben und diese häufig auch bearbeiten können, ist dieses Szenario durchaus denkbar.
Die NaMi löst dieses Problem intern mit einer Versionsnummer, die jedes Mal um 1 erhöht wird, wenn jemand Änderungen an den Basisdaten vornimmt. So lässt sich feststellen, dass zwischenzeitlich eine Änderung durch eine\*n dritte\*n erfolgt ist.
## Versionen in Adrema
Die Adrema macht sich dieses System zunutze. Vor einem Update wird geprüft, ob zwischenzeitlich ein Update in NaMi vorgenommen wurde. Ist das der Fall, wird ein Hinweis angezeigt:
Du hast hier nun zwei Optionen:
> 1. Du aktualisierst das Mitglied in Adrema. Dadurch werden deine Änderungen rückgängig gemacht und das Mitglied erneut aus NaMi abgerufen. Danach kannst du deine Änderung erneut vornehmen.
>
> 2. Du aktualisierst das Mitglied. Dabei spielt der aktuelle NaMi-Stand keine Rolle. __Dabei kann es allerdings zu Datenverlust kommen__ (wie oben beschrieben).
Auf diese Art und Weise ist sichergestellt, dass Änderungen sich nicht gegenseitig überschreiben.

@ -1 +1 @@
Subproject commit 33875d36fa5bd6fab4147e95f4aa705092f42d93
Subproject commit c5ea29af1bb1591238bb037da93739f5bd874334

8
resources/js/app.js vendored
View File

@ -42,6 +42,14 @@ Vue.component('toolbar-button', ToolbarButton);
Vue.component('page-layout', PageLayout);
Vue.component('save-button', () => import(/* webpackChunkName: "form" */ './components/SaveButton'));
// ------------------------------ Full components ------------------------------
Vue.component('full-page-heading', () => import(/* webpackChunkName: "full" */ './components/Full/PageHeading.vue'));
// ------------------------------- UI Components -------------------------------
Vue.component('ui-button', () => import(/* webpackChunkName: "ui" */ './components/Ui/Button.vue'));
Vue.component('ui-spinner', () => import(/* webpackChunkName: "ui" */ './components/Ui/Spinner.vue'));
// ----------------------------------- init ------------------------------------
const el = document.getElementById('app');
const pinia = createPinia();

View File

@ -0,0 +1,7 @@
<template>
<h1 class="text-xl border-b-2 pb-1 mb-4 text-primary-100 text-center border-primary-800"><slot></slot></h1>
</template>
<script>
export default {};
</script>

View File

@ -0,0 +1,24 @@
<template>
<button class="btn btn-primary relative group">
<div :class="{hidden: !isLoading, flex: isLoading}" class="absolute items-center top-0 h-full left-0 ml-2">
<ui-spinner class="border-primary-400 w-6 h-6 group-hover:border-primary-200"></ui-spinner>
</div>
Weiter
</button>
</template>
<script>
import {menuStore} from '../../stores/menuStore.js';
export default {
data: function () {
return {};
},
props: {
isLoading: {
type: Boolean,
default: false,
},
},
};
</script>

View File

@ -0,0 +1,57 @@
<template>
<div :class="`spin-${type}`">
<div v-if="type === 'ring'"></div>
<div v-if="type === 'ring'"></div>
<div v-if="type === 'ring'"></div>
<div v-if="type === 'ring'"></div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: () => 'ring',
},
},
};
</script>
<style>
.spin-ring {
display: inline-block;
position: relative;
}
.spin-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 100%;
height: 100%;
border: 3px solid #fff;
border-radius: 50%;
animation: ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-top-color: inherit;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
.spin-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.spin-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.spin-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div id="app" class="bg-gray-900 font-sans flex flex-col grow items-center justify-center">
<div id="app" class="bg-gray-900 font-sans flex flex-col grow items-center justify-center p-6">
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<div class="bg-gray-800 rounded-xl overflow-hidden shadow-lg p-6">
<slot></slot>

View File

@ -1,63 +1,57 @@
<template>
<div>
<div v-if="step === 0">
<full-page-heading>Willkommen im Adrema-Setup.<br /></full-page-heading>
<div class="prose prose-invert">
<p>Willkommen im Adrema-Setup.<br /></p>
<p>
Bitte gib deine NaMi-Zugangsdaten ein,<br />
um eine erste Synchronisation durchzuführen.
</p>
<p>Bitte gib deine NaMi-Zugangsdaten ein,<br />um eine erste Synchronisation durchzuführen.</p>
</div>
<form @submit.prevent="check" class="grid gap-3 mt-5">
<f-text v-model="values.mglnr" label="Mitgliedsnummer" name="mglnr" id="mglnr" type="tel" required></f-text>
<f-text v-model="values.password" type="password" label="Passwort" name="password" id="password" required></f-text>
<button type="submit" class="btn w-full btn-primary mt-6 inline-block">Weiter</button>
<ui-button class="mt-6" :is-loading="loading" type="submit">Weiter</ui-button>
</form>
</div>
<div v-if="step === 1" class="flex flex-col" style="width: 90vw; height: 90vh">
<form @submit.prevent="storeSearch" class="border-2 border-primary-700 border-solid p-3 rounded-lg grid grid-cols-3 gap-3 flex-none">
<div v-if="step === 1" class="grid grid-cols-5 w-full gap-3">
<full-page-heading class="col-span-full !mb-0">Suchkriterien festlegen</full-page-heading>
<form @submit.prevent="storeSearch" class="border-2 border-primary-800 border-solid p-3 rounded-lg grid gap-3 col-span-2">
<div class="prose prose-invert max-w-none col-span-full">
<p>
Lege hier die Suchkriterien für den Abruf der Mitglieder-Daten fest. Mit diesen Suchkriterien wird im Anschluss eine Mitgliedersuche in NaMi durchgeführt. Alle Mitglieder, die
dann dort auftauchen werden in die Adrema übernommen. Dir wird hier eine Vorschau eingeblendet, damit du sicherstellen kannst, dass die Suchkriterien die richtigen sind.
</p>
<p>
Außerdem werden diese Suchkriterien bei jedem neuen Abgleich (der automatisch täglich erfolgt) angewendet. Du kannst die Suchkriterien in den globalen Einstellungen jederzeit
ändern.
</p>
</div>
<f-text
<f-select
v-model="values.params.gruppierung1Id"
label="Diözesan-Gruppierung"
name="gruppierung1Id"
id="gruppierung1Id"
type="tel"
size="sm"
@input="search"
:options="searchLayerOptions[0]"
@input="loadSearchLayer(1, $event, search)"
hint="Gruppierungs-Nummer einer Diözese, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deiner Diözese. Entspricht dem Feld '1. Ebene' in der NaMi Suche."
></f-text>
<f-text
></f-select>
<f-select
v-model="values.params.gruppierung2Id"
label="Bezirks-Gruppierung"
name="gruppierung2Id"
id="gruppierung2Id"
type="tel"
hint="Gruppierungs-Nummer eines Bezirks, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Bezirks. Entspricht dem Feld '2. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Bezirk zu begrenzen."
:disabled="!values.params.gruppierung1Id"
@input="search"
@input="loadSearchLayer(2, $event, search)"
size="sm"
></f-text>
<f-text
:options="searchLayerOptions[1]"
></f-select>
<f-select
v-model="values.params.gruppierung3Id"
label="Stammes-Gruppierung"
name="gruppierung3Id"
id="gruppierung3Id"
type="tel"
size="sm"
@input="search"
hint="Gruppierungs-Nummer deines Stammes, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Stammes. Entspricht dem Feld '3. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Stamm zu beschränken."
:disabled="!values.params.gruppierung1Id || !values.params.gruppierung2Id"
></f-text>
:options="searchLayerOptions[2]"
></f-select>
<f-select
v-model="values.params.mglStatusId"
label="Mitglieds-Status"
@ -86,12 +80,12 @@
hint="Mitglieder finden, die direktes Mitglied in einer Untergruppe der kleinsten befüllten Gruppierung sind."
size="sm"
></f-switch>
<div class="col-span-full">
<button type="submit" class="btn btn-primary btn-sm">Weiter</button>
<div class="col-span-full flex justify-center">
<ui-button :is-loading="loading" class="px-10" type="submit">Weiter</ui-button>
</div>
</form>
<section class="grow border-2 border-primary-700 border-solid p-3 rounded-lg mt-4" v-if="preview !== null && preview.data.length">
<section class="col-span-3 text-sm col-span-3" v-if="preview !== null && preview.data.length">
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm hidden md:table">
<thead>
<th>GruppierungsNr</th>
@ -114,9 +108,10 @@
<v-pages class="mt-4" :value="preview" @reload="reloadPage"></v-pages>
</div>
</section>
<section class="grow items-center justify-center flex text-xl text-gray-200 border-2 border-primary-700 border-solid p-3 rounded-lg mt-4" v-else>Keine Mitglieder gefunden</section>
<section class="col-span-3 items-center justify-center flex text-xl text-gray-200 border-2 border-primary-800 border-solid p-3 rounded-lg mt-4" v-else>Keine Mitglieder gefunden</section>
</div>
<div v-if="step === 2">
<full-page-heading>Standard-Gruppierung</full-page-heading>
<div class="prose prose-invert">
<p>Bitte gib hier deine Standard-Gruppierungsnummer ein.</p>
<p>Dieser Gruppierung werden Mitglieder automatisch zugeordnet,<br />falls nichts anderes angegeben wurde.</p>
@ -128,6 +123,7 @@
</form>
</div>
<div v-if="step === 3">
<full-page-heading>Einrichtung abgeschlossen</full-page-heading>
<div class="prose prose-invert">
<p>Wir werden nun die Mitgliederdaten anhand deiner festgelegten Kriterien abrufen.</p>
<p>Per Klick auf "Abschließen" gelangst du zum Dashboard</p>
@ -150,6 +146,8 @@ export default {
data: function () {
return {
searchLayerOptions: [[], [], []],
loading: false,
preview: null,
states: [
{id: 'INAKTIV', name: 'Inaktiv'},
@ -188,23 +186,57 @@ export default {
await this.loadSearchResult(page);
},
async check() {
this.loading = true;
try {
await this.axios.post('/nami/login-check', this.values);
this.step = 1;
await this.loadSearchResult(1);
await this.loadSearchLayer(0, null, () => '');
this.step = 1;
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}
},
search: debounce(async function () {
await this.loadSearchResult(1);
}, 500),
async loadSearchLayer(parentLayer, parent, after) {
this.loading = true;
try {
var result = await this.axios.post('/nami/get-search-layer', {...this.values, layer: parentLayer, parent});
this.searchLayerOptions = this.searchLayerOptions.map((layers, index) => {
if (index < parentLayer) {
return layers;
}
var groupIndex = index + 1;
this.values.params[`gruppierung${groupIndex}Id`] = null;
if (index === parentLayer) {
return result.data;
}
return [];
});
after();
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}
},
async loadSearchResult(page) {
this.loading = true;
try {
var result = await this.axios.post('/nami/search', {...this.values, page: page});
this.preview = result.data;
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}
},
},

View File

@ -17,6 +17,7 @@ use App\Dashboard\Actions\IndexAction as DashboardIndexAction;
use App\Efz\ShowEfzDocumentAction;
use App\Initialize\Actions\InitializeAction;
use App\Initialize\Actions\InitializeFormAction;
use App\Initialize\Actions\NamiGetSearchLayerAction;
use App\Initialize\Actions\NamiLoginCheckAction;
use App\Initialize\Actions\NamiSearchAction;
use App\Member\Actions\ExportAction;
@ -41,6 +42,7 @@ Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
Route::group(['middleware' => 'auth:web'], function (): void {
Route::get('/', DashboardIndexAction::class)->name('home');
Route::post('/nami/login-check', NamiLoginCheckAction::class)->name('nami.login-check');
Route::post('/nami/get-search-layer', NamiGetSearchLayerAction::class)->name('nami.get-search-layer');
Route::post('/nami/search', NamiSearchAction::class)->name('nami.search');
Route::post('/api/member/search', SearchAction::class)->name('member.search');
Route::get('/initialize', InitializeFormAction::class)->name('initialize.form');

View File

@ -0,0 +1,78 @@
<?php
namespace Tests\Feature\Initializer;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Zoomyboy\LaravelNami\Authentication\Auth;
use Zoomyboy\LaravelNami\Fakes\SearchLayerFake;
class GetSearchLayerActionTest extends TestCase
{
use DatabaseTransactions;
public function setUp(): void
{
parent::setUp();
$this->login();
Auth::success(333, 'secret');
}
public function testItFindsRoots(): void
{
$this->withoutExceptionHandling();
app(SearchLayerFake::class)->fetches('1', [
['descriptor' => 'aa', 'id' => 5],
]);
$response = $this->postJson('/nami/get-search-layer', [
'layer' => 0,
'parent' => null,
'mglnr' => 333,
'password' => 'secret',
]);
$response->assertStatus(200);
$response->assertJsonPath('0.name', 'aa');
$response->assertJsonPath('0.id', 5);
}
public function testItFindsFirstLayer(): void
{
$this->withoutExceptionHandling();
app(SearchLayerFake::class)->fetches('2/gruppierung1/20', [
['descriptor' => 'aa', 'id' => 5],
]);
$response = $this->postJson('/nami/get-search-layer', [
'layer' => 1,
'parent' => 20,
'mglnr' => 333,
'password' => 'secret',
]);
$response->assertStatus(200);
$response->assertJsonPath('0.name', 'aa');
$response->assertJsonPath('0.id', 5);
}
public function testItFindsSecondLayer(): void
{
$this->withoutExceptionHandling();
app(SearchLayerFake::class)->fetches('3/gruppierung2/30', [
['descriptor' => 'aa', 'id' => 5],
]);
$response = $this->postJson('/nami/get-search-layer', [
'layer' => 2,
'parent' => 30,
'mglnr' => 333,
'password' => 'secret',
]);
$response->assertStatus(200);
$response->assertJsonPath('0.name', 'aa');
$response->assertJsonPath('0.id', 5);
}
}