Compare commits
No commits in common. "master" and "form-contribution" have entirely different histories.
master
...
form-contr
|
|
@ -1,4 +1,4 @@
|
|||
FROM php:8.3.36-fpm AS php
|
||||
FROM php:8.3.11-fpm AS php
|
||||
WORKDIR /app
|
||||
RUN ls /app
|
||||
RUN apt-get update
|
||||
|
|
|
|||
|
|
@ -31,15 +31,10 @@ steps:
|
|||
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_submodules
|
||||
image: node:20.15.0-slim
|
||||
commands:
|
||||
- cd packages/adrema-form && npm ci && npm run build-import && rm -R node_modules && cd ../../
|
||||
|
||||
- name: node
|
||||
image: node:20.15.0-slim
|
||||
commands:
|
||||
- npm ci && npm run img && npm run prod && rm -R node_modules
|
||||
- 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
|
||||
|
|
@ -146,7 +141,7 @@ steps:
|
|||
- composer install --ignore-platform-reqs --no-dev
|
||||
|
||||
- name: compress
|
||||
image: php:8.3.36
|
||||
image: php:8.1.6
|
||||
commands:
|
||||
- apt-get update -yqq
|
||||
- apt-get install -yqq zip tar
|
||||
|
|
|
|||
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -1,47 +1,5 @@
|
|||
# Letzte Änderungen
|
||||
|
||||
### 1.12.29
|
||||
|
||||
- Veranstaltungen können nun "bereinigt", sprich alle TN gelöscht werden
|
||||
|
||||
### 1.12.27
|
||||
|
||||
- Bei Rechnungs-und Erinnerungsmails wird nun der Stammesname angezeigt. Zudm kann eine ReplyTo gesetzt werden, wenn gewünscht
|
||||
|
||||
### 1.12.26
|
||||
|
||||
- Felder im Form-Builder können nun verschoben und kopiert werden
|
||||
|
||||
### 1.12.25
|
||||
|
||||
- Ein Bug wurde behoben, sodass nun wieder Bedingungen für Formular-Mails vergeben werden können
|
||||
|
||||
### 1.12.24
|
||||
|
||||
- Gruppen werden nun wöchentlich aus NaMi neu abgerufen
|
||||
|
||||
### 1.12.23
|
||||
|
||||
- Veranstaltungs-Teilnehmer*innen können nun abgemeldet statt vollständig gelöscht werden.
|
||||
- Beim Excel-Export wird eine Spalte "ID" angezeigt mit der ID des TNs
|
||||
|
||||
### 1.12.22
|
||||
|
||||
- Bei Mitgliedern wird nun auch die geschäftliche tel-nr aktualisiert
|
||||
|
||||
### 1.12.21
|
||||
|
||||
- Reply-To-Header bei Versand von Prävention-und-Veranstaltungs-Mails kann nun eingestellt werden
|
||||
|
||||
### 1.12.20
|
||||
|
||||
- Nachnelde-Link kann nun erstellt werden für Veranstaltungen
|
||||
|
||||
### 1.12.19
|
||||
|
||||
- Zuschusslisten können nun aus Veranstaltungs-Daten erstellt werden
|
||||
- Veranstaltungs-Übersicht zeigt nun Tags an
|
||||
|
||||
### 1.12.18
|
||||
|
||||
- Fix: Initialisierung klappt nun auch, wenn Mitgliedsnummer mit einer 0 beginnt
|
||||
|
|
|
|||
57
CLAUDE.md
57
CLAUDE.md
|
|
@ -1,57 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project
|
||||
|
||||
AdReMa ("AddressManagement") is a Laravel 11 + Inertia/Vue 3 app for German Scout groups (DPSG) to manage member data, courses, invoicing/contributions, and syncs with the external NaMi member database. Backend is PHP 8.3, frontend is Vue 3 + TypeScript rendered via Inertia (no separate SPA API).
|
||||
|
||||
## Commands
|
||||
|
||||
### PHP / backend
|
||||
|
||||
- Run all tests: `php artisan test` (or `vendor/bin/pest`)
|
||||
- Run a single test file: `php artisan test tests/Feature/Member/SomeTest.php`
|
||||
- Run a single test by name: `php artisan test --filter=test_name`
|
||||
- Static analysis: `vendor/bin/phpstan analyse` (larastan, level 6, config in `phpstan.neon`)
|
||||
- Test suites are split in `phpunit.xml`: `Unit`, `Feature`, `Fileshare`, `Arch` (architecture rules in `tests/Arch.php`), `NamiUnit` (packages/laravel-nami), `EndToEnd`
|
||||
|
||||
### JS / frontend
|
||||
|
||||
- Dev server: `npm run dev` (alias for `vite`)
|
||||
- Production build: `npm run prod`
|
||||
- Lint: `npm run lint`, autofix: `npm run fix`
|
||||
- Rebuild SVG sprite: `npm run img`
|
||||
- The `packages/adrema-form` package (public event-registration form, embeddable) has its own `npm run build` / `build-import` (import mode is used when embedded in the main app) and is built separately.
|
||||
|
||||
### Environment
|
||||
|
||||
- Local dev runs via Docker Compose (`docker-compose.yml`); `.app.env` holds environment config, copied from `.app.env.example`.
|
||||
- Submodules must be initialized: `git submodule update --init`.
|
||||
- CI (`.drone.yml`) runs: composer install → npm build (main app + `adrema-form`) → `php artisan migrate` → `php artisan test` → `vendor/bin/phpstan analyse`.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend structure
|
||||
|
||||
Code under `app/` is organized by **domain module**, not by technical layer (no global `app/Http/Controllers` for most features). Each domain (e.g. `Member`, `Activity`, `Form`, `Invoice`, `Course`, `Fileshare`, `Contribution`, `Mailman`, `Mailgateway`, `Efz`, `Prevention`) typically contains its own `Actions/`, models, requests, and resources together. Older/shared code still lives in `app/Actions`.
|
||||
|
||||
- **Actions pattern**: business logic lives in single-purpose Action classes (`lorisleiva/laravel-actions`, `use AsAction`) with a `handle()` method, invoked directly from routes in `routes/web.php` rather than through traditional controllers.
|
||||
- **NaMi integration**: `packages/laravel-nami` is a local Composer package (symlinked via `repositories` path in `composer.json`) wrapping the external NaMi API (members, courses, memberships, confessions, fees, regions, etc.). App-level `Pull*Action`/`Insert*Action` classes (`app/Actions/PullMemberAction.php`, `InsertMemberAction.php`, etc.) sync NaMi data into local models. - Please never edit files here, unless explicitly specified.
|
||||
- **Local packages** (all under `packages/`, wired via Composer path repositories): `laravel-nami` (NaMi API client), `table-document` (PDF/table document generation), `flysystem-webdav` (WebDAV filesystem driver, used for cloud file storage/CardDAV-adjacent features), `tex` (LaTeX rendering, used for invoices/Bescheinigungen), `medialibrary-helper` (Vue components + backend helpers for file uploads, e.g. `FSinglefile`/`FMultiplefiles`), `adrema-form` (standalone Vite/Vue app for public event registration forms, built separately and embedded/imported into the main app). - Please never edit files here, unless explicitly specified
|
||||
- **Modules feature flag**: `App\Module\Module` enum (`bill`, `course`, `event`) + `ModuleSettings` gate optional features per-installation; checked server-side via `hasModule()` and client-side via the `hasModule` Vue mixin (`resources/js/mixins/hasModule.js`).
|
||||
- Data transfer uses `spatie/laravel-data` (`Data` classes) in several domains instead of plain arrays/DTOs.
|
||||
- PHPStan type aliases for complex array shapes (e.g. contribution API payloads) are defined centrally in `phpstan.neon`.
|
||||
|
||||
### Frontend structure
|
||||
|
||||
- Entry point `resources/js/app.js` bootstraps Inertia + Pinia + Vue; pages are Vue SFCs under `resources/js/views/**`, resolved by path via Inertia's `resolve`.
|
||||
- `resources/js/layouts/AppLayout.vue` is the default page layout, applied automatically unless a page sets its own `layout`.
|
||||
- Global mixins (`hasModule`, `hasFlash`) are applied app-wide instead of per-component imports.
|
||||
- State is managed with Pinia stores (`resources/js/stores`).
|
||||
|
||||
### Testing conventions
|
||||
|
||||
- Tests use Pest (`pestphp/pest`), with `tests/TestCase.php` / `tests/EndToEndTestCase.php` / `tests/FileshareTestCase.php` as base cases for different suites.
|
||||
- `tests/RequestFactories` (worksome/request-factories) and `tests/Datasets` provide reusable test data/request builders.
|
||||
- `tests/Arch.php` enforces architectural constraints via Pest's arch testing.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@servers(['docker' => ['stamm-silva@zoomyboy.de', 'stammgallier@stamm-gallier.de', 'dpsg-lennep@zoomyboy.de', 'dpsgbergischland@zoomyboy.de', 'dpsg@adrema.dpsg-koeln.de']])
|
||||
@servers(['docker' => ['stamm-silva@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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace App\Console;
|
|||
|
||||
use App\Actions\DbMaintainAction;
|
||||
use App\Form\Actions\PreventionRememberAction;
|
||||
use App\Initialize\Actions\InitializeUpdateAction;
|
||||
use App\Initialize\InitializeMembers;
|
||||
use App\Invoice\Actions\InvoiceSendAction;
|
||||
use App\Prevention\Actions\YearlyRememberAction;
|
||||
|
|
@ -24,7 +23,6 @@ class Kernel extends ConsoleKernel
|
|||
DbMaintainAction::class,
|
||||
PreventionRememberAction::class,
|
||||
YearlyRememberAction::class,
|
||||
InitializeUpdateAction::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -39,7 +37,6 @@ class Kernel extends ConsoleKernel
|
|||
$schedule->command(PreventionRememberAction::class)->dailyAt('11:00');
|
||||
$schedule->command(InvoiceSendAction::class)->dailyAt('10:00');
|
||||
$schedule->command(YearlyRememberAction::class)->dailyAt('09:00');
|
||||
$schedule->command(InitializeUpdateAction::class)->weekly();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ abstract class ConnectionType extends Data
|
|||
{
|
||||
$filesystem = $this->getFilesystem();
|
||||
|
||||
return collect($filesystem->directories($parent ?: '/'))->sort()->values()->toArray();
|
||||
return $filesystem->directories($parent ?: '/');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,27 +31,25 @@ class CreateExcelDocumentAction
|
|||
private function allSheet(Collection $participants): TableDocumentData
|
||||
{
|
||||
$document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]);
|
||||
$headers = $this->form->getFields()->names()->push('Abgemeldet am')->prepend('ID')->toArray();
|
||||
[$activeParticipants, $cancelledParticipants] = $participants->partition(fn ($participant) => $participant->cancelled_at === null);
|
||||
$headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray();
|
||||
|
||||
$document->addSheet(SheetData::from([
|
||||
'header' => $headers,
|
||||
'data' => $this->rowsFor($activeParticipants),
|
||||
'data' => $participants
|
||||
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
|
||||
->toArray(),
|
||||
'name' => 'Alle',
|
||||
]));
|
||||
$document->addSheet(SheetData::from([
|
||||
'header' => $headers,
|
||||
'data' => $this->rowsFor($cancelledParticipants),
|
||||
'name' => 'Abgemeldet',
|
||||
]));
|
||||
|
||||
if ($this->form->export->groupBy) {
|
||||
$groups = $activeParticipants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
|
||||
$groups = $participants->groupBy(fn ($participant) => $participant->getFields()->findByKey($this->form->export->groupBy)->presentRaw());
|
||||
|
||||
foreach ($groups as $name => $groupedParticipants) {
|
||||
foreach ($groups as $name => $participants) {
|
||||
$document->addSheet(SheetData::from([
|
||||
'header' => $headers,
|
||||
'data' => $this->rowsFor($groupedParticipants),
|
||||
'data' => $participants
|
||||
->map(fn ($participant) => $this->form->getFields()->map(fn ($field) => $participant->getFields()->find($field)->presentRaw())->toArray())
|
||||
->toArray(),
|
||||
'name' => $name,
|
||||
]));
|
||||
}
|
||||
|
|
@ -66,17 +64,6 @@ class CreateExcelDocumentAction
|
|||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Participant> $participants
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function rowsFor(Collection $participants): array {
|
||||
return $participants->map(fn ($participant) => $participant->getFields()->presentValues()
|
||||
->put('Abgemeldet am', $participant->cancelled_at?->format('d.m.Y H:i:s') ?: '')
|
||||
->prepend((string) $participant->id, 'ID')
|
||||
)->toArray();
|
||||
}
|
||||
|
||||
private function tempPath(): string
|
||||
{
|
||||
return sys_get_temp_dir() . '/' . str()->uuid()->toString();
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Lib\Events\Succeeded;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormCleanupAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Form $form): void
|
||||
{
|
||||
$form->participants()->get()->each->delete();
|
||||
$form->save();
|
||||
ClearFrontendCacheAction::run();
|
||||
}
|
||||
|
||||
public function asController(Form $form): JsonResponse
|
||||
{
|
||||
$this->handle($form);
|
||||
|
||||
Succeeded::message('Teilnehmende gelöscht.')->dispatch();
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Actions;
|
||||
|
||||
use App\Form\FormSettings;
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class FormGenerateLaterlinkAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function asController(Form $form): JsonResponse
|
||||
{
|
||||
$registerUrl = str(app(FormSettings::class)->registerUrl)->replace('{slug}', $form->slug)->toString();
|
||||
$laterId = str()->uuid()->toString();
|
||||
$laterUrl = URL::signedRoute('form.register', ['form' => $form, 'later' => '1', 'id' => $laterId]);
|
||||
$urlParts = parse_url($laterUrl);
|
||||
|
||||
Cache::remember('later_'.$laterId, 2592000, fn () => $form->id); // Link ist 40 Tage gültig
|
||||
|
||||
return response()->json([
|
||||
'url' => $registerUrl.'?'.data_get($urlParts, 'query')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ class FormIndexAction
|
|||
*/
|
||||
public function handle(string $filter): LengthAwarePaginator
|
||||
{
|
||||
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount(['participants' => fn ($q) => $q->whereNull('cancelled_at')]))->paginate(15);
|
||||
return FormFilterScope::fromRequest($filter)->getQuery()->query(fn ($query) => $query->withCount('participants'))->paginate(15);
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request): Response
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Form\Models\Participant;
|
|||
use App\Lib\JobMiddleware\JobChannels;
|
||||
use App\Lib\JobMiddleware\WithJobState;
|
||||
use App\Lib\Queue\TracksJob;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class ParticipantDestroyAction
|
||||
|
|
@ -14,20 +13,14 @@ class ParticipantDestroyAction
|
|||
use AsAction;
|
||||
use TracksJob;
|
||||
|
||||
public function handle(int $participantId, bool $force): void
|
||||
public function handle(int $participantId): void
|
||||
{
|
||||
$participant = Participant::findOrFail($participantId);
|
||||
|
||||
if ($force) {
|
||||
$participant->delete();
|
||||
} else {
|
||||
$participant->update(['cancelled_at' => now()]);
|
||||
}
|
||||
Participant::findOrFail($participantId)->delete();
|
||||
}
|
||||
|
||||
public function asController(ActionRequest $request, Participant $participant): void
|
||||
public function asController(Participant $participant): void
|
||||
{
|
||||
$this->startJob($participant->id, $request->header('X-Force') === '1');
|
||||
$this->startJob($participant->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ use App\Form\Models\Form;
|
|||
use App\Form\Models\Participant;
|
||||
use App\Member\Member;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
|
@ -23,9 +20,13 @@ class RegisterAction
|
|||
*/
|
||||
public function handle(Form $form, array $input): Participant
|
||||
{
|
||||
if (!$form->canRegister()) {
|
||||
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
|
||||
}
|
||||
|
||||
$memberQuery = FieldCollection::fromRequest($form, $input)
|
||||
->withNamiType()
|
||||
->reduce(fn($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
|
||||
->reduce(fn ($query, $field) => $field->namiType->performQuery($query, $field->value), (new Member())->newQuery());
|
||||
$member = $form->getFields()->withNamiType()->count() && $memberQuery->count() === 1 ? $memberQuery->first() : null;
|
||||
|
||||
$participant = $form->participants()->create([
|
||||
|
|
@ -33,7 +34,7 @@ class RegisterAction
|
|||
'member_id' => $member?->id,
|
||||
]);
|
||||
|
||||
$form->getFields()->each(fn($field) => $field->afterRegistration($form, $participant, $input));
|
||||
$form->getFields()->each(fn ($field) => $field->afterRegistration($form, $participant, $input));
|
||||
|
||||
$participant->sendConfirmationMail();
|
||||
ExportSyncAction::dispatch($form->id);
|
||||
|
|
@ -76,30 +77,8 @@ class RegisterAction
|
|||
|
||||
public function asController(ActionRequest $request, Form $form): JsonResponse
|
||||
{
|
||||
if (!$form->canRegister() && !$this->isRegisteringLater($request, $form)) {
|
||||
throw ValidationException::withMessages(['event' => 'Anmeldung zzt nicht möglich.']);
|
||||
}
|
||||
|
||||
$participant = $this->handle($form, $request->validated());
|
||||
|
||||
if ($this->isRegisteringLater($request, $form)) {
|
||||
Cache::forget('later_'.request('id'));
|
||||
}
|
||||
|
||||
return response()->json($participant);
|
||||
}
|
||||
|
||||
public function isRegisteringLater(ActionRequest $request, Form $form): bool {
|
||||
$validator = Validator::make($request->query(), [
|
||||
'later' => 'required|numeric|in:1',
|
||||
'id' => 'required|string|uuid:4',
|
||||
'signature' => 'required|string',
|
||||
]);
|
||||
|
||||
if (!URL::hasValidSignature($request) || $validator->fails()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Cache::get('later_'.data_get($validator->validated(), 'id')) === $form->id;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ class UpdateParticipantSearchIndexAction
|
|||
$form->searchableUsing()->updateIndexSettings(
|
||||
$form->participantsSearchableAs(),
|
||||
[
|
||||
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id', 'cancelled_at'],
|
||||
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'],
|
||||
'searchableAttributes' => $form->getFields()->searchables()->getKeys(),
|
||||
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id', 'created_at'],
|
||||
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id', 'cancelled_at'],
|
||||
'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id'],
|
||||
'pagination' => [
|
||||
'maxTotalHits' => 1000000,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -99,19 +99,19 @@ class FieldCollection extends Collection
|
|||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, string>
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function names(): Collection
|
||||
public function names(): array
|
||||
{
|
||||
return $this->map(fn ($field) => $field->name);
|
||||
return $this->map(fn ($field) => $field->name)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<string, string>
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function presentValues(): Collection
|
||||
public function presentValues(): array
|
||||
{
|
||||
return $this->mapWithKeys(fn ($field) => [$field->name => $field->presentRaw()]);
|
||||
return $this->map(fn ($field) => $field->presentRaw())->toArray();
|
||||
}
|
||||
|
||||
public function hasSpecialType(SpecialType $specialType): bool
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Actions\SettingStoreAction;
|
||||
use App\Setting\Contracts\Storeable;
|
||||
use App\Setting\LocalSettings;
|
||||
use Lorisleiva\Actions\ActionRequest;
|
||||
|
|
@ -10,7 +11,6 @@ class FormSettings extends LocalSettings implements Storeable
|
|||
{
|
||||
public string $registerUrl;
|
||||
public string $clearCacheUrl;
|
||||
public ?string $replyToMail;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
|
|
@ -19,7 +19,7 @@ class FormSettings extends LocalSettings implements Storeable
|
|||
|
||||
public static function title(): string
|
||||
{
|
||||
return 'Veranstaltungen';
|
||||
return 'Formulare';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -30,7 +30,6 @@ class FormSettings extends LocalSettings implements Storeable
|
|||
return [
|
||||
'registerUrl' => 'present|string',
|
||||
'clearCacheUrl' => 'present|string',
|
||||
'replyToMail' => 'nullable|string|email',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +47,6 @@ class FormSettings extends LocalSettings implements Storeable
|
|||
'data' => [
|
||||
'registerUrl' => $this->registerUrl,
|
||||
'clearCacheUrl' => $this->clearCacheUrl,
|
||||
'replyToMail' => $this->replyToMail,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ namespace App\Form\Mails;
|
|||
|
||||
use App\Form\Data\FormConfigData;
|
||||
use App\Form\Editor\FormConditionResolver;
|
||||
use App\Form\FormSettings;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Lib\Editor\Condition;
|
||||
use Illuminate\Bus\Queueable;
|
||||
|
|
@ -25,8 +24,6 @@ class ConfirmRegistrationMail extends Mailable
|
|||
/** @var array<int, mixed> */
|
||||
public array $bottomText;
|
||||
|
||||
public FormSettings $formSettings;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
|
|
@ -35,7 +32,6 @@ class ConfirmRegistrationMail extends Mailable
|
|||
public function __construct(public Participant $participant)
|
||||
{
|
||||
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
|
||||
$this->formSettings = app(FormSettings::class);
|
||||
$this->fullname = $participant->getFields()->getFullname();
|
||||
$this->config = $participant->getConfig();
|
||||
$this->topText = $conditionResolver->makeBlocks($participant->form->mail_top);
|
||||
|
|
@ -49,15 +45,9 @@ class ConfirmRegistrationMail extends Mailable
|
|||
*/
|
||||
public function envelope()
|
||||
{
|
||||
$envelope = new Envelope(
|
||||
return new Envelope(
|
||||
subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
|
||||
);
|
||||
|
||||
if ($this->formSettings->replyToMail !== null) {
|
||||
$envelope->replyTo($this->formSettings->replyToMail);
|
||||
}
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class Form extends Model implements HasMedia
|
|||
return Sorting::from($this->meta['sorting']);
|
||||
}
|
||||
|
||||
public function isInDates(): bool
|
||||
public function canRegister(): bool
|
||||
{
|
||||
if ($this->registration_from && $this->registration_from->gt(now())) {
|
||||
return false;
|
||||
|
|
@ -202,9 +202,4 @@ class Form extends Model implements HasMedia
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canRegister(): bool
|
||||
{
|
||||
return $this->is_active && $this->isInDates();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ class Participant extends Model implements Preventable
|
|||
public $casts = [
|
||||
'data' => 'json',
|
||||
'last_remembered_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -109,12 +108,7 @@ class Participant extends Model implements Preventable
|
|||
/** @return array<string, mixed> */
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
...$this->data,
|
||||
'parent-id' => $this->parent_id,
|
||||
'created_at' => $this->created_at->timestamp,
|
||||
'cancelled_at' => $this->cancelled_at
|
||||
];
|
||||
return [...$this->data, 'parent-id' => $this->parent_id, 'created_at' => $this->created_at->timestamp];
|
||||
}
|
||||
|
||||
public function matchesCondition(Condition $condition): bool {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class FormResource extends JsonResource
|
|||
'mail_bottom' => $this->mail_bottom,
|
||||
'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'),
|
||||
'registration_until' => $this->registration_until?->format('Y-m-d H:i:s'),
|
||||
'is_in_dates' => $this->isInDates(),
|
||||
'config' => $this->config,
|
||||
'participants_count' => $this->participants_count,
|
||||
'is_active' => $this->is_active,
|
||||
|
|
@ -65,13 +64,11 @@ class FormResource extends JsonResource
|
|||
'participant_root_index' => route('form.participant.index', ['form' => $this->getModel(), 'parent' => -1]),
|
||||
'update' => route('form.update', $this->getModel()),
|
||||
'destroy' => route('form.destroy', $this->getModel()),
|
||||
'cleanup' => route('form.cleanup', $this->getModel()),
|
||||
'is_dirty' => route('form.is-dirty', $this->getModel()),
|
||||
'frontend' => str(app(FormSettings::class)->registerUrl)->replace('{slug}', $this->slug),
|
||||
'export' => route('form.export', $this->getModel()),
|
||||
'copy' => route('form.copy', $this->getModel()),
|
||||
'contribution' => route('form.contribution', $this->getModel()),
|
||||
'laterlink' => route('form.laterlink', $this->getModel()),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ class ParticipantFilterScope extends ScoutFilter
|
|||
public string $search = '',
|
||||
public array $options = [],
|
||||
public ?int $parent = null,
|
||||
public ?Sorting $sort = null,
|
||||
public bool $showCancelled = false,
|
||||
public ?Sorting $sort = null
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +54,6 @@ class ParticipantFilterScope extends ScoutFilter
|
|||
$filter->push('parent-id IS NULL');
|
||||
}
|
||||
|
||||
if ($this->showCancelled) {
|
||||
$filter->push('cancelled_at IS NOT NULL');
|
||||
} else {
|
||||
$filter->push('cancelled_at IS NULL');
|
||||
}
|
||||
|
||||
if ($this->parent !== null && $this->parent !== -1) {
|
||||
$filter->push('parent-id = ' . $this->parent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Initialize\Actions;
|
||||
|
||||
use App\Initialize\InitializeGroups;
|
||||
use App\Setting\NamiSettings;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class InitializeUpdateAction
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public string $commandSignature = 'initialize:update';
|
||||
|
||||
/**
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
public array $initializers = [
|
||||
InitializeGroups::class,
|
||||
];
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$api = app(NamiSettings::class)->login();
|
||||
|
||||
foreach ($this->initializers as $initializer) {
|
||||
app($initializer)->handle($api);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ class MemberNewInvoiceAction
|
|||
*/
|
||||
public function handle(Member $member, Subscription $subscription, int $year): array
|
||||
{
|
||||
$invoice = Invoice::createForMember($member, collect([$member]), $year, $subscription);
|
||||
$invoice = Invoice::createForMember($member, Member::where('id', $member->id)->get(), $year, $subscription);
|
||||
|
||||
return [
|
||||
...$invoice->getAttributes(),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
namespace App\Invoice;
|
||||
|
||||
use App\Invoice\Models\Invoice;
|
||||
use App\Payment\Payment;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
use Zoomyboy\Tex\Document;
|
||||
use Zoomyboy\Tex\Engine;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
|
|||
public ?string $zip;
|
||||
public ?string $iban;
|
||||
public ?string $bic;
|
||||
public ?string $replyTo;
|
||||
public ?int $rememberWeeks;
|
||||
|
||||
public static function group(): string
|
||||
|
|
@ -44,7 +43,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
|
|||
'iban' => $this->iban,
|
||||
'bic' => $this->bic,
|
||||
'rememberWeeks' => $this->rememberWeeks,
|
||||
'replyTo' => $this->replyTo,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
@ -66,7 +64,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
|
|||
'iban' => '',
|
||||
'bic' => '',
|
||||
'rememberWeeks' => '',
|
||||
'replyTo' => '',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Invoice\Mails;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
|
@ -13,8 +12,6 @@ class BillMail extends Mailable
|
|||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public InvoiceSettings $settings;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
|
|
@ -22,7 +19,6 @@ class BillMail extends Mailable
|
|||
*/
|
||||
public function __construct(public Invoice $invoice, public string $filename)
|
||||
{
|
||||
$this->settings = app(InvoiceSettings::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +30,7 @@ class BillMail extends Mailable
|
|||
{
|
||||
return $this->markdown('mail.invoice.bill')
|
||||
->attach($this->filename)
|
||||
->when($this->settings->replyTo, fn ($mail) => $mail->replyTo($this->settings->replyTo))
|
||||
->subject('Rechnung | '.$this->settings->from_long);
|
||||
->replyTo('kasse@stamm-silva.de')
|
||||
->subject('Rechnung | DPSG Stamm Silva');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace App\Invoice\Mails;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Invoice\Models\Invoice;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
|
@ -13,8 +12,6 @@ class RememberMail extends Mailable
|
|||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public InvoiceSettings $settings;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
|
|
@ -22,7 +19,6 @@ class RememberMail extends Mailable
|
|||
*/
|
||||
public function __construct(public Invoice $invoice, public string $filename)
|
||||
{
|
||||
$this->settings = app(InvoiceSettings::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,7 +30,7 @@ class RememberMail extends Mailable
|
|||
{
|
||||
return $this->markdown('mail.invoice.remember')
|
||||
->attach($this->filename)
|
||||
->when($this->settings->replyTo, fn ($mail) => $mail->replyTo($this->settings->replyTo))
|
||||
->subject('Zahlungserinnerung | '.$this->settings->from_long);
|
||||
->replyTo('kasse@stamm-silva.de')
|
||||
->subject('Zahlungserinnerung | DPSG Stamm Silva');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ use App\Invoice\Enums\InvoiceStatus;
|
|||
use App\Invoice\InvoiceDocument;
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Invoice\RememberDocument;
|
||||
use App\Invoice\Scopes\InvoiceFilterScope;
|
||||
use App\Member\Member;
|
||||
use App\Payment\Subscription;
|
||||
use Database\Factories\Invoice\Models\InvoiceFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Searchable;
|
||||
use stdClass;
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ class Invoice extends Model
|
|||
/**
|
||||
* @param Collection<int, Member> $members
|
||||
*/
|
||||
public static function createForMember(Member $member, Collection $members, int $year, ?Subscription $subscription = null): self
|
||||
public static function createForMember(Member $member, Collection $members, int $year, Subscription $subscription = null): self
|
||||
{
|
||||
$invoice = new self([
|
||||
'to' => [
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ class MemberRequest extends FormRequest
|
|||
'send_newspaper' => 'boolean',
|
||||
'main_phone' => ['nullable', new ValidPhoneRule('Telefon (Eltern)')],
|
||||
'mobile_phone' => ['nullable', new ValidPhoneRule('Handy (Eltern)')],
|
||||
'work_phone' => ['nullable', new ValidPhoneRule('Tel geschäftlich')],
|
||||
'invoice_address' => '',
|
||||
'gender_id' => 'nullable|exists:genders,id',
|
||||
'region_id' => 'nullable|exists:regions,id',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ class SettingStoreAction
|
|||
'weeks' => 'required|numeric|gte:0',
|
||||
'freshRememberInterval' => 'required|numeric|gte:0',
|
||||
'active' => 'boolean',
|
||||
'replyToMail' => 'nullable|string|email',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +33,6 @@ class SettingStoreAction
|
|||
$settings->formmail = EditorData::from($request->formmail);
|
||||
$settings->yearlymail = EditorData::from($request->yearlymail);
|
||||
$settings->weeks = $request->weeks;
|
||||
$settings->replyToMail = $request->replyToMail;
|
||||
$settings->freshRememberInterval = $request->freshRememberInterval;
|
||||
$settings->active = $request->active;
|
||||
$settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use App\Invoice\InvoiceSettings;
|
|||
use App\Lib\Editor\EditorData;
|
||||
use App\Prevention\Contracts\Preventable;
|
||||
use App\Prevention\Data\PreventionData;
|
||||
use App\Prevention\PreventionSettings;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Attachment;
|
||||
use Illuminate\Mail\Mailable;
|
||||
|
|
@ -20,7 +19,6 @@ class YearlyMail extends Mailable
|
|||
use Queueable, SerializesModels;
|
||||
|
||||
public InvoiceSettings $settings;
|
||||
public PreventionSettings $preventionSettings;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
|
|
@ -29,7 +27,6 @@ class YearlyMail extends Mailable
|
|||
public function __construct(public Preventable $preventable, public EditorData $bodyText, public Collection $preventions)
|
||||
{
|
||||
$this->settings = app(InvoiceSettings::class);
|
||||
$this->preventionSettings = app(PreventionSettings::class);
|
||||
$this->bodyText = $this->bodyText
|
||||
->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray());
|
||||
}
|
||||
|
|
@ -41,15 +38,9 @@ class YearlyMail extends Mailable
|
|||
*/
|
||||
public function envelope()
|
||||
{
|
||||
$envelope = (new Envelope(
|
||||
return (new Envelope(
|
||||
subject: $this->preventable->preventableSubject(),
|
||||
))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name);
|
||||
|
||||
if ($this->preventionSettings->replyToMail !== null) {
|
||||
$envelope->replyTo($this->preventionSettings->replyToMail);
|
||||
}
|
||||
|
||||
return $envelope;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class PreventionSettings extends LocalSettings
|
|||
public int $freshRememberInterval;
|
||||
public bool $active;
|
||||
public FilterScope $yearlyMemberFilter;
|
||||
public ?string $replyToMail;
|
||||
/**
|
||||
* @var array<int, string>
|
||||
* @todo Create collection cast to Collection of enums
|
||||
|
|
@ -50,7 +49,6 @@ class PreventionSettings extends LocalSettings
|
|||
...$this->toArray(),
|
||||
'weeks' => (string) $this->weeks,
|
||||
'freshRememberInterval' => (string) $this->freshRememberInterval,
|
||||
'replyToMail' => $this->replyToMail,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,6 @@ class ParticipantFactory extends Factory
|
|||
return $this->state(['data' => $data]);
|
||||
}
|
||||
|
||||
public function cancelled(): self {
|
||||
return $this->state(['cancelled_at' => now()->subWeek()]);
|
||||
}
|
||||
|
||||
public function nr(int $number): self
|
||||
{
|
||||
return $this->state(['member_id' => $number]);
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('participants', function (Blueprint $table) {
|
||||
$table->datetime('cancelled_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('participants', function (Blueprint $table) {
|
||||
$table->dropColumn('cancelled_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->add('bill.replyTo', '');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||
|
||||
return new class extends SettingsMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->add('form.replyToMail', '');
|
||||
$this->migrator->add('prevention.replyToMail', '');
|
||||
}
|
||||
};
|
||||
|
|
@ -30,7 +30,10 @@ export default typescriptEslint.config(
|
|||
'vue/multi-word-component-names': 'off',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/first-attribute-linebreak': 'off',
|
||||
"vue/first-attribute-linebreak": ["error", {
|
||||
"singleline": "beside",
|
||||
"multiline": "beside"
|
||||
}],
|
||||
'vue/no-undef-properties': ['error', {
|
||||
'ignores': ['/^\\$/']
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@
|
|||
"uuid": "^11.1.0",
|
||||
"vite": "^4.5.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"wnumb": "^1.2.0"
|
||||
|
|
@ -1873,16 +1872,6 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/clipboard": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
|
||||
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
|
||||
"dependencies": {
|
||||
"good-listener": "^1.2.2",
|
||||
"select": "^1.1.2",
|
||||
"tiny-emitter": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
|
|
@ -2151,11 +2140,6 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegate": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
|
||||
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
|
|
@ -2864,14 +2848,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/good-listener": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
|
||||
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
|
||||
"dependencies": {
|
||||
"delegate": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
|
|
@ -4099,11 +4075,6 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/select": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
|
||||
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
|
|
@ -4574,11 +4545,6 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.14",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||
|
|
@ -4986,14 +4952,6 @@
|
|||
"vue": "^3.0.0 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-clipboard3": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-clipboard3/-/vue-clipboard3-2.0.0.tgz",
|
||||
"integrity": "sha512-Q9S7dzWGax7LN5iiSPcu/K1GGm2gcBBlYwmMsUc5/16N6w90cbKow3FnPmPs95sungns4yvd9/+JhbAznECS2A==",
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
"uuid": "^11.1.0",
|
||||
"vite": "^4.5.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"wnumb": "^1.2.0"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 010825124f7791dfc0dc607e23ec99f49be8aef5
|
||||
Subproject commit a4a2a2b3fd7a099cf1e5a9360d5d5450030bf673
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit f7b04591830ebdeaddf76236e4cbc87a8b3eec8f
|
||||
Subproject commit 7304963370ff64fb5accf08da4864981cc424301
|
||||
|
|
@ -1,7 +1,3 @@
|
|||
.v-popper__popper {
|
||||
@apply max-w-lg;
|
||||
}
|
||||
|
||||
.v-popper--theme-tooltip .v-popper__inner {
|
||||
@apply bg-primary-400 text-primary-800 px-3 py-1 text-sm;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><g class="" transform="translate(0,0)" style=""><path d="m153.654 18 52.57 134.734c1.698 3.994 4.05 5.83 7.243 6.977 3.2 1.15 7.36 1.2 11.058.17 3.698-1.03 6.71-3.146 7.996-4.915 1.288-1.77 1.634-2.564.505-5.24l-.046-.112L181.57 18h-27.916zm94.168 120.143 1.88 4.81-.09-.223c3.346 7.937 1.828 16.822-2.532 22.82-4.36 5.996-10.773 9.734-17.723 11.67-6.95 1.937-14.653 2.065-21.98-.57-7.327-2.634-14.155-8.447-17.742-16.923l-.05-.118-1.757-4.5c-31.31 19.804-42.47 42.026-35.367 68.89 1.24 4.681 3.422 12.364 5.964 22.13 74.37-5.274 139.945-23.872 199.808-51.6-10.297-13.867-22.5-25.83-38.232-34.53-20.505-11.34-47.652-20.157-72.178-21.857zm120.557 71.52c-61.497 28.81-129.173 48.378-205.575 54.196 2.03 8.683 4.08 18.28 5.95 28.495 89.592-10.084 163.043-26.22 217.755-48.767-5.743-11.72-11.593-23.19-18.13-33.924zm26.04 50.16c-57.093 23.772-131.99 40.087-222.73 50.322C180.697 371.423 179.614 446.752 128 480c16.27 0 31.892-.152 46.926-.45 17.84-25.554 31.27-66.222 32.08-86.146 8.27 16.793 3.297 59.32-5.36 85.434 2.735-.093 5.435-.193 8.127-.297 11.824-12.397 11.724-28.632 14.72-47.284 3.324 14.92 7 32.967 9.505 46.156 11.273-.616 22.152-1.34 32.606-2.183 16.38-20.358 21.65-49.604 18.63-85.48 4.226 29.1 9.116 62.138 11.873 82.55a771.724 771.724 0 0 0 27.807-3.614c5.04-18.787-4.1-48.444-2.072-69.54 11.123 43.113 22.247 55.45 33.37 64.043a456.166 456.166 0 0 0 15.733-3.526c-4.7-13.95 1.573-22.497 1.18-39.986 5.647 18.99 14.625 26.958 24.428 32.816 6.506-2.1 12.66-4.336 18.492-6.697-10.538-6.57-10.113-26.374-12.38-42.926 5.954 21.703 14.413 32.418 24.083 37.816 29.124-13.8 48.69-31.534 60.398-53.657-9.078-3.82-18.674-13.002-28.068-20.092 13.214 7.477 23.684 10.614 32.37 10.93a112.386 112.386 0 0 0 3.552-9.868c-56.326-19.528-80.07-64.018-101.58-108.178z" fill="#fff" fill-opacity="1"></path></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg x="0px" y="0px" viewBox="0 0 511.626 511.627" xml:space="preserve">
|
||||
<path d="M392.857,292.354h-18.274c-2.669,0-4.859,0.855-6.563,2.573c-1.718,1.708-2.573,3.897-2.573,6.563v91.361 c0,12.563-4.47,23.315-13.415,32.262c-8.945,8.945-19.701,13.414-32.264,13.414H82.224c-12.562,0-23.317-4.469-32.264-13.414 c-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562,4.471-23.313,13.417-32.259c8.947-8.947,19.702-13.418,32.264-13.418 h200.994c2.669,0,4.859-0.859,6.57-2.57c1.711-1.713,2.566-3.9,2.566-6.567V82.221c0-2.662-0.855-4.853-2.566-6.563 c-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648,0-42.016,8.042-58.102,24.125C8.042,113.297,0,132.665,0,155.313v237.542 c0,22.647,8.042,42.018,24.123,58.095c16.086,16.084,35.454,24.13,58.102,24.13h237.543c22.647,0,42.017-8.046,58.101-24.13 c16.085-16.077,24.127-35.447,24.127-58.095v-91.358c0-2.669-0.856-4.859-2.574-6.57 C397.709,293.209,395.519,292.354,392.857,292.354z"/>
|
||||
<path d="M506.199,41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948,0-9.233,1.807-12.847,5.424 c-3.617,3.615-5.428,7.898-5.428,12.847s1.811,9.233,5.428,12.85l50.247,50.248L198.424,304.067 c-1.906,1.903-2.856,4.093-2.856,6.563c0,2.479,0.953,4.668,2.856,6.571l32.548,32.544c1.903,1.903,4.093,2.852,6.567,2.852 s4.665-0.948,6.567-2.852l186.148-186.148l50.251,50.248c3.614,3.617,7.898,5.426,12.847,5.426s9.233-1.809,12.851-5.426 c3.617-3.616,5.424-7.898,5.424-12.847V54.818C511.626,49.866,509.813,45.586,506.199,41.971z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -47,10 +47,8 @@ export function useApiIndex(firstUrl, siteName = null) {
|
|||
single.value = null;
|
||||
}
|
||||
|
||||
async function remove(model, force = true) {
|
||||
await axios.delete(model.links.destroy, {
|
||||
headers: { 'X-Force': force ? '1' : '0' }
|
||||
});
|
||||
async function remove(model) {
|
||||
await axios.delete(model.links.destroy);
|
||||
await reload();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ var options = {
|
|||
},
|
||||
};
|
||||
|
||||
export { Plugin, options };
|
||||
export {Plugin, options};
|
||||
|
|
|
|||
|
|
@ -23,54 +23,33 @@
|
|||
<div v-show="active === 0" class="grid grid-cols-4 gap-3">
|
||||
<div class="flex space-x-3 col-span-2">
|
||||
<f-text id="name" v-model="single.name" class="grow" label="Name" required />
|
||||
<f-switch
|
||||
id="is_active"
|
||||
v-model="single.is_active"
|
||||
name="is_active"
|
||||
label="Aktiv"
|
||||
hint="Inaktive Veranstaltungen werden außerhalb von Adrema wie nicht existierende Veranstaltungen betrachtet. Insbesondere ist eine Anmeldung dann nicht möglich und die Veranstaltung erscheint auch nicht in der Veranstaltungs-Übersicht."
|
||||
/>
|
||||
<f-switch
|
||||
id="is_private"
|
||||
v-model="single.is_private"
|
||||
name="is_private"
|
||||
label="Privat"
|
||||
hint="Ist eine Veranstaltung privat, so wird diese nicht auf der Website angezeigt. Eine Anmeldung ist jedoch trotzdem möglich, wenn man über den Anmelde-Link verfügt."
|
||||
/>
|
||||
<f-switch id="is_active" v-model="single.is_active" name="is_active" label="Aktiv" />
|
||||
<f-switch id="is_private" v-model="single.is_private" name="is_private" label="Privat" />
|
||||
</div>
|
||||
<f-singlefile
|
||||
id="header_image"
|
||||
v-model="single.header_image"
|
||||
class="col-span-2"
|
||||
label="Bild"
|
||||
name="header_image"
|
||||
parent-name="form"
|
||||
:parent-id="single.id"
|
||||
collection="headerImage"
|
||||
required
|
||||
<f-singlefile id="header_image"
|
||||
v-model="single.header_image"
|
||||
class="col-span-2"
|
||||
label="Bild"
|
||||
name="header_image"
|
||||
parent-name="form"
|
||||
:parent-id="single.id"
|
||||
collection="headerImage"
|
||||
required
|
||||
/>
|
||||
<f-text id="from" v-model="single.from" type="date" label="Von" required />
|
||||
<f-text id="to" v-model="single.to" type="date" label="Bis" required />
|
||||
<f-text id="zip" v-model="single.zip" label="PLZ" />
|
||||
<f-text id="location" v-model="single.location" label="Ort" />
|
||||
<f-select id="country" v-model="single.country" class="col-span-2" name="country" label="Land" :options="meta.countries" />
|
||||
<f-text
|
||||
id="registration_from"
|
||||
v-model="single.registration_from"
|
||||
type="datetime-local"
|
||||
label="Registrierung von"
|
||||
hint="Ist eine Anmeldung laut dieser zwei Datumsangaben möglich, kann man sich anmelden. Andernfalls wird die Veranstaltung (mit Beschreibungstext) auf der Übersichtsseite angezeigt, man kommt allerdings nicht zum Anmeldeformular."
|
||||
required
|
||||
/>
|
||||
<f-text id="registration_from" v-model="single.registration_from" type="datetime-local" label="Registrierung von" required />
|
||||
<f-text id="registration_until" v-model="single.registration_until" type="datetime-local" label="Registrierung bis" required />
|
||||
<f-textarea
|
||||
id="excerpt"
|
||||
v-model="single.excerpt"
|
||||
hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)."
|
||||
label="Auszug"
|
||||
:rows="5"
|
||||
class="col-span-full"
|
||||
required
|
||||
<f-textarea id="excerpt"
|
||||
v-model="single.excerpt"
|
||||
hint="Gebe hier eine kurze Beschreibung für die Veranstaltungs-Übersicht ein (Maximal 130 Zeichen)."
|
||||
label="Auszug"
|
||||
:rows="5"
|
||||
class="col-span-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div v-if="active === 1">
|
||||
|
|
@ -82,33 +61,32 @@
|
|||
</div>
|
||||
<div v-show="active === 3" class="grid grid-cols-[1fr_300px] gap-3">
|
||||
<ui-note class="mt-2 col-span-full">
|
||||
Hier kannst du die E-Mail anpassen, die nach der Anmeldung an den Teilnehmer verschickt wird.<br />
|
||||
Es gibt dafür einen ersten E-Mail-Teil und einen zweiten E-Mail-Teil. Dazwischen werden die Daten des Teilnehmers aufgelistet.<br />
|
||||
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br />
|
||||
Hier kannst du die E-Mail anpassen, die nach der Anmeldung an den Teilnehmer verschickt wird.<br>
|
||||
Es gibt dafür einen ersten E-Mail-Teil und einen zweiten E-Mail-Teil. Dazwischen werden die Daten des Teilnehmers aufgelistet.<br>
|
||||
Die Anrede ("Hallo Max Mustermann") wird automatisch an den Anfang gesetzt.<br>
|
||||
Außerdem kannst du Dateien hochladen, die automatisch mit angehangen werden.
|
||||
</ui-note>
|
||||
<div>
|
||||
<ui-tabs v-model="activeMailTab" :entries="mailTabs" />
|
||||
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" :rows="8" conditions required>
|
||||
<template #conditions="{data, resolve}">
|
||||
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve" />
|
||||
<template #conditions="{cData, resolve}">
|
||||
<conditions-form id="mail_top_conditions" :single="single" :value="cData" @save="resolve" />
|
||||
</template>
|
||||
</f-editor>
|
||||
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" :rows="8" conditions required>
|
||||
<template #conditions="{data, resolve}">
|
||||
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve" />
|
||||
<template #conditions="{d, resolve}">
|
||||
<conditions-form id="mail_bottom_conditions" :single="single" :value="d" @save="resolve" />
|
||||
</template>
|
||||
</f-editor>
|
||||
</div>
|
||||
<f-multiplefiles
|
||||
id="mailattachments"
|
||||
v-model="single.mailattachments"
|
||||
label="Anhänge"
|
||||
name="mailattachments"
|
||||
parent-name="form"
|
||||
:parent-id="single.id"
|
||||
collection="mailattachments"
|
||||
class="row-span-2"
|
||||
<f-multiplefiles id="mailattachments"
|
||||
v-model="single.mailattachments"
|
||||
label="Anhänge"
|
||||
name="mailattachments"
|
||||
parent-name="form"
|
||||
:parent-id="single.id"
|
||||
collection="mailattachments"
|
||||
class="row-span-2"
|
||||
>
|
||||
<template #buttons="{file, buttonClass, iconClass}">
|
||||
<a v-tooltip="`Bedingungen`" href="#" :class="[buttonClass, 'bg-blue-200', 'relative']" @click.prevent="fileSettingPopup = file">
|
||||
|
|
@ -127,12 +105,11 @@
|
|||
</div>
|
||||
<div v-show="active === 5" class="grid grid-cols-2 gap-3">
|
||||
<f-switch id="needs_prevention" v-model="single.needs_prevention" name="needs_prevention" label="Prävention" />
|
||||
<f-editor
|
||||
id="prevention_text"
|
||||
v-model="single.prevention_text"
|
||||
hint="Wird an die Präventions-Email angehangen, die Teilnehmende dieser Veranstaltung erhalten"
|
||||
:rows="6"
|
||||
label="Präventions-Hinweis"
|
||||
<f-editor id="prevention_text"
|
||||
v-model="single.prevention_text"
|
||||
hint="Wird an die Präventions-Email angehangen, die Teilnehmende dieser Veranstaltung erhalten"
|
||||
:rows="6"
|
||||
label="Präventions-Hinweis"
|
||||
/>
|
||||
<ui-box heading="Bedingung für Präventions-Unterlagen">
|
||||
<conditions id="prevention_conditions" v-model="single.prevention_conditions" :single="single" />
|
||||
|
|
@ -169,7 +146,6 @@
|
|||
<th>Name</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tags</th>
|
||||
<th>Anzahl TN</th>
|
||||
<th />
|
||||
</tr>
|
||||
|
|
@ -186,13 +162,6 @@
|
|||
<td>
|
||||
<div v-text="form.to_human" />
|
||||
</td>
|
||||
<td>
|
||||
<div class="bool-row">
|
||||
<ui-bool true-comment="aktiv" false-comment="inaktiv" :value="form.is_active">A</ui-bool>
|
||||
<ui-bool true-comment="private Veranstaltung" false-comment="nicht private Veranstaltung" :value="form.is_private">P</ui-bool>
|
||||
<ui-bool true-comment="Anmeldung möglich (lt. 'Registrierung von / bis')" false-comment="Anmeldeschluss erreicht" :value="form.is_in_dates">D</ui-bool>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-text="form.participants_count" />
|
||||
</td>
|
||||
|
|
@ -202,11 +171,9 @@
|
|||
<ui-action-button tooltip="Teilnehmende anzeigen" class="btn-info" icon="user" @click.prevent="showParticipants(form)" />
|
||||
<ui-action-button :href="form.links.frontend" target="_BLANK" tooltip="zur Anmeldeseite" class="btn-info" icon="eye" />
|
||||
<ui-action-button tooltip="Kopieren" class="btn-info" icon="copy" @click="onCopy(form)" />
|
||||
<ui-action-button tooltip="Nachmelde-Link kopieren" class="btn-info" icon="externallink" @click="copyLaterLink(form)" />
|
||||
<ui-action-button tooltip="Zuschuss-Liste erstellen" class="btn-info" icon="contribution" @click="onGenerateContribution(form)" />
|
||||
<ui-action-button :href="form.links.export" target="_BLANK" tooltip="als Tabellendokument exportieren" class="btn-info" icon="document" />
|
||||
<ui-action-button tooltip="alle Teilnehmende löschen" class="btn-danger" icon="broom" @click.prevent="onCleanup(form)" />
|
||||
<ui-action-button tooltip="Veranstaltung löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(form)" />
|
||||
<ui-action-button tooltip="Löschen" class="btn-danger" icon="trash" @click.prevent="onDelete(form)" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -219,31 +186,29 @@
|
|||
</template>
|
||||
|
||||
<script lang="js" setup>
|
||||
import {ref, inject, computed} from 'vue';
|
||||
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
|
||||
import { ref, inject, computed } from 'vue';
|
||||
import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
|
||||
import FormBuilder from '../formtemplate/FormBuilder.vue';
|
||||
import Participants from './Participants.vue';
|
||||
import Conditions from './Conditions.vue';
|
||||
import ConditionsForm from './ConditionsForm.vue';
|
||||
import {useToast} from 'vue-toastification';
|
||||
import { useToast } from 'vue-toastification';
|
||||
import useSwal from '@/stores/swalStore.ts';
|
||||
import useDownloads from '@/composables/useDownloads.ts';
|
||||
import useClipboard from 'vue-clipboard3';
|
||||
|
||||
const props = defineProps(indexProps);
|
||||
const {meta, data, reloadPage, reload, create, single, edit, cancel, submit, remove, getFilter, setFilter} = useIndex(props.data, 'form');
|
||||
const { meta, data, reloadPage, reload, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
|
||||
const axios = inject('axios');
|
||||
const toast = useToast();
|
||||
const {download} = useDownloads();
|
||||
const {toClipboard} = useClipboard();
|
||||
|
||||
const showing = ref(null);
|
||||
const fileSettingPopup = ref(null);
|
||||
|
||||
const active = ref(0);
|
||||
const activeMailTab = ref(0);
|
||||
const tabs = [{title: 'Allgemeines'}, {title: 'Beschreibung'}, {title: 'Formular'}, {title: 'Bestätigungs-E-Mail'}, {title: 'Export'}, {title: 'Prävention'}, {title: 'Zuschüsse'}];
|
||||
const mailTabs = [{title: 'vor Daten'}, {title: 'nach Daten'}];
|
||||
const tabs = [{ title: 'Allgemeines' }, { title: 'Beschreibung' }, { title: 'Formular' }, { title: 'Bestätigungs-E-Mail' }, { title: 'Export' }, { title: 'Prävention' }, {title: 'Zuschüsse'}];
|
||||
const mailTabs = [{ title: 'vor Daten' }, { title: 'nach Daten' }];
|
||||
const swal = useSwal();
|
||||
|
||||
const allFields = computed(() => {
|
||||
|
|
@ -254,7 +219,7 @@ const allFields = computed(() => {
|
|||
const result = [];
|
||||
single.value.config.sections.forEach((section) => {
|
||||
section.fields.forEach((field) => {
|
||||
result.push({id: field.key, name: field.name});
|
||||
result.push({ id: field.key, name: field.name });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -262,28 +227,21 @@ const allFields = computed(() => {
|
|||
});
|
||||
|
||||
async function onCopy(form) {
|
||||
await swal.confirm(
|
||||
'Diese Veranstaltung kopieren?',
|
||||
'Nach dem Kopieren wird die Veranstaltung auf inaktiv gesetzt. Bitte aktiviere den Filter "inaktive zeigen", um die kopierte Veranstaltung zu sehen.',
|
||||
);
|
||||
await swal.confirm('Diese Veranstaltung kopieren?', 'Nach dem Kopieren wird die Veranstaltung auf inaktiv gesetzt. Bitte aktiviere den Filter "inaktive zeigen", um die kopierte Veranstaltung zu sehen.');
|
||||
await axios.post(form.links.copy, {});
|
||||
reload(false);
|
||||
}
|
||||
|
||||
async function onGenerateContribution(form) {
|
||||
const response = await swal.ask(
|
||||
'Zuschussliste erstellen',
|
||||
'Hiermit erstellst du eine Zuschussliste mit allen angemeldeten Mitgliedern. Bite wähle aus, für welche Organisation du eine Liste erstellen willst.',
|
||||
[
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Organisation',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: meta.value.contribution_types,
|
||||
},
|
||||
],
|
||||
);
|
||||
const response = await swal.ask('Zuschussliste erstellen', 'Hiermit erstellst du eine Zuschussliste mit allen angemeldeten Mitgliedern. Bite wähle aus, für welche Organisation du eine Liste erstellen willst.', [
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Organisation',
|
||||
required: true,
|
||||
type: 'select',
|
||||
options: meta.value.contribution_types,
|
||||
}
|
||||
]);
|
||||
await download(form.links.contribution, {type: response.type, validate: '1'});
|
||||
await download(form.links.contribution, {type: response.type});
|
||||
}
|
||||
|
|
@ -293,12 +251,6 @@ async function onDelete(form) {
|
|||
await remove(form);
|
||||
}
|
||||
|
||||
async function onCleanup(form) {
|
||||
await swal.confirm('Teilnehmende löschen?', `Alle Teilnehmenden der Veranstaltung ${form.name} werden unwiderruflich gelöscht. Die Veranstaltung selbst bleibt erhalten.`);
|
||||
await axios.delete(form.links.cleanup);
|
||||
reload(false);
|
||||
}
|
||||
|
||||
function setTemplate(template) {
|
||||
active.value = 0;
|
||||
single.value.config = template.config;
|
||||
|
|
@ -306,12 +258,6 @@ function setTemplate(template) {
|
|||
single.value.mail_bottom = template.mail_bottom;
|
||||
}
|
||||
|
||||
async function copyLaterLink(form) {
|
||||
const response = await axios.get(form.links.laterlink);
|
||||
await toClipboard(response.data.url);
|
||||
toast.success('Link in Zwischenablage kopiert');
|
||||
}
|
||||
|
||||
async function saveFileConditions(conditions) {
|
||||
await axios.patch(`/mediaupload/${fileSettingPopup.value.id}`, {
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@
|
|||
<ui-popup v-if="assigning !== null" heading="Mitglied zuweisen" closeable @close="assigning = null">
|
||||
<member-assign @assign="assign" />
|
||||
</ui-popup>
|
||||
<ui-popup v-if="deleting !== null" heading="Teilnehmer*in abmelden?" @close="deleting = null">
|
||||
<ui-popup v-if="deleting !== null" heading="Teilnehmer*in löschen?" @close="deleting = null">
|
||||
<div>
|
||||
<p class="mt-4">Den*Die Teilnehmer*in abmelden?</p>
|
||||
<f-switch class="mt-2" v-model="deleting.force" name="force_delete" id="force_delete" label="löschen statt abmelden (permanent)" size="sm" />
|
||||
<p class="mt-4">Den*Die Teilnehmer*in löschen?</p>
|
||||
<div class="grid grid-cols-2 gap-3 mt-6">
|
||||
<a href="#" class="text-center btn btn-danger" @click.prevent="handleDelete">Mitglied loschen</a>
|
||||
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
|
||||
|
|
@ -30,7 +29,6 @@
|
|||
</template>
|
||||
|
||||
<template #fields>
|
||||
<f-switch id="show_cancelled" v-model="innerFilter.show_cancelled" label="Abgemeldete zeigen" size="sm" name="show_cancelled" />
|
||||
<template v-for="(filter, index) in meta.filters">
|
||||
<f-select v-if="filter.base_type === 'CheckboxField'"
|
||||
:id="`filter-field-${index}`"
|
||||
|
|
@ -97,7 +95,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(participant)"><ui-sprite src="pencil" /></a>
|
||||
<a v-tooltip="`Abmelden`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = {model: participant, force: false}"><ui-sprite src="trash" /></a>
|
||||
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = participant"><ui-sprite src="trash" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="child in childrenOf(participant.id)" :key="child.id">
|
||||
|
|
@ -116,7 +114,7 @@
|
|||
</td>
|
||||
<td>
|
||||
<a v-tooltip="`Bearbeiten`" href="#" class="ml-2 inline-flex btn btn-warning btn-sm" @click.prevent="editReal(child)"><ui-sprite src="pencil" /></a>
|
||||
<a v-tooltip="`Abmelden`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = {model: child, force: false}"><ui-sprite src="trash" /></a>
|
||||
<a v-tooltip="`Löschen`" href="#" class="ml-2 inline-flex btn btn-danger btn-sm" @click.prevent="deleting = child"><ui-sprite src="trash" /></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -211,7 +209,7 @@ const sortingConfig = computed({
|
|||
});
|
||||
|
||||
async function handleDelete() {
|
||||
await remove(deleting.value.model, deleting.value.force);
|
||||
await remove(deleting.value);
|
||||
deleting.value = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@
|
|||
<f-text id="sectionform-name" v-model="singleSection.model.name" label="Name"></f-text>
|
||||
<f-textarea id="sectionform-intro" v-model="singleSection.model.intro" label="Einleitung"></f-textarea>
|
||||
</asideform>
|
||||
<asideform
|
||||
v-if="moving !== null"
|
||||
heading="Feld verschieben nach Sektion …"
|
||||
@close="moving = null"
|
||||
>
|
||||
<div class="mt-3 grid gap-3">
|
||||
<a v-for="(section, index) in modelValue.sections" :key="index" class="py-2 px-3 border rounded bg-zinc-800 hover:bg-zinc-700 transition" href="#" @click.prevent="moveFieldToSection(index)">
|
||||
<span v-text="section.name"></span>
|
||||
</a>
|
||||
</div>
|
||||
</asideform>
|
||||
<asideform v-if="singleSection !== null && singleSection.mode === 'reorder'" heading="Felder ordnen" @close="singleSection = null" @submit="storeSection">
|
||||
<draggable v-model="singleSection.model.fields" item-key="key" :component-data="{class: 'mt-3 grid gap-3'}">
|
||||
<template #item="{element}">
|
||||
|
|
@ -70,8 +59,6 @@
|
|||
@editFields="startReordering($event.detail[0])"
|
||||
@deleteSection="deleteSection($event.detail[0])"
|
||||
@active="updateActive($event.detail[0])"
|
||||
@copy-field="copyField($event.detail[0], $event.detail[1])"
|
||||
@move-field="moving = {section: $event.detail[0], field: $event.detail[1]}"
|
||||
></event-form>
|
||||
</ui-box>
|
||||
</form>
|
||||
|
|
@ -80,7 +67,7 @@
|
|||
<script lang="js" setup>
|
||||
import { watch, computed, ref } from 'vue';
|
||||
import { snakeCase } from 'change-case';
|
||||
import '!/adrema-form/dist/assets/main.js';
|
||||
import '!/adrema-form/dist/main.js';
|
||||
import Asideform from './Asideform.vue';
|
||||
import TextareaField from './TextareaField.vue';
|
||||
import TextField from './TextField.vue';
|
||||
|
|
@ -96,7 +83,6 @@ import Draggable from 'vuedraggable';
|
|||
|
||||
const singleSection = ref(null);
|
||||
const singleField = ref(null);
|
||||
const moving = ref(null);
|
||||
const active = ref(null);
|
||||
|
||||
async function onReorder() {
|
||||
|
|
@ -138,29 +124,6 @@ function editSection(sectionIndex) {
|
|||
mode: 'edit',
|
||||
};
|
||||
}
|
||||
function moveFieldToSection(newSectionIndex) {
|
||||
if (!moving.value) {
|
||||
return;
|
||||
}
|
||||
singleField.value = {
|
||||
model: JSON.parse(JSON.stringify(inner.value.sections[moving.value.section].fields[moving.value.field])),
|
||||
sectionIndex: newSectionIndex,
|
||||
index: null,
|
||||
};
|
||||
storeField();
|
||||
deleteField(moving.value.section, moving.value.field);
|
||||
moving.value = null;
|
||||
}
|
||||
|
||||
function copyField(sectionIndex, fieldIndex) {
|
||||
var field = JSON.parse(JSON.stringify(inner.value.sections[sectionIndex].fields[fieldIndex]));
|
||||
field.name = field.name + ' - Kopie';
|
||||
singleField.value = {
|
||||
model: field,
|
||||
sectionIndex: sectionIndex,
|
||||
index: null,
|
||||
};
|
||||
}
|
||||
|
||||
function startReordering(index) {
|
||||
singleSection.value = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
<f-text id="iban" v-model="inner.iban" label="IBAN"></f-text>
|
||||
<f-text id="bic" v-model="inner.bic" label="BIC"></f-text>
|
||||
<f-text id="remember_weeks" v-model="inner.rememberWeeks" type="number" label="Erinnerung alle X Wochen versenden"></f-text>
|
||||
<f-text id="reply_to" v-model="inner.replyTo" label="Reply-To E-Mail-Adresse"></f-text>
|
||||
</form>
|
||||
</setting-layout>
|
||||
</page-layout>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
<div class="grid grid-cols-2 gap-4">
|
||||
<f-text id="register_url" v-model="inner.registerUrl" label="Formular-Link"></f-text>
|
||||
<f-text id="clear_cache_url" v-model="inner.clearCacheUrl" label="Frontend-Cache-Url"></f-text>
|
||||
<f-text id="reply_to_mail" v-model="inner.replyToMail" label="Reply-To-Adresse"></f-text>
|
||||
</div>
|
||||
</form>
|
||||
</setting-layout>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
<f-editor v-if="active === 1" id="yearlymail" v-model="data.yearlymail" label="Jährliche Präventions-Erinnerung"></f-editor>
|
||||
<f-member-filter id="yearly_member_filter" v-model="data.yearlyMemberFilter" label="nur für folgende Mitglieder erlauben" />
|
||||
<f-multipleselect id="prevent_against" v-model="data.preventAgainst" :options="meta.preventAgainsts" label="An diese Dokumente erinnern" size="sm"></f-multipleselect>
|
||||
<f-text id="reply_to_mail" v-model="data.replyToMail" label="Reply-To-Adresse"></f-text>
|
||||
</div>
|
||||
</form>
|
||||
</setting-layout>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@component('mail::message')
|
||||
# {{ $invoice->greeting }},
|
||||
|
||||
Im Anhang findet ihr die aktuelle Rechnung an {{$settings->from}} für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
Im Anhang findet ihr die aktuelle Rechnung des Stammes Silva für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
|
||||
@component('mail::subcopy')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
@component('mail::message')
|
||||
# {{ $invoice->greeting }},
|
||||
|
||||
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge an {{$settings->from}} für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge des Stammes Silva für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
|
||||
|
||||
@component('mail::subcopy')
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,8 @@ use App\Fileshare\Actions\FileshareStoreAction;
|
|||
use App\Fileshare\Actions\FileshareUpdateAction;
|
||||
use App\Fileshare\Actions\ListFilesAction;
|
||||
use App\Form\Actions\ExportAction as ActionsExportAction;
|
||||
use App\Form\Actions\FormCleanupAction;
|
||||
use App\Form\Actions\FormCopyAction;
|
||||
use App\Form\Actions\FormDestroyAction;
|
||||
use App\Form\Actions\FormGenerateLaterlinkAction;
|
||||
use App\Form\Actions\FormIndexAction;
|
||||
use App\Group\Actions\GroupBulkstoreAction;
|
||||
use App\Group\Actions\GroupIndexAction;
|
||||
|
|
@ -168,7 +166,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::get('/form', FormIndexAction::class)->name('form.index');
|
||||
Route::patch('/form/{form}', FormUpdateAction::class)->name('form.update');
|
||||
Route::delete('/form/{form}', FormDestroyAction::class)->name('form.destroy');
|
||||
Route::delete('/form/{form}/cleanup', FormCleanupAction::class)->name('form.cleanup');
|
||||
Route::post('/formtemplate', FormtemplateStoreAction::class)->name('formtemplate.store');
|
||||
Route::patch('/formtemplate/{formtemplate}', FormtemplateUpdateAction::class)->name('formtemplate.update');
|
||||
Route::delete('/formtemplate/{formtemplate}', FormtemplateDestroyAction::class)->name('formtemplate.destroy');
|
||||
|
|
@ -183,7 +180,6 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
|||
Route::post('/form/{form}/participant', ParticipantStoreAction::class)->name('form.participant.store');
|
||||
Route::post('/form/{form}/copy', FormCopyAction::class)->name('form.copy');
|
||||
Route::get('/form/{form}/contribution', GenerateContributionAction::class)->name('form.contribution');
|
||||
Route::get('/form/{form}/laterlink', FormGenerateLaterlinkAction::class)->name('form.laterlink');
|
||||
|
||||
// ------------------------------------ fileshare -----------------------------------
|
||||
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ it('testItDisplaysForms', function () {
|
|||
->assertInertiaPath('data.data.0.links.participant_index', route('form.participant.index', ['form' => $form]))
|
||||
->assertInertiaPath('data.data.0.links.export', route('form.export', ['form' => $form]))
|
||||
->assertInertiaPath('data.data.0.links.contribution', route('form.contribution', ['form' => $form]))
|
||||
->assertInertiaPath('data.data.0.links.laterlink', route('form.laterlink', ['form' => $form]))
|
||||
->assertInertiaPath('data.meta.links.store', route('form.store'))
|
||||
->assertInertiaPath('data.meta.links.formtemplate_index', route('formtemplate.index'))
|
||||
->assertInertiaPath('data.meta.default.name', '')
|
||||
|
|
@ -98,18 +97,6 @@ it('testItDisplaysForms', function () {
|
|||
->assertInertiaPath('data.meta.default.location', '');
|
||||
});
|
||||
|
||||
it('displays participants count', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
Form::factory()
|
||||
->has(Participant::factory()->count(2))
|
||||
->has(Participant::factory()->cancelled()->count(3))
|
||||
->create();
|
||||
|
||||
sleep(1);
|
||||
$this->get(route('form.index'))
|
||||
->assertInertiaPath('data.data.0.participants_count', 2);
|
||||
});
|
||||
|
||||
it('testFormtemplatesHaveData', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
Formtemplate::factory()->name('tname')->sections([FormtemplateSectionRequest::new()->name('sname')->fields([
|
||||
|
|
@ -196,22 +183,6 @@ it('testItDoesntReturnInactiveForms', function () {
|
|||
$this->callFilter('form.index', ['inactive' => false])->assertInertiaCount('data.data', 2);
|
||||
});
|
||||
|
||||
it('returns in dates', function () {
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Form::factory()->create();
|
||||
|
||||
sleep(1);
|
||||
$this->callFilter('form.index', [])->assertInertiaPath('data.data.0.is_in_dates', true);
|
||||
});
|
||||
|
||||
it('returns not in dates', function () {
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
Form::factory()->registrationFrom(now()->addDay(2))->create();
|
||||
|
||||
sleep(1);
|
||||
$this->callFilter('form.index', [])->assertInertiaPath('data.data.0.is_in_dates', false);
|
||||
});
|
||||
|
||||
it('testItOrdersByStartDateDesc', function () {
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$form1 = Form::factory()->from(now()->addDays(4))->to(now()->addYear())->create();
|
||||
|
|
|
|||
|
|
@ -78,9 +78,6 @@ it('testItShowsEmptyFilters', function () {
|
|||
$this->callFilter('form.participant.index', ['data' => ['check' => null]], ['form' => $form])->assertHasJsonPath('meta.filter.data.check')->assertJsonPath('meta.filter.data.check', null);
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => 'A']], ['form' => $form])->assertJsonPath('meta.filter.data.check', 'A');
|
||||
$this->callFilter('form.participant.index', ['data' => []], ['form' => $form])->assertJsonPath('meta.filter.data.check', ParticipantFilterScope::$nan);
|
||||
$this->callFilter('form.participant.index', ['data' => []], ['form' => $form])->assertJsonPath('meta.filter.show_cancelled', false);
|
||||
$this->callFilter('form.participant.index', ['show_cancelled' => true], ['form' => $form])->assertJsonPath('meta.filter.show_cancelled', true);
|
||||
$this->callFilter('form.participant.index', ['show_cancelled' => false], ['form' => $form])->assertJsonPath('meta.filter.show_cancelled', false);
|
||||
});
|
||||
|
||||
it('sorts by active colums sorting by default', function (array $sorting, string $by, bool $direction) {
|
||||
|
|
@ -189,22 +186,6 @@ it('testItFiltersParticipantsByRadioValue', function () {
|
|||
->assertJsonCount(4, 'data');
|
||||
});
|
||||
|
||||
it('filters participants by cancelled at', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory()->count(1))
|
||||
->has(Participant::factory()->cancelled()->count(2))
|
||||
->create();
|
||||
|
||||
sleep(2);
|
||||
$this->callFilter('form.participant.index', [], ['form' => $form])
|
||||
->assertJsonCount(1, 'data');
|
||||
$this->callFilter('form.participant.index', ['show_cancelled' => false], ['form' => $form])
|
||||
->assertJsonCount(1, 'data');
|
||||
$this->callFilter('form.participant.index', ['show_cancelled' => true], ['form' => $form])
|
||||
->assertJsonCount(2, 'data');
|
||||
});
|
||||
|
||||
it('testItPresentsNamiField', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
|
|
@ -288,15 +269,6 @@ it('testItShowsPreventionState', function () {
|
|||
->assertJsonPath('data.0.prevention_items.0.tooltip', 'erweitertes Führungszeugnis nicht vorhanden');
|
||||
});
|
||||
|
||||
it('doesnt show cancelled participants', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$participant = Participant::factory()->for(Form::factory())->create(['cancelled_at' => now()]);
|
||||
|
||||
sleep(2);
|
||||
$this->callFilter('form.participant.index', [], ['form' => $participant->form])
|
||||
->assertJsonCount(0, 'data');
|
||||
});
|
||||
|
||||
it('test it orders participants by value', function (array $values, array $sorting, array $expected) {
|
||||
list($key, $direction) = $sorting;
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
|
|
|
|||
|
|
@ -248,17 +248,6 @@ it('notices a few weeks before', function ($date, bool $shouldSend) {
|
|||
[fn() => now()->subYears(5)->addWeeks(2)->subDay(), false],
|
||||
]);
|
||||
|
||||
it('sets reply to mail', function () {
|
||||
Mail::fake();
|
||||
app(PreventionSettings::class)->fill(['replyToMail' => 'admin@example.com'])->save();
|
||||
createMember(['has_vk' => false]);
|
||||
|
||||
sleep(2);
|
||||
YearlyRememberAction::run();
|
||||
|
||||
Mail::assertSent(YearlyMail::class, fn ($message) => $message->hasReplyTo('admin@example.com'));
|
||||
});
|
||||
|
||||
it('remembers members yearly', function ($date, $shouldSend) {
|
||||
Mail::fake();
|
||||
createMember(['efz' => $date, 'ps_at' => now(), 'has_vk' => true]);
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Form;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\Lib\CreatesFormFields;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(CreatesFormFields::class);
|
||||
|
||||
beforeEach(function () {
|
||||
test()->fakeMessages();
|
||||
test()->setUpForm();
|
||||
});
|
||||
|
||||
it('deletes all participants of a form', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory()->count(2))
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
||||
it('does not delete the form itself', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseHas('forms', ['id' => $form->id]);
|
||||
});
|
||||
|
||||
it('does not delete participants of other forms', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
$otherForm = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 1);
|
||||
test()->assertDatabaseHas('participants', ['id' => $otherForm->participants->first()->id]);
|
||||
});
|
||||
|
||||
it('deletes participants that are children of other participants', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->sections([])->create();
|
||||
$parent = Participant::factory()->for($form)->create();
|
||||
Participant::factory()->for($form)->create(['parent_id' => $parent->id]);
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
||||
it('clears the frontend cache', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertFrontendCacheCleared();
|
||||
});
|
||||
|
||||
it('shows success message', function () {
|
||||
test()->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
test()->deleteJson(route('form.cleanup', ['form' => $form]))
|
||||
->assertOk();
|
||||
|
||||
test()->assertSuccessMessage('Teilnehmende gelöscht.');
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Form;
|
||||
|
||||
use App\Form\FormSettings;
|
||||
use App\Form\Models\Form;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\Lib\CreatesFormFields;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(CreatesFormFields::class);
|
||||
|
||||
beforeEach(function () {
|
||||
test()->setUpForm();
|
||||
Mail::fake();
|
||||
});
|
||||
|
||||
it('generates a later link', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
app(FormSettings::class)->fill(['registerUrl' => 'https://example.com/register/{slug}'])->save();
|
||||
$form = Form::factory()->name('fff')->create();
|
||||
|
||||
$url = $this->get(route('form.laterlink', ['form' => $form]))->json('url');
|
||||
test()->assertNotNull($url);
|
||||
$this->assertTrue(str($url)->startsWith('https://example.com/register/fff'));
|
||||
|
||||
$query = data_get(parse_url($url), 'query');
|
||||
parse_str($query, $queryParts);
|
||||
$this->assertEquals('1', $queryParts['later']);
|
||||
|
||||
$this->assertEquals($form->id, Cache::get('later_'.$queryParts['id']));
|
||||
});
|
||||
|
||||
|
|
@ -4,7 +4,6 @@ namespace Tests\Feature\Form;
|
|||
|
||||
use App\Form\Enums\NamiType;
|
||||
use App\Form\Enums\SpecialType;
|
||||
use App\Form\FormSettings;
|
||||
use App\Form\Mails\ConfirmRegistrationMail;
|
||||
use App\Form\Models\Form;
|
||||
use App\Group;
|
||||
|
|
@ -12,7 +11,6 @@ use App\Group\Enums\Level;
|
|||
use Carbon\Carbon;
|
||||
use Database\Factories\Member\MemberFactory;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\Lib\CreatesFormFields;
|
||||
|
||||
|
|
@ -272,14 +270,6 @@ it('testItSavesParticipantAsModel', function () {
|
|||
$this->assertEquals('Abraham', $participants->first()->data['spitzname']);
|
||||
});
|
||||
|
||||
|
||||
it('cannot register when event is inactive', function () {
|
||||
$this->login()->loginNami();
|
||||
$form = Form::factory()->isActive(false)->create();
|
||||
|
||||
$this->register($form, [])->assertJsonValidationErrors(['event' => 'Anmeldung zzt nicht möglich.']);
|
||||
});
|
||||
|
||||
it('testItCannotRegisterWhenRegistrationFromReached', function () {
|
||||
$this->login()->loginNami();
|
||||
$form = Form::factory()->registrationFrom(now()->addDay())->create();
|
||||
|
|
@ -309,22 +299,6 @@ it('testItSendsEmailToParticipant', function () {
|
|||
Mail::assertQueued(ConfirmRegistrationMail::class, fn($message) => $message->hasTo('example@test.test', 'Lala GG') && $message->hasSubject('Deine Anmeldung zu Ver2'));
|
||||
});
|
||||
|
||||
it('sets reply to in email', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
app(FormSettings::class)->fill(['replyToMail' => 'reply@example.com'])->save();
|
||||
$form = Form::factory()->name('Ver2')->fields([
|
||||
$this->textField('vorname')->specialType(SpecialType::FIRSTNAME),
|
||||
$this->textField('nachname')->specialType(SpecialType::LASTNAME),
|
||||
$this->textField('email')->specialType(SpecialType::EMAIL),
|
||||
])
|
||||
->create();
|
||||
|
||||
$this->register($form, ['vorname' => 'Lala', 'nachname' => 'GG', 'email' => 'example@test.test'])
|
||||
->assertOk();
|
||||
|
||||
Mail::assertQueued(ConfirmRegistrationMail::class, fn($message) => $message->hasReplyTo('reply@example.com'));
|
||||
});
|
||||
|
||||
it('testItDoesntSendEmailWhenNoMailFieldGiven', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->fields([
|
||||
|
|
@ -753,39 +727,3 @@ it('testItSetsRegionIfMemberIsDirectRegionMember', function () {
|
|||
$this->register($form, ['bezirk' => $bezirk->id, 'members' => [['id' => '5505']]])->assertOk();
|
||||
$this->assertEquals($bezirk->id, $form->participants->get(1)->data['bezirk']);
|
||||
});
|
||||
|
||||
it('registers via later link', function () {
|
||||
$this->login()->loginNami();
|
||||
$laterId = str()->uuid()->toString();
|
||||
$form = Form::factory()->fields([])
|
||||
->registrationUntil(now()->subDay())
|
||||
->create();
|
||||
Cache::set('later_'.$laterId, $form->id);
|
||||
|
||||
$this->registerLater($form, [], $laterId)->assertOk();
|
||||
$this->assertDatabaseCount('participants', 1);
|
||||
$this->assertNull(Cache::get('later_'.$laterId));
|
||||
});
|
||||
|
||||
it('checks signature of later link', function () {
|
||||
$this->login()->loginNami();
|
||||
$form = Form::factory()->fields([])
|
||||
->registrationUntil(now()->subDay())
|
||||
->create();
|
||||
|
||||
$this->registerLaterWithWrongSignature($form, [], str()->uuid())->assertStatus(422);
|
||||
$this->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
||||
it('checks if later links is from current form', function () {
|
||||
$this->login()->loginNami();
|
||||
$foreignForm = Form::factory()->create();
|
||||
$form = Form::factory()->fields([])
|
||||
->registrationUntil(now()->subDay())
|
||||
->create();
|
||||
$laterId = str()->uuid()->toString();
|
||||
Cache::set('later_'.$laterId, $foreignForm->id);
|
||||
|
||||
$this->registerLater($form, [], $laterId)->assertStatus(422);
|
||||
$this->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ beforeEach(function () {
|
|||
test()->setUpForm();
|
||||
});
|
||||
|
||||
it('cancels a participant', function () {
|
||||
it('testItCanDestroyAParticipant', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
|
|
@ -24,22 +24,5 @@ it('cancels a participant', function () {
|
|||
$this->deleteJson(route('participant.destroy', ['participant' => $form->participants->first()]))
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('participants', 1);
|
||||
$this->assertDatabaseHas('participants', [
|
||||
'cancelled_at' => now(),
|
||||
'id' => $form->participants->first()->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('testItCanDestroyAParticipant', function () {
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory())
|
||||
->sections([])
|
||||
->create();
|
||||
|
||||
$this->deleteJson(route('participant.destroy', ['participant' => $form->participants->first()]), [], ['X-Force' => '1'])
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseCount('participants', 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
namespace Tests\Feature\Form;
|
||||
|
||||
use DB;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
|
@ -21,14 +20,15 @@ it('testItShowsParticipantsAndColumns', function () {
|
|||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()
|
||||
->has(Participant::factory()->data(['stufe' => 'Pfadfinder', 'vorname' => 'Max', 'select' => ['A', 'B']]))
|
||||
->fields([
|
||||
$this->textField('vorname')->name('Vorname'),
|
||||
$this->checkboxesField('select')->name('Abcselect')->options(['A', 'B', 'C']),
|
||||
$this->dropdownField('stufe')->name('Stufe')->options(['Wölfling', 'Jungpfadfinder', 'Pfadfinder']),
|
||||
->sections([
|
||||
FormtemplateSectionRequest::new()->fields([
|
||||
$this->textField('vorname')->name('Vorname'),
|
||||
$this->checkboxesField('select')->name('Abcselect')->options(['A', 'B', 'C']),
|
||||
$this->dropdownField('stufe')->name('Stufe')->options(['Wölfling', 'Jungpfadfinder', 'Pfadfinder']),
|
||||
]),
|
||||
])
|
||||
->name('ZEM 2024')
|
||||
->create();
|
||||
DB::table('participants')->where('id', $form->participants->first()->id)->update(['id' => 9909]);
|
||||
|
||||
$this->get(route('form.export', ['form' => $form]))->assertDownload('tn-zem-2024.xlsx');
|
||||
$contents = Storage::disk('temp')->get('tn-zem-2024.xlsx');
|
||||
|
|
@ -37,17 +37,4 @@ it('testItShowsParticipantsAndColumns', function () {
|
|||
$this->assertExcelContent('Pfadfinder', $contents);
|
||||
$this->assertExcelContent('Stufe', $contents);
|
||||
$this->assertExcelContent('Abcselect', $contents);
|
||||
$this->assertExcelContent('9909', $contents);
|
||||
});
|
||||
|
||||
it('shows cancelled at', function () {
|
||||
Storage::fake('temp');
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->name('ZEM 2024')
|
||||
->has(Participant::factory()->state(['cancelled_at' => now()->subWeek()]))
|
||||
->create();
|
||||
|
||||
$this->get(route('form.export', ['form' => $form]))->assertDownload('tn-zem-2024.xlsx');
|
||||
$contents = Storage::disk('temp')->get('tn-zem-2024.xlsx');
|
||||
$this->assertExcelContent(now()->subWeek()->format('d.m.Y'), $contents);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class InvoiceSendActionTest extends TestCase
|
|||
Tex::spy();
|
||||
Storage::fake('temp');
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
app(InvoiceSettings::class)->fill(['from_long' => 'Stammname', 'replyTo' => 'reply@mail.com'])->save();
|
||||
$invoice = Invoice::factory()
|
||||
->to(ReceiverRequestFactory::new()->name('Familie Muster'))
|
||||
->has(InvoicePosition::factory()->withMember(), 'positions')
|
||||
|
|
@ -58,13 +57,7 @@ class InvoiceSendActionTest extends TestCase
|
|||
|
||||
InvoiceSendAction::run();
|
||||
|
||||
Mail::assertSent(RememberMail::class, fn ($mail) => $mail->build()
|
||||
&& $mail->hasTo('max@muster.de', 'Familie Muster')
|
||||
&& $mail->hasSubject('Zahlungserinnerung | Stammname')
|
||||
&& Storage::disk('temp')->path('zahlungserinnerung-fur-familie-muster.pdf') === $mail->filename
|
||||
&& Storage::disk('temp')->exists('zahlungserinnerung-fur-familie-muster.pdf')
|
||||
&& $mail->hasReplyTo('reply@mail.com')
|
||||
);
|
||||
Mail::assertSent(RememberMail::class, fn ($mail) => $mail->build() && $mail->hasTo('max@muster.de', 'Familie Muster') && Storage::disk('temp')->path('zahlungserinnerung-fur-familie-muster.pdf') === $mail->filename && Storage::disk('temp')->exists('zahlungserinnerung-fur-familie-muster.pdf'));
|
||||
Tex::assertCompiled(RememberDocument::class, fn ($document) => 'Familie Muster' === $document->toName);
|
||||
$this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->last_remembered_at->format('Y-m-d'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ class SettingTest extends TestCase
|
|||
'zip' => '12345',
|
||||
'iban' => 'DE05',
|
||||
'bic' => 'SOLSDE',
|
||||
'rememberWeeks' => 6,
|
||||
'replyTo' => 'reply@example.com',
|
||||
'rememberWeeks' => 6
|
||||
])->save();
|
||||
|
||||
$this->get(route('setting.data', ['settingGroup' => 'bill']))
|
||||
|
|
@ -50,8 +49,7 @@ class SettingTest extends TestCase
|
|||
->assertInertiaPath('data.zip', '12345')
|
||||
->assertInertiaPath('data.iban', 'DE05')
|
||||
->assertInertiaPath('data.bic', 'SOLSDE')
|
||||
->assertInertiaPath('data.rememberWeeks', 6)
|
||||
->assertInertiaPath('data.replyTo', 'reply@example.com');
|
||||
->assertInertiaPath('data.rememberWeeks', 6);
|
||||
}
|
||||
|
||||
public function testItReturnsTabs(): void
|
||||
|
|
@ -80,8 +78,7 @@ class SettingTest extends TestCase
|
|||
'zip' => '12345',
|
||||
'iban' => 'DE05',
|
||||
'bic' => 'SOLSDE',
|
||||
'rememberWeeks' => 10,
|
||||
'replyTo' => 'reply@example.com2',
|
||||
'rememberWeeks' => 10
|
||||
]);
|
||||
|
||||
$response->assertRedirect('/setting/bill');
|
||||
|
|
@ -89,7 +86,6 @@ class SettingTest extends TestCase
|
|||
$this->assertEquals('DPSG Stamm Muster', $settings->from_long);
|
||||
$this->assertEquals('DE05', $settings->iban);
|
||||
$this->assertEquals('SOLSDE', $settings->bic);
|
||||
$this->assertEquals('reply@example.com2', $settings->replyTo);
|
||||
$this->assertEquals(10, $settings->rememberWeeks);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,18 +157,6 @@ it('testItSetsLocationToNull', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
it('updates work phone', function () {
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$member = factory()->notInNami()->create();
|
||||
fakeRequest();
|
||||
NamiPutMemberAction::allowToRun();
|
||||
|
||||
$this->patch("/member/{$member->id}", MemberUpdateRequestFactory::new()->noNami()->create([
|
||||
'work_phone' => '+49 212 1353688',
|
||||
]));
|
||||
test()->assertDatabaseHas('members', ['work_phone' => '+49 212 1353688']);
|
||||
});
|
||||
|
||||
it('testItUpdatesContact', function () {
|
||||
$this->withoutExceptionHandling()->login()->loginNami();
|
||||
$member = factory()->notInNami()->create();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace Tests\Feature\Nami;
|
||||
|
||||
use App\Invoice\InvoiceSettings;
|
||||
use App\Setting\NamiSettings;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\TestCase;
|
||||
use Zoomyboy\LaravelNami\Authentication\Auth;
|
||||
use Zoomyboy\LaravelNami\Nami;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ it('receives settings', function () {
|
|||
'weeks' => 9,
|
||||
'freshRememberInterval' => 11,
|
||||
'active' => true,
|
||||
'replyToMail' => 'admin@example.com',
|
||||
'preventAgainst' => [Prevention::MOREPS->name],
|
||||
'yearlyMemberFilter' => FilterScope::from([
|
||||
'memberships' => [['group_ids' => [33]]],
|
||||
|
|
@ -42,7 +41,6 @@ it('receives settings', function () {
|
|||
->assertJsonPath('data.weeks', '9')
|
||||
->assertJsonPath('data.active', true)
|
||||
->assertJsonPath('data.freshRememberInterval', '11')
|
||||
->assertJsonPath('data.replyToMail', 'admin@example.com')
|
||||
->assertJsonPath('data.yearlyMemberFilter.search', 'searchstring')
|
||||
->assertJsonPath('data.yearlyMemberFilter.memberships.0.group_ids.0', 33)
|
||||
->assertJsonPath('data.preventAgainst', ['MOREPS'])
|
||||
|
|
@ -60,7 +58,6 @@ it('testItStoresSettings', function () {
|
|||
'freshRememberInterval' => 11,
|
||||
'active' => true,
|
||||
'preventAgainst' => ['EFZ'],
|
||||
'replyToMail' => 'admin@example.com',
|
||||
'yearlyMemberFilter' => [
|
||||
'memberships' => [['group_ids' => 33]],
|
||||
'search' => 'searchstring',
|
||||
|
|
@ -69,7 +66,6 @@ it('testItStoresSettings', function () {
|
|||
test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
|
||||
test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd']));
|
||||
test()->assertEquals(9, app(PreventionSettings::class)->weeks);
|
||||
test()->assertEquals('admin@example.com', app(PreventionSettings::class)->replyToMail);
|
||||
test()->assertEquals(11, app(PreventionSettings::class)->freshRememberInterval);
|
||||
test()->assertTrue(app(PreventionSettings::class)->active);
|
||||
test()->assertEquals([['group_ids' => 33]], app(PreventionSettings::class)->yearlyMemberFilter->memberships);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ services:
|
|||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./oc_tmp/files:/mnt/data
|
||||
- ./08-skeleton.sh:/etc/entrypoint.d/08-skeleton.sh
|
||||
|
||||
mariadb:
|
||||
|
|
@ -43,6 +44,8 @@ services:
|
|||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./oc_tmp/db:/var/lib/mysql
|
||||
|
||||
nextclouddb:
|
||||
image: mariadb:10.6
|
||||
|
|
@ -52,6 +55,8 @@ services:
|
|||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- ./oc_tmp/ncdb:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=secret
|
||||
- MYSQL_PASSWORD=secret
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ use App\Form\Models\Form;
|
|||
use App\Member\Member;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Tests\Feature\Form\FormtemplateFieldRequest;
|
||||
|
||||
|
|
@ -43,22 +42,6 @@ trait CreatesFormFields
|
|||
return $this->postJson(route('form.register', ['form' => $form]), $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function registerLater(Form $form, array $payload, string $laterId): TestResponse
|
||||
{
|
||||
return $this->postJson(URL::signedRoute('form.register', ['form' => $form, 'later' => '1', 'id' => $laterId]), $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function registerLaterWithWrongSignature(Form $form, array $payload, string $laterId): TestResponse
|
||||
{
|
||||
return $this->postJson(route('form.register', ['form' => $form, 'later' => '1', 'id' => $laterId, 'signature' => '-1']), $payload);
|
||||
}
|
||||
|
||||
public function setUpForm() {
|
||||
app(FormSettings::class)->fill(['clearCacheUrl' => 'http://event.com/clear-cache'])->save();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Illuminate\Testing;
|
||||
|
||||
namespace Spatie\LaravelSettings;
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue