Compare commits

..

No commits in common. "master" and "form-contribution" have entirely different histories.

45 changed files with 60 additions and 540 deletions

View File

@ -1,27 +1,5 @@
# Letzte Änderungen # Letzte Änderungen
### 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 ### 1.12.18
- Fix: Initialisierung klappt nun auch, wenn Mitgliedsnummer mit einer 0 beginnt - Fix: Initialisierung klappt nun auch, wenn Mitgliedsnummer mit einer 0 beginnt

View File

@ -31,27 +31,25 @@ class CreateExcelDocumentAction
private function allSheet(Collection $participants): TableDocumentData private function allSheet(Collection $participants): TableDocumentData
{ {
$document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]); $document = TableDocumentData::from(['title' => 'Anmeldungen für ' . $this->form->name, 'sheets' => []]);
$headers = $this->form->getFields()->names()->push('Abgemeldet am')->prepend('ID')->toArray(); $headers = $this->form->getFields()->map(fn ($field) => $field->name)->toArray();
[$activeParticipants, $cancelledParticipants] = $participants->partition(fn ($participant) => $participant->cancelled_at === null);
$document->addSheet(SheetData::from([ $document->addSheet(SheetData::from([
'header' => $headers, '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', 'name' => 'Alle',
])); ]));
$document->addSheet(SheetData::from([
'header' => $headers,
'data' => $this->rowsFor($cancelledParticipants),
'name' => 'Abgemeldet',
]));
if ($this->form->export->groupBy) { 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([ $document->addSheet(SheetData::from([
'header' => $headers, '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, 'name' => $name,
])); ]));
} }
@ -66,17 +64,6 @@ class CreateExcelDocumentAction
return $document; 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 private function tempPath(): string
{ {
return sys_get_temp_dir() . '/' . str()->uuid()->toString(); return sys_get_temp_dir() . '/' . str()->uuid()->toString();

View File

@ -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')
]);
}
}

View File

@ -20,7 +20,7 @@ class FormIndexAction
*/ */
public function handle(string $filter): LengthAwarePaginator 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 public function asController(ActionRequest $request): Response

View File

@ -6,7 +6,6 @@ use App\Form\Models\Participant;
use App\Lib\JobMiddleware\JobChannels; use App\Lib\JobMiddleware\JobChannels;
use App\Lib\JobMiddleware\WithJobState; use App\Lib\JobMiddleware\WithJobState;
use App\Lib\Queue\TracksJob; use App\Lib\Queue\TracksJob;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class ParticipantDestroyAction class ParticipantDestroyAction
@ -14,20 +13,14 @@ class ParticipantDestroyAction
use AsAction; use AsAction;
use TracksJob; use TracksJob;
public function handle(int $participantId, bool $force): void public function handle(int $participantId): void
{ {
$participant = Participant::findOrFail($participantId); Participant::findOrFail($participantId)->delete();
if ($force) {
$participant->delete();
} else {
$participant->update(['cancelled_at' => now()]);
}
} }
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);
} }
/** /**

View File

@ -7,9 +7,6 @@ use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Member\Member; use App\Member\Member;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@ -23,6 +20,10 @@ class RegisterAction
*/ */
public function handle(Form $form, array $input): Participant 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) $memberQuery = FieldCollection::fromRequest($form, $input)
->withNamiType() ->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());
@ -76,30 +77,8 @@ class RegisterAction
public function asController(ActionRequest $request, Form $form): JsonResponse 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()); $participant = $this->handle($form, $request->validated());
if ($this->isRegisteringLater($request, $form)) {
Cache::forget('later_'.request('id'));
}
return response()->json($participant); 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;
}
} }

View File

@ -18,10 +18,10 @@ class UpdateParticipantSearchIndexAction
$form->searchableUsing()->updateIndexSettings( $form->searchableUsing()->updateIndexSettings(
$form->participantsSearchableAs(), $form->participantsSearchableAs(),
[ [
'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id', 'cancelled_at'], 'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'],
'searchableAttributes' => $form->getFields()->searchables()->getKeys(), 'searchableAttributes' => $form->getFields()->searchables()->getKeys(),
'sortableAttributes' => [...$form->getFields()->sortables()->getKeys(), 'id', 'created_at'], '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' => [ 'pagination' => [
'maxTotalHits' => 1000000, 'maxTotalHits' => 1000000,
] ]

View File

@ -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 public function hasSpecialType(SpecialType $specialType): bool

View File

@ -2,6 +2,7 @@
namespace App\Form; namespace App\Form;
use App\Form\Actions\SettingStoreAction;
use App\Setting\Contracts\Storeable; use App\Setting\Contracts\Storeable;
use App\Setting\LocalSettings; use App\Setting\LocalSettings;
use Lorisleiva\Actions\ActionRequest; use Lorisleiva\Actions\ActionRequest;
@ -10,7 +11,6 @@ class FormSettings extends LocalSettings implements Storeable
{ {
public string $registerUrl; public string $registerUrl;
public string $clearCacheUrl; public string $clearCacheUrl;
public ?string $replyToMail;
public static function group(): string public static function group(): string
{ {
@ -19,7 +19,7 @@ class FormSettings extends LocalSettings implements Storeable
public static function title(): string public static function title(): string
{ {
return 'Veranstaltungen'; return 'Formulare';
} }
/** /**
@ -30,7 +30,6 @@ class FormSettings extends LocalSettings implements Storeable
return [ return [
'registerUrl' => 'present|string', 'registerUrl' => 'present|string',
'clearCacheUrl' => 'present|string', 'clearCacheUrl' => 'present|string',
'replyToMail' => 'nullable|string|email',
]; ];
} }
@ -48,7 +47,6 @@ class FormSettings extends LocalSettings implements Storeable
'data' => [ 'data' => [
'registerUrl' => $this->registerUrl, 'registerUrl' => $this->registerUrl,
'clearCacheUrl' => $this->clearCacheUrl, 'clearCacheUrl' => $this->clearCacheUrl,
'replyToMail' => $this->replyToMail,
] ]
] ]
]; ];

View File

@ -4,7 +4,6 @@ namespace App\Form\Mails;
use App\Form\Data\FormConfigData; use App\Form\Data\FormConfigData;
use App\Form\Editor\FormConditionResolver; use App\Form\Editor\FormConditionResolver;
use App\Form\FormSettings;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use App\Lib\Editor\Condition; use App\Lib\Editor\Condition;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@ -25,8 +24,6 @@ class ConfirmRegistrationMail extends Mailable
/** @var array<int, mixed> */ /** @var array<int, mixed> */
public array $bottomText; public array $bottomText;
public FormSettings $formSettings;
/** /**
* Create a new message instance. * Create a new message instance.
* *
@ -35,7 +32,6 @@ class ConfirmRegistrationMail extends Mailable
public function __construct(public Participant $participant) public function __construct(public Participant $participant)
{ {
$conditionResolver = app(FormConditionResolver::class)->forParticipant($participant); $conditionResolver = app(FormConditionResolver::class)->forParticipant($participant);
$this->formSettings = app(FormSettings::class);
$this->fullname = $participant->getFields()->getFullname(); $this->fullname = $participant->getFields()->getFullname();
$this->config = $participant->getConfig(); $this->config = $participant->getConfig();
$this->topText = $conditionResolver->makeBlocks($participant->form->mail_top); $this->topText = $conditionResolver->makeBlocks($participant->form->mail_top);
@ -49,15 +45,9 @@ class ConfirmRegistrationMail extends Mailable
*/ */
public function envelope() public function envelope()
{ {
$envelope = new Envelope( return new Envelope(
subject: 'Deine Anmeldung zu ' . $this->participant->form->name, subject: 'Deine Anmeldung zu ' . $this->participant->form->name,
); );
if ($this->formSettings->replyToMail !== null) {
$envelope->replyTo($this->formSettings->replyToMail);
}
return $envelope;
} }
/** /**

View File

@ -190,7 +190,8 @@ class Form extends Model implements HasMedia
return Sorting::from($this->meta['sorting']); return Sorting::from($this->meta['sorting']);
} }
public function isInDates(): bool { public function canRegister(): bool
{
if ($this->registration_from && $this->registration_from->gt(now())) { if ($this->registration_from && $this->registration_from->gt(now())) {
return false; return false;
} }
@ -201,9 +202,4 @@ class Form extends Model implements HasMedia
return true; return true;
} }
public function canRegister(): bool
{
return $this->is_active && $this->isInDates();
}
} }

View File

@ -31,7 +31,6 @@ class Participant extends Model implements Preventable
public $casts = [ public $casts = [
'data' => 'json', 'data' => 'json',
'last_remembered_at' => 'datetime', 'last_remembered_at' => 'datetime',
'cancelled_at' => 'datetime',
]; ];
/** /**
@ -109,12 +108,7 @@ class Participant extends Model implements Preventable
/** @return array<string, mixed> */ /** @return array<string, mixed> */
public function toSearchableArray(): array public function toSearchableArray(): array
{ {
return [ return [...$this->data, 'parent-id' => $this->parent_id, 'created_at' => $this->created_at->timestamp];
...$this->data,
'parent-id' => $this->parent_id,
'created_at' => $this->created_at->timestamp,
'cancelled_at' => $this->cancelled_at
];
} }
public function matchesCondition(Condition $condition): bool { public function matchesCondition(Condition $condition): bool {

View File

@ -46,7 +46,6 @@ class FormResource extends JsonResource
'mail_bottom' => $this->mail_bottom, 'mail_bottom' => $this->mail_bottom,
'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'), 'registration_from' => $this->registration_from?->format('Y-m-d H:i:s'),
'registration_until' => $this->registration_until?->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, 'config' => $this->config,
'participants_count' => $this->participants_count, 'participants_count' => $this->participants_count,
'is_active' => $this->is_active, 'is_active' => $this->is_active,
@ -70,7 +69,6 @@ class FormResource extends JsonResource
'export' => route('form.export', $this->getModel()), 'export' => route('form.export', $this->getModel()),
'copy' => route('form.copy', $this->getModel()), 'copy' => route('form.copy', $this->getModel()),
'contribution' => route('form.contribution', $this->getModel()), 'contribution' => route('form.contribution', $this->getModel()),
'laterlink' => route('form.laterlink', $this->getModel()),
] ]
]; ];
} }

View File

@ -32,8 +32,7 @@ class ParticipantFilterScope extends ScoutFilter
public string $search = '', public string $search = '',
public array $options = [], public array $options = [],
public ?int $parent = null, public ?int $parent = null,
public ?Sorting $sort = null, public ?Sorting $sort = null
public bool $showCancelled = false,
) { ) {
} }
@ -55,12 +54,6 @@ class ParticipantFilterScope extends ScoutFilter
$filter->push('parent-id IS NULL'); $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) { if ($this->parent !== null && $this->parent !== -1) {
$filter->push('parent-id = ' . $this->parent); $filter->push('parent-id = ' . $this->parent);
} }

View File

@ -75,7 +75,6 @@ class MemberRequest extends FormRequest
'send_newspaper' => 'boolean', 'send_newspaper' => 'boolean',
'main_phone' => ['nullable', new ValidPhoneRule('Telefon (Eltern)')], 'main_phone' => ['nullable', new ValidPhoneRule('Telefon (Eltern)')],
'mobile_phone' => ['nullable', new ValidPhoneRule('Handy (Eltern)')], 'mobile_phone' => ['nullable', new ValidPhoneRule('Handy (Eltern)')],
'work_phone' => ['nullable', new ValidPhoneRule('Tel geschäftlich')],
'invoice_address' => '', 'invoice_address' => '',
'gender_id' => 'nullable|exists:genders,id', 'gender_id' => 'nullable|exists:genders,id',
'region_id' => 'nullable|exists:regions,id', 'region_id' => 'nullable|exists:regions,id',

View File

@ -24,7 +24,6 @@ class SettingStoreAction
'weeks' => 'required|numeric|gte:0', 'weeks' => 'required|numeric|gte:0',
'freshRememberInterval' => 'required|numeric|gte:0', 'freshRememberInterval' => 'required|numeric|gte:0',
'active' => 'boolean', 'active' => 'boolean',
'replyToMail' => 'nullable|string|email',
]; ];
} }
@ -34,7 +33,6 @@ class SettingStoreAction
$settings->formmail = EditorData::from($request->formmail); $settings->formmail = EditorData::from($request->formmail);
$settings->yearlymail = EditorData::from($request->yearlymail); $settings->yearlymail = EditorData::from($request->yearlymail);
$settings->weeks = $request->weeks; $settings->weeks = $request->weeks;
$settings->replyToMail = $request->replyToMail;
$settings->freshRememberInterval = $request->freshRememberInterval; $settings->freshRememberInterval = $request->freshRememberInterval;
$settings->active = $request->active; $settings->active = $request->active;
$settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter); $settings->yearlyMemberFilter = FilterScope::from($request->yearlyMemberFilter);

View File

@ -6,7 +6,6 @@ use App\Invoice\InvoiceSettings;
use App\Lib\Editor\EditorData; use App\Lib\Editor\EditorData;
use App\Prevention\Contracts\Preventable; use App\Prevention\Contracts\Preventable;
use App\Prevention\Data\PreventionData; use App\Prevention\Data\PreventionData;
use App\Prevention\PreventionSettings;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment; use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
@ -20,7 +19,6 @@ class YearlyMail extends Mailable
use Queueable, SerializesModels; use Queueable, SerializesModels;
public InvoiceSettings $settings; public InvoiceSettings $settings;
public PreventionSettings $preventionSettings;
/** /**
* Create a new message instance. * 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) public function __construct(public Preventable $preventable, public EditorData $bodyText, public Collection $preventions)
{ {
$this->settings = app(InvoiceSettings::class); $this->settings = app(InvoiceSettings::class);
$this->preventionSettings = app(PreventionSettings::class);
$this->bodyText = $this->bodyText $this->bodyText = $this->bodyText
->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray()); ->replaceWithList('wanted', $preventions->map(fn($prevention) => $prevention->text())->toArray());
} }
@ -41,15 +38,9 @@ class YearlyMail extends Mailable
*/ */
public function envelope() public function envelope()
{ {
$envelope = (new Envelope( return (new Envelope(
subject: $this->preventable->preventableSubject(), subject: $this->preventable->preventableSubject(),
))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name); ))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name);
if ($this->preventionSettings->replyToMail !== null) {
$envelope->replyTo($this->preventionSettings->replyToMail);
}
return $envelope;
} }
/** /**

View File

@ -15,7 +15,6 @@ class PreventionSettings extends LocalSettings
public int $freshRememberInterval; public int $freshRememberInterval;
public bool $active; public bool $active;
public FilterScope $yearlyMemberFilter; public FilterScope $yearlyMemberFilter;
public ?string $replyToMail;
/** /**
* @var array<int, string> * @var array<int, string>
* @todo Create collection cast to Collection of enums * @todo Create collection cast to Collection of enums
@ -50,7 +49,6 @@ class PreventionSettings extends LocalSettings
...$this->toArray(), ...$this->toArray(),
'weeks' => (string) $this->weeks, 'weeks' => (string) $this->weeks,
'freshRememberInterval' => (string) $this->freshRememberInterval, 'freshRememberInterval' => (string) $this->freshRememberInterval,
'replyToMail' => $this->replyToMail,
]; ];
} }
} }

View File

@ -39,10 +39,6 @@ class ParticipantFactory extends Factory
return $this->state(['data' => $data]); return $this->state(['data' => $data]);
} }
public function cancelled(): self {
return $this->state(['cancelled_at' => now()->subWeek()]);
}
public function nr(int $number): self public function nr(int $number): self
{ {
return $this->state(['member_id' => $number]); return $this->state(['member_id' => $number]);

View File

@ -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');
});
}
};

View File

@ -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', '');
}
};

42
package-lock.json generated
View File

@ -30,7 +30,6 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "^4.5.2", "vite": "^4.5.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"wnumb": "^1.2.0" "wnumb": "^1.2.0"
@ -1873,16 +1872,6 @@
"node": ">= 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": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -2151,11 +2140,6 @@
"node": ">=0.4.0" "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": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -2864,14 +2848,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -4099,11 +4075,6 @@
"node": ">=10" "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": { "node_modules/semver": {
"version": "7.7.2", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@ -4574,11 +4545,6 @@
"node": ">=0.8" "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": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -4986,14 +4952,6 @@
"vue": "^3.0.0 || ^2.0.0" "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": { "node_modules/vue-demi": {
"version": "0.14.10", "version": "0.14.10",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",

View File

@ -47,7 +47,6 @@
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "^4.5.2", "vite": "^4.5.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-clipboard3": "^2.0.0",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"wnumb": "^1.2.0" "wnumb": "^1.2.0"

@ -1 +1 @@
Subproject commit f7b04591830ebdeaddf76236e4cbc87a8b3eec8f Subproject commit 7304963370ff64fb5accf08da4864981cc424301

View File

@ -1,7 +1,3 @@
.v-popper__popper {
@apply max-w-lg;
}
.v-popper--theme-tooltip .v-popper__inner { .v-popper--theme-tooltip .v-popper__inner {
@apply bg-primary-400 text-primary-800 px-3 py-1 text-sm; @apply bg-primary-400 text-primary-800 px-3 py-1 text-sm;
} }

View File

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

View File

@ -47,10 +47,8 @@ export function useApiIndex(firstUrl, siteName = null) {
single.value = null; single.value = null;
} }
async function remove(model, force = true) { async function remove(model) {
await axios.delete(model.links.destroy, { await axios.delete(model.links.destroy);
headers: { 'X-Force': force ? '1' : '0' }
});
await reload(); await reload();
} }

View File

@ -23,8 +23,8 @@
<div v-show="active === 0" class="grid grid-cols-4 gap-3"> <div v-show="active === 0" class="grid grid-cols-4 gap-3">
<div class="flex space-x-3 col-span-2"> <div class="flex space-x-3 col-span-2">
<f-text id="name" v-model="single.name" class="grow" label="Name" required /> <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_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" 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_private" v-model="single.is_private" name="is_private" label="Privat" />
</div> </div>
<f-singlefile id="header_image" <f-singlefile id="header_image"
v-model="single.header_image" v-model="single.header_image"
@ -41,7 +41,7 @@
<f-text id="zip" v-model="single.zip" label="PLZ" /> <f-text id="zip" v-model="single.zip" label="PLZ" />
<f-text id="location" v-model="single.location" label="Ort" /> <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-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-text id="registration_until" v-model="single.registration_until" type="datetime-local" label="Registrierung bis" required />
<f-textarea id="excerpt" <f-textarea id="excerpt"
v-model="single.excerpt" v-model="single.excerpt"
@ -146,7 +146,6 @@
<th>Name</th> <th>Name</th>
<th>Von</th> <th>Von</th>
<th>Bis</th> <th>Bis</th>
<th>Tags</th>
<th>Anzahl TN</th> <th>Anzahl TN</th>
<th /> <th />
</tr> </tr>
@ -163,13 +162,6 @@
<td> <td>
<div v-text="form.to_human" /> <div v-text="form.to_human" />
</td> </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> <td>
<div v-text="form.participants_count" /> <div v-text="form.participants_count" />
</td> </td>
@ -179,7 +171,6 @@
<ui-action-button tooltip="Teilnehmende anzeigen" class="btn-info" icon="user" @click.prevent="showParticipants(form)" /> <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 :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="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 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 :href="form.links.export" target="_BLANK" tooltip="als Tabellendokument exportieren" class="btn-info" icon="document" />
<ui-action-button tooltip="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)" />
@ -204,14 +195,12 @@ import ConditionsForm from './ConditionsForm.vue';
import { useToast } from 'vue-toastification'; import { useToast } from 'vue-toastification';
import useSwal from '@/stores/swalStore.ts'; import useSwal from '@/stores/swalStore.ts';
import useDownloads from '@/composables/useDownloads.ts'; import useDownloads from '@/composables/useDownloads.ts';
import useClipboard from 'vue-clipboard3';
const props = defineProps(indexProps); 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 axios = inject('axios');
const toast = useToast(); const toast = useToast();
const {download} = useDownloads(); const {download} = useDownloads();
const {toClipboard} = useClipboard();
const showing = ref(null); const showing = ref(null);
const fileSettingPopup = ref(null); const fileSettingPopup = ref(null);
@ -269,12 +258,6 @@ function setTemplate(template) {
single.value.mail_bottom = template.mail_bottom; 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) { async function saveFileConditions(conditions) {
await axios.patch(`/mediaupload/${fileSettingPopup.value.id}`, { await axios.patch(`/mediaupload/${fileSettingPopup.value.id}`, {
properties: { properties: {

View File

@ -11,10 +11,9 @@
<ui-popup v-if="assigning !== null" heading="Mitglied zuweisen" closeable @close="assigning = null"> <ui-popup v-if="assigning !== null" heading="Mitglied zuweisen" closeable @close="assigning = null">
<member-assign @assign="assign" /> <member-assign @assign="assign" />
</ui-popup> </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> <div>
<p class="mt-4">Den*Die Teilnehmer*in abmelden?</p> <p class="mt-4">Den*Die Teilnehmer*in löschen?</p>
<f-switch class="mt-2" v-model="deleting.force" name="force_delete" id="force_delete" label="löschen statt abmelden (permanent)" size="sm" />
<div class="grid grid-cols-2 gap-3 mt-6"> <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-danger" @click.prevent="handleDelete">Mitglied loschen</a>
<a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a> <a href="#" class="text-center btn btn-primary" @click.prevent="deleting = null">Abbrechen</a>
@ -30,7 +29,6 @@
</template> </template>
<template #fields> <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"> <template v-for="(filter, index) in meta.filters">
<f-select v-if="filter.base_type === 'CheckboxField'" <f-select v-if="filter.base_type === 'CheckboxField'"
:id="`filter-field-${index}`" :id="`filter-field-${index}`"
@ -97,7 +95,7 @@
</td> </td>
<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="`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> </td>
</tr> </tr>
<template v-for="child in childrenOf(participant.id)" :key="child.id"> <template v-for="child in childrenOf(participant.id)" :key="child.id">
@ -116,7 +114,7 @@
</td> </td>
<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="`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> </td>
</tr> </tr>
</template> </template>
@ -211,7 +209,7 @@ const sortingConfig = computed({
}); });
async function handleDelete() { async function handleDelete() {
await remove(deleting.value.model, deleting.value.force); await remove(deleting.value);
deleting.value = null; deleting.value = null;
} }

View File

@ -11,7 +11,6 @@
<div class="grid grid-cols-2 gap-4"> <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="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="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> </div>
</form> </form>
</setting-layout> </setting-layout>

View File

@ -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-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-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-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> </div>
</form> </form>
</setting-layout> </setting-layout>

View File

@ -24,7 +24,6 @@ use App\Fileshare\Actions\ListFilesAction;
use App\Form\Actions\ExportAction as ActionsExportAction; use App\Form\Actions\ExportAction as ActionsExportAction;
use App\Form\Actions\FormCopyAction; use App\Form\Actions\FormCopyAction;
use App\Form\Actions\FormDestroyAction; use App\Form\Actions\FormDestroyAction;
use App\Form\Actions\FormGenerateLaterlinkAction;
use App\Form\Actions\FormIndexAction; use App\Form\Actions\FormIndexAction;
use App\Group\Actions\GroupBulkstoreAction; use App\Group\Actions\GroupBulkstoreAction;
use App\Group\Actions\GroupIndexAction; use App\Group\Actions\GroupIndexAction;
@ -181,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}/participant', ParticipantStoreAction::class)->name('form.participant.store');
Route::post('/form/{form}/copy', FormCopyAction::class)->name('form.copy'); Route::post('/form/{form}/copy', FormCopyAction::class)->name('form.copy');
Route::get('/form/{form}/contribution', GenerateContributionAction::class)->name('form.contribution'); Route::get('/form/{form}/contribution', GenerateContributionAction::class)->name('form.contribution');
Route::get('/form/{form}/laterlink', FormGenerateLaterlinkAction::class)->name('form.laterlink');
// ------------------------------------ fileshare ----------------------------------- // ------------------------------------ fileshare -----------------------------------
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store'); Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');

View File

@ -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.participant_index', route('form.participant.index', ['form' => $form]))
->assertInertiaPath('data.data.0.links.export', route('form.export', ['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.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.store', route('form.store'))
->assertInertiaPath('data.meta.links.formtemplate_index', route('formtemplate.index')) ->assertInertiaPath('data.meta.links.formtemplate_index', route('formtemplate.index'))
->assertInertiaPath('data.meta.default.name', '') ->assertInertiaPath('data.meta.default.name', '')
@ -98,18 +97,6 @@ it('testItDisplaysForms', function () {
->assertInertiaPath('data.meta.default.location', ''); ->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 () { it('testFormtemplatesHaveData', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
Formtemplate::factory()->name('tname')->sections([FormtemplateSectionRequest::new()->name('sname')->fields([ 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); $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 () { it('testItOrdersByStartDateDesc', function () {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$form1 = Form::factory()->from(now()->addDays(4))->to(now()->addYear())->create(); $form1 = Form::factory()->from(now()->addDays(4))->to(now()->addYear())->create();

View File

@ -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' => 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' => ['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.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) { 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'); ->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 () { it('testItPresentsNamiField', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory() $form = Form::factory()
@ -288,15 +269,6 @@ it('testItShowsPreventionState', function () {
->assertJsonPath('data.0.prevention_items.0.tooltip', 'erweitertes Führungszeugnis nicht vorhanden'); ->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) { it('test it orders participants by value', function (array $values, array $sorting, array $expected) {
list($key, $direction) = $sorting; list($key, $direction) = $sorting;
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();

View File

@ -248,17 +248,6 @@ it('notices a few weeks before', function ($date, bool $shouldSend) {
[fn() => now()->subYears(5)->addWeeks(2)->subDay(), false], [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) { it('remembers members yearly', function ($date, $shouldSend) {
Mail::fake(); Mail::fake();
createMember(['efz' => $date, 'ps_at' => now(), 'has_vk' => true]); createMember(['efz' => $date, 'ps_at' => now(), 'has_vk' => true]);

View File

@ -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']));
});

View File

@ -4,7 +4,6 @@ namespace Tests\Feature\Form;
use App\Form\Enums\NamiType; use App\Form\Enums\NamiType;
use App\Form\Enums\SpecialType; use App\Form\Enums\SpecialType;
use App\Form\FormSettings;
use App\Form\Mails\ConfirmRegistrationMail; use App\Form\Mails\ConfirmRegistrationMail;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Group; use App\Group;
@ -12,7 +11,6 @@ use App\Group\Enums\Level;
use Carbon\Carbon; use Carbon\Carbon;
use Database\Factories\Member\MemberFactory; use Database\Factories\Member\MemberFactory;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Tests\Lib\CreatesFormFields; use Tests\Lib\CreatesFormFields;
@ -272,14 +270,6 @@ it('testItSavesParticipantAsModel', function () {
$this->assertEquals('Abraham', $participants->first()->data['spitzname']); $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 () { it('testItCannotRegisterWhenRegistrationFromReached', function () {
$this->login()->loginNami(); $this->login()->loginNami();
$form = Form::factory()->registrationFrom(now()->addDay())->create(); $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')); 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 () { it('testItDoesntSendEmailWhenNoMailFieldGiven', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory()->fields([ $form = Form::factory()->fields([
@ -753,39 +727,3 @@ it('testItSetsRegionIfMemberIsDirectRegionMember', function () {
$this->register($form, ['bezirk' => $bezirk->id, 'members' => [['id' => '5505']]])->assertOk(); $this->register($form, ['bezirk' => $bezirk->id, 'members' => [['id' => '5505']]])->assertOk();
$this->assertEquals($bezirk->id, $form->participants->get(1)->data['bezirk']); $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);
});

View File

@ -14,7 +14,7 @@ beforeEach(function () {
test()->setUpForm(); test()->setUpForm();
}); });
it('cancels a participant', function () { it('testItCanDestroyAParticipant', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory() $form = Form::factory()
->has(Participant::factory()) ->has(Participant::factory())
@ -24,22 +24,5 @@ it('cancels a participant', function () {
$this->deleteJson(route('participant.destroy', ['participant' => $form->participants->first()])) $this->deleteJson(route('participant.destroy', ['participant' => $form->participants->first()]))
->assertOk(); ->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); $this->assertDatabaseCount('participants', 0);
}); });

View File

@ -2,7 +2,6 @@
namespace Tests\Feature\Form; namespace Tests\Feature\Form;
use DB;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Participant; use App\Form\Models\Participant;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -21,14 +20,15 @@ it('testItShowsParticipantsAndColumns', function () {
$this->login()->loginNami()->withoutExceptionHandling(); $this->login()->loginNami()->withoutExceptionHandling();
$form = Form::factory() $form = Form::factory()
->has(Participant::factory()->data(['stufe' => 'Pfadfinder', 'vorname' => 'Max', 'select' => ['A', 'B']])) ->has(Participant::factory()->data(['stufe' => 'Pfadfinder', 'vorname' => 'Max', 'select' => ['A', 'B']]))
->fields([ ->sections([
FormtemplateSectionRequest::new()->fields([
$this->textField('vorname')->name('Vorname'), $this->textField('vorname')->name('Vorname'),
$this->checkboxesField('select')->name('Abcselect')->options(['A', 'B', 'C']), $this->checkboxesField('select')->name('Abcselect')->options(['A', 'B', 'C']),
$this->dropdownField('stufe')->name('Stufe')->options(['Wölfling', 'Jungpfadfinder', 'Pfadfinder']), $this->dropdownField('stufe')->name('Stufe')->options(['Wölfling', 'Jungpfadfinder', 'Pfadfinder']),
]),
]) ])
->name('ZEM 2024') ->name('ZEM 2024')
->create(); ->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'); $this->get(route('form.export', ['form' => $form]))->assertDownload('tn-zem-2024.xlsx');
$contents = Storage::disk('temp')->get('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('Pfadfinder', $contents);
$this->assertExcelContent('Stufe', $contents); $this->assertExcelContent('Stufe', $contents);
$this->assertExcelContent('Abcselect', $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);
}); });

View File

@ -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 () { it('testItUpdatesContact', function () {
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
$member = factory()->notInNami()->create(); $member = factory()->notInNami()->create();

View File

@ -2,9 +2,12 @@
namespace Tests\Feature\Nami; namespace Tests\Feature\Nami;
use App\Invoice\InvoiceSettings;
use App\Setting\NamiSettings; use App\Setting\NamiSettings;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
use Zoomyboy\LaravelNami\Authentication\Auth; use Zoomyboy\LaravelNami\Authentication\Auth;
use Zoomyboy\LaravelNami\Nami;
uses(DatabaseTransactions::class); uses(DatabaseTransactions::class);

View File

@ -28,7 +28,6 @@ it('receives settings', function () {
'weeks' => 9, 'weeks' => 9,
'freshRememberInterval' => 11, 'freshRememberInterval' => 11,
'active' => true, 'active' => true,
'replyToMail' => 'admin@example.com',
'preventAgainst' => [Prevention::MOREPS->name], 'preventAgainst' => [Prevention::MOREPS->name],
'yearlyMemberFilter' => FilterScope::from([ 'yearlyMemberFilter' => FilterScope::from([
'memberships' => [['group_ids' => [33]]], 'memberships' => [['group_ids' => [33]]],
@ -42,7 +41,6 @@ it('receives settings', function () {
->assertJsonPath('data.weeks', '9') ->assertJsonPath('data.weeks', '9')
->assertJsonPath('data.active', true) ->assertJsonPath('data.active', true)
->assertJsonPath('data.freshRememberInterval', '11') ->assertJsonPath('data.freshRememberInterval', '11')
->assertJsonPath('data.replyToMail', 'admin@example.com')
->assertJsonPath('data.yearlyMemberFilter.search', 'searchstring') ->assertJsonPath('data.yearlyMemberFilter.search', 'searchstring')
->assertJsonPath('data.yearlyMemberFilter.memberships.0.group_ids.0', 33) ->assertJsonPath('data.yearlyMemberFilter.memberships.0.group_ids.0', 33)
->assertJsonPath('data.preventAgainst', ['MOREPS']) ->assertJsonPath('data.preventAgainst', ['MOREPS'])
@ -60,7 +58,6 @@ it('testItStoresSettings', function () {
'freshRememberInterval' => 11, 'freshRememberInterval' => 11,
'active' => true, 'active' => true,
'preventAgainst' => ['EFZ'], 'preventAgainst' => ['EFZ'],
'replyToMail' => 'admin@example.com',
'yearlyMemberFilter' => [ 'yearlyMemberFilter' => [
'memberships' => [['group_ids' => 33]], 'memberships' => [['group_ids' => 33]],
'search' => 'searchstring', 'search' => 'searchstring',
@ -69,7 +66,6 @@ it('testItStoresSettings', function () {
test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem'])); test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem']));
test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd'])); test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd']));
test()->assertEquals(9, app(PreventionSettings::class)->weeks); test()->assertEquals(9, app(PreventionSettings::class)->weeks);
test()->assertEquals('admin@example.com', app(PreventionSettings::class)->replyToMail);
test()->assertEquals(11, app(PreventionSettings::class)->freshRememberInterval); test()->assertEquals(11, app(PreventionSettings::class)->freshRememberInterval);
test()->assertTrue(app(PreventionSettings::class)->active); test()->assertTrue(app(PreventionSettings::class)->active);
test()->assertEquals([['group_ids' => 33]], app(PreventionSettings::class)->yearlyMemberFilter->memberships); test()->assertEquals([['group_ids' => 33]], app(PreventionSettings::class)->yearlyMemberFilter->memberships);

View File

@ -18,7 +18,6 @@ use App\Form\Models\Form;
use App\Member\Member; use App\Member\Member;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Illuminate\Testing\TestResponse; use Illuminate\Testing\TestResponse;
use Tests\Feature\Form\FormtemplateFieldRequest; use Tests\Feature\Form\FormtemplateFieldRequest;
@ -43,22 +42,6 @@ trait CreatesFormFields
return $this->postJson(route('form.register', ['form' => $form]), $payload); 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() { public function setUpForm() {
app(FormSettings::class)->fill(['clearCacheUrl' => 'http://event.com/clear-cache'])->save(); app(FormSettings::class)->fill(['clearCacheUrl' => 'http://event.com/clear-cache'])->save();

View File

@ -1,5 +1,7 @@
<?php <?php
namespace Illuminate\Testing;
namespace Spatie\LaravelSettings; namespace Spatie\LaravelSettings;
/** /**