Compare commits

..

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

59 changed files with 77 additions and 682 deletions

View File

@ -31,15 +31,10 @@ steps:
commands: commands:
- while ! curl --silent 'http://owncloudserver:8080/ocs/v1.php/cloud/capabilities?format=json' -u admin:admin | grep '"status":"ok"'; do sleep 1; done - 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 - name: node
image: node:20.15.0-slim image: node:20.15.0-slim
commands: 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 - name: tests
image: zoomyboy/adrema-base:latest image: zoomyboy/adrema-base:latest

View File

@ -1,43 +1,5 @@
# Letzte Änderungen # Letzte Änderungen
### 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 ### 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

@ -4,7 +4,6 @@ namespace App\Console;
use App\Actions\DbMaintainAction; use App\Actions\DbMaintainAction;
use App\Form\Actions\PreventionRememberAction; use App\Form\Actions\PreventionRememberAction;
use App\Initialize\Actions\InitializeUpdateAction;
use App\Initialize\InitializeMembers; use App\Initialize\InitializeMembers;
use App\Invoice\Actions\InvoiceSendAction; use App\Invoice\Actions\InvoiceSendAction;
use App\Prevention\Actions\YearlyRememberAction; use App\Prevention\Actions\YearlyRememberAction;
@ -24,7 +23,6 @@ class Kernel extends ConsoleKernel
DbMaintainAction::class, DbMaintainAction::class,
PreventionRememberAction::class, PreventionRememberAction::class,
YearlyRememberAction::class, YearlyRememberAction::class,
InitializeUpdateAction::class,
]; ];
/** /**
@ -39,7 +37,6 @@ class Kernel extends ConsoleKernel
$schedule->command(PreventionRememberAction::class)->dailyAt('11:00'); $schedule->command(PreventionRememberAction::class)->dailyAt('11:00');
$schedule->command(InvoiceSendAction::class)->dailyAt('10:00'); $schedule->command(InvoiceSendAction::class)->dailyAt('10:00');
$schedule->command(YearlyRememberAction::class)->dailyAt('09:00'); $schedule->command(YearlyRememberAction::class)->dailyAt('09:00');
$schedule->command(InitializeUpdateAction::class)->weekly();
} }
/** /**

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

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

View File

@ -18,7 +18,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
public ?string $zip; public ?string $zip;
public ?string $iban; public ?string $iban;
public ?string $bic; public ?string $bic;
public ?string $replyTo;
public ?int $rememberWeeks; public ?int $rememberWeeks;
public static function group(): string public static function group(): string
@ -44,7 +43,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
'iban' => $this->iban, 'iban' => $this->iban,
'bic' => $this->bic, 'bic' => $this->bic,
'rememberWeeks' => $this->rememberWeeks, 'rememberWeeks' => $this->rememberWeeks,
'replyTo' => $this->replyTo,
] ]
]; ];
} }
@ -66,7 +64,6 @@ class InvoiceSettings extends LocalSettings implements Storeable
'iban' => '', 'iban' => '',
'bic' => '', 'bic' => '',
'rememberWeeks' => '', 'rememberWeeks' => '',
'replyTo' => '',
]; ];
} }

View File

@ -2,7 +2,6 @@
namespace App\Invoice\Mails; namespace App\Invoice\Mails;
use App\Invoice\InvoiceSettings;
use App\Invoice\Models\Invoice; use App\Invoice\Models\Invoice;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
@ -13,8 +12,6 @@ class BillMail extends Mailable
use Queueable; use Queueable;
use SerializesModels; use SerializesModels;
public InvoiceSettings $settings;
/** /**
* Create a new message instance. * Create a new message instance.
* *
@ -22,7 +19,6 @@ class BillMail extends Mailable
*/ */
public function __construct(public Invoice $invoice, public string $filename) 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') return $this->markdown('mail.invoice.bill')
->attach($this->filename) ->attach($this->filename)
->when($this->settings->replyTo, fn ($mail) => $mail->replyTo($this->settings->replyTo)) ->replyTo('kasse@stamm-silva.de')
->subject('Rechnung | '.$this->settings->from_long); ->subject('Rechnung | DPSG Stamm Silva');
} }
} }

View File

@ -2,7 +2,6 @@
namespace App\Invoice\Mails; namespace App\Invoice\Mails;
use App\Invoice\InvoiceSettings;
use App\Invoice\Models\Invoice; use App\Invoice\Models\Invoice;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
@ -13,8 +12,6 @@ class RememberMail extends Mailable
use Queueable; use Queueable;
use SerializesModels; use SerializesModels;
public InvoiceSettings $settings;
/** /**
* Create a new message instance. * Create a new message instance.
* *
@ -22,7 +19,6 @@ class RememberMail extends Mailable
*/ */
public function __construct(public Invoice $invoice, public string $filename) 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') return $this->markdown('mail.invoice.remember')
->attach($this->filename) ->attach($this->filename)
->when($this->settings->replyTo, fn ($mail) => $mail->replyTo($this->settings->replyTo)) ->replyTo('kasse@stamm-silva.de')
->subject('Zahlungserinnerung | '.$this->settings->from_long); ->subject('Zahlungserinnerung | DPSG Stamm Silva');
} }
} }

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,11 +0,0 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('bill.replyTo', '');
}
};

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 010825124f7791dfc0dc607e23ec99f49be8aef5 Subproject commit a4a2a2b3fd7a099cf1e5a9360d5d5450030bf673

@ -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"
@ -69,13 +69,13 @@
<div> <div>
<ui-tabs v-model="activeMailTab" :entries="mailTabs" /> <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> <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}"> <template #conditions="{cData, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve" /> <conditions-form id="mail_top_conditions" :single="single" :value="cData" @save="resolve" />
</template> </template>
</f-editor> </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> <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}"> <template #conditions="{d, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve" /> <conditions-form id="mail_bottom_conditions" :single="single" :value="d" @save="resolve" />
</template> </template>
</f-editor> </f-editor>
</div> </div>
@ -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,17 +11,6 @@
<f-text id="sectionform-name" v-model="singleSection.model.name" label="Name"></f-text> <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> <f-textarea id="sectionform-intro" v-model="singleSection.model.intro" label="Einleitung"></f-textarea>
</asideform> </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"> <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'}"> <draggable v-model="singleSection.model.fields" item-key="key" :component-data="{class: 'mt-3 grid gap-3'}">
<template #item="{element}"> <template #item="{element}">
@ -70,8 +59,6 @@
@editFields="startReordering($event.detail[0])" @editFields="startReordering($event.detail[0])"
@deleteSection="deleteSection($event.detail[0])" @deleteSection="deleteSection($event.detail[0])"
@active="updateActive($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> ></event-form>
</ui-box> </ui-box>
</form> </form>
@ -80,7 +67,7 @@
<script lang="js" setup> <script lang="js" setup>
import { watch, computed, ref } from 'vue'; import { watch, computed, ref } from 'vue';
import { snakeCase } from 'change-case'; import { snakeCase } from 'change-case';
import '!/adrema-form/dist/assets/main.js'; import '!/adrema-form/dist/main.js';
import Asideform from './Asideform.vue'; import Asideform from './Asideform.vue';
import TextareaField from './TextareaField.vue'; import TextareaField from './TextareaField.vue';
import TextField from './TextField.vue'; import TextField from './TextField.vue';
@ -96,7 +83,6 @@ import Draggable from 'vuedraggable';
const singleSection = ref(null); const singleSection = ref(null);
const singleField = ref(null); const singleField = ref(null);
const moving = ref(null);
const active = ref(null); const active = ref(null);
async function onReorder() { async function onReorder() {
@ -138,29 +124,6 @@ function editSection(sectionIndex) {
mode: 'edit', 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) { function startReordering(index) {
singleSection.value = { singleSection.value = {

View File

@ -18,7 +18,6 @@
<f-text id="iban" v-model="inner.iban" label="IBAN"></f-text> <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="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="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> </form>
</setting-layout> </setting-layout>
</page-layout> </page-layout>

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

@ -1,7 +1,7 @@
@component('mail::message') @component('mail::message')
# {{ $invoice->greeting }}, # {{ $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') @component('mail::subcopy')

View File

@ -1,7 +1,7 @@
@component('mail::message') @component('mail::message')
# {{ $invoice->greeting }}, # {{ $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') @component('mail::subcopy')

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

@ -48,7 +48,6 @@ class InvoiceSendActionTest extends TestCase
Tex::spy(); Tex::spy();
Storage::fake('temp'); Storage::fake('temp');
$this->withoutExceptionHandling()->login()->loginNami(); $this->withoutExceptionHandling()->login()->loginNami();
app(InvoiceSettings::class)->fill(['from_long' => 'Stammname', 'replyTo' => 'reply@mail.com'])->save();
$invoice = Invoice::factory() $invoice = Invoice::factory()
->to(ReceiverRequestFactory::new()->name('Familie Muster')) ->to(ReceiverRequestFactory::new()->name('Familie Muster'))
->has(InvoicePosition::factory()->withMember(), 'positions') ->has(InvoicePosition::factory()->withMember(), 'positions')
@ -58,13 +57,7 @@ class InvoiceSendActionTest extends TestCase
InvoiceSendAction::run(); InvoiceSendAction::run();
Mail::assertSent(RememberMail::class, fn ($mail) => $mail->build() 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'));
&& $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')
);
Tex::assertCompiled(RememberDocument::class, fn ($document) => 'Familie Muster' === $document->toName); 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')); $this->assertEquals(now()->format('Y-m-d'), $invoice->fresh()->last_remembered_at->format('Y-m-d'));
} }

View File

@ -33,8 +33,7 @@ class SettingTest extends TestCase
'zip' => '12345', 'zip' => '12345',
'iban' => 'DE05', 'iban' => 'DE05',
'bic' => 'SOLSDE', 'bic' => 'SOLSDE',
'rememberWeeks' => 6, 'rememberWeeks' => 6
'replyTo' => 'reply@example.com',
])->save(); ])->save();
$this->get(route('setting.data', ['settingGroup' => 'bill'])) $this->get(route('setting.data', ['settingGroup' => 'bill']))
@ -50,8 +49,7 @@ class SettingTest extends TestCase
->assertInertiaPath('data.zip', '12345') ->assertInertiaPath('data.zip', '12345')
->assertInertiaPath('data.iban', 'DE05') ->assertInertiaPath('data.iban', 'DE05')
->assertInertiaPath('data.bic', 'SOLSDE') ->assertInertiaPath('data.bic', 'SOLSDE')
->assertInertiaPath('data.rememberWeeks', 6) ->assertInertiaPath('data.rememberWeeks', 6);
->assertInertiaPath('data.replyTo', 'reply@example.com');
} }
public function testItReturnsTabs(): void public function testItReturnsTabs(): void
@ -80,8 +78,7 @@ class SettingTest extends TestCase
'zip' => '12345', 'zip' => '12345',
'iban' => 'DE05', 'iban' => 'DE05',
'bic' => 'SOLSDE', 'bic' => 'SOLSDE',
'rememberWeeks' => 10, 'rememberWeeks' => 10
'replyTo' => 'reply@example.com2',
]); ]);
$response->assertRedirect('/setting/bill'); $response->assertRedirect('/setting/bill');
@ -89,7 +86,6 @@ class SettingTest extends TestCase
$this->assertEquals('DPSG Stamm Muster', $settings->from_long); $this->assertEquals('DPSG Stamm Muster', $settings->from_long);
$this->assertEquals('DE05', $settings->iban); $this->assertEquals('DE05', $settings->iban);
$this->assertEquals('SOLSDE', $settings->bic); $this->assertEquals('SOLSDE', $settings->bic);
$this->assertEquals('reply@example.com2', $settings->replyTo);
$this->assertEquals(10, $settings->rememberWeeks); $this->assertEquals(10, $settings->rememberWeeks);
} }
} }

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;
/** /**