Add options for search layers
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
50dc714f18
commit
f1c55bedce
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 33875d36fa5bd6fab4147e95f4aa705092f42d93
|
||||
Subproject commit c5ea29af1bb1591238bb037da93739f5bd874334
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue