Compare commits

..

1 Commits

Author SHA1 Message Date
philipp lang 0724052313 Add skipping for yearly prevention
continuous-integration/drone/push Build is failing Details
2026-03-15 04:42:26 +01:00
23 changed files with 102 additions and 115 deletions

View File

@ -31,15 +31,10 @@ steps:
commands:
- while ! curl --silent 'http://owncloudserver:8080/ocs/v1.php/cloud/capabilities?format=json' -u admin:admin | grep '"status":"ok"'; do sleep 1; done
- name: node_submodules
image: node:20.15.0-slim
commands:
- cd packages/adrema-form && npm ci && npm run build-import && rm -R node_modules && cd ../../
- name: node
image: node:20.15.0-slim
commands:
- npm ci && npm run img && npm run prod && rm -R node_modules
- npm ci && cd packages/adrema-form && npm ci && npm run build && rm -R node_modules && cd ../../ && npm run img && npm run prod && rm -R node_modules
- name: tests
image: zoomyboy/adrema-base:latest

View File

@ -1,17 +1,5 @@
# 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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
namespace App\Lib;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Laravel\Scout\Builder;
use Spatie\LaravelData\Data;
@ -40,4 +41,20 @@ abstract class ScoutFilter extends Data
{
return static::factory()->withoutMagicalCreation()->from($post ?: []);
}
/**
* @param Collection<int, string> $filter
* @param array<string, bool|null> $conditions
* @return Collection<int, string>
*/
public function switches(Collection $filter, array $conditions): Collection
{
foreach ($conditions as $field => $value) {
if ($value !== null) {
$filter->push($field . ' = ' . ($value ? 'true' : 'false'));
}
}
return $filter;
}
}

View File

@ -48,6 +48,8 @@ class FilterScope extends ScoutFilter
public ?bool $hasBirthday = null,
public ?bool $hasSvk = null,
public ?bool $hasVk = null,
public ?bool $skipYearlyPrevention = null,
public ?bool $skipEventPrevention = null,
) {}
/**
@ -72,6 +74,7 @@ class FilterScope extends ScoutFilter
$this->search = $this->search ?: '';
return Member::search($this->search, function ($engine, string $query, array $options) {
/** @var Collection<int, string> */
$filter = collect([]);
if ($this->hasFullAddress === true) {
@ -86,12 +89,12 @@ class FilterScope extends ScoutFilter
if ($this->hasBirthday === true) {
$filter->push('birthday IS NOT NULL');
}
if ($this->hasSvk !== null) {
$filter->push('has_svk = ' . ($this->hasSvk ? 'true' : 'false'));
}
if ($this->hasVk !== null) {
$filter->push('has_vk = ' . ($this->hasVk ? 'true' : 'false'));
}
$filter = $this->switches($filter, [
'skip_yearly_prevention' => $this->skipYearlyPrevention,
'skip_event_prevention' => $this->skipEventPrevention,
'has_vk' => $this->hasVk,
'has_svk' => $this->hasSvk,
]);
if ($this->ausstand === true) {
$filter->push('ausstand > 0');
}

View File

@ -79,6 +79,8 @@ class Member extends Model implements Geolocatable, Preventable
'nami_id' => 'integer',
'has_svk' => 'boolean',
'has_vk' => 'boolean',
'skip_yearly_prevention' => 'boolean',
'skip_event_prevention' => 'boolean',
'multiply_pv' => 'boolean',
'multiply_more_pv' => 'boolean',
'is_leader' => 'boolean',
@ -597,6 +599,8 @@ class Member extends Model implements Geolocatable, Preventable
'group_name' => $this->group->inner_name ?: $this->group->name,
'has_vk' => $this->has_vk,
'has_svk' => $this->has_svk,
'skip_yearly_prevention' => $this->skip_yearly_prevention,
'skip_event_prevention' => $this->skip_event_prevention,
'links' => [
'show' => route('member.show', ['member' => $this], false),
'edit' => route('member.edit', ['member' => $this], false),

View File

@ -26,7 +26,7 @@ class YearlyRememberAction
return;
}
foreach ($settings->yearlyMemberFilter->getQuery()->get() as $member) {
foreach ($settings->getYearlyMemberFilter()->getQuery()->get() as $member) {
// @todo add this check to FilterScope
if ($member->getMailRecipient() === null) {
continue;

View File

@ -53,4 +53,11 @@ class PreventionSettings extends LocalSettings
'replyToMail' => $this->replyToMail,
];
}
public function getYearlyMemberFilter(): FilterScope
{
$this->yearlyMemberFilter->skipYearlyPrevention = false;
return $this->yearlyMemberFilter;
}
}

View File

@ -138,10 +138,10 @@ return [
'key' => env('MEILI_MASTER_KEY', null),
'index-settings' => [
Member::class => [
'filterableAttributes' => ['address', 'birthday', 'ausstand', 'bill_kind', 'group_id', 'memberships', 'has_vk', 'has_svk', 'id'],
'filterableAttributes' => ['address', 'birthday', 'ausstand', 'bill_kind', 'group_id', 'memberships', 'has_vk', 'has_svk', 'id', 'skip_yearly_prevention', 'skip_event_prevention'],
'searchableAttributes' => ['fullname', 'address'],
'sortableAttributes' => ['lastname', 'firstname'],
'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'],
'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id', 'skip_yearly_prevention', 'skip_event_prevention', 'has_vk', 'has_svk'],
'pagination' => [
'maxTotalHits' => 1000000,
]

View File

@ -39,6 +39,8 @@ class MemberFactory extends Factory
'keepdata' => false,
'has_svk' => $this->faker->boolean(),
'has_vk' => $this->faker->boolean(),
'skip_yearly_prevention' => false,
'skip_event_prevention' => false,
];
}

View File

@ -0,0 +1,30 @@
<?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('members', function (Blueprint $table) {
$table->boolean('skip_yearly_prevention')->default(false);
$table->boolean('skip_event_prevention')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropColumn('skip_yearly_prevention');
$table->dropColumn('skip_event_prevention');
});
}
};

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

@ -1 +1 @@
Subproject commit 010825124f7791dfc0dc607e23ec99f49be8aef5
Subproject commit a4a2a2b3fd7a099cf1e5a9360d5d5450030bf673

View File

@ -69,13 +69,13 @@
<div>
<ui-tabs v-model="activeMailTab" :entries="mailTabs" />
<f-editor v-if="activeMailTab === 0" id="mail_top" v-model="single.mail_top" name="mail_top" label="E-Mail-Teil 1" :rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="data" @save="resolve" />
<template #conditions="{cData, resolve}">
<conditions-form id="mail_top_conditions" :single="single" :value="cData" @save="resolve" />
</template>
</f-editor>
<f-editor v-if="activeMailTab === 1" id="mail_bottom" v-model="single.mail_bottom" name="mail_bottom" label="E-Mail-Teil 2" :rows="8" conditions required>
<template #conditions="{data, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="data" @save="resolve" />
<template #conditions="{d, resolve}">
<conditions-form id="mail_bottom_conditions" :single="single" :value="d" @save="resolve" />
</template>
</f-editor>
</div>

View File

@ -11,17 +11,6 @@
<f-text id="sectionform-name" v-model="singleSection.model.name" label="Name"></f-text>
<f-textarea id="sectionform-intro" v-model="singleSection.model.intro" label="Einleitung"></f-textarea>
</asideform>
<asideform
v-if="moving !== null"
heading="Feld verschieben nach Sektion …"
@close="moving = null"
>
<div class="mt-3 grid gap-3">
<a v-for="(section, index) in modelValue.sections" :key="index" class="py-2 px-3 border rounded bg-zinc-800 hover:bg-zinc-700 transition" href="#" @click.prevent="moveFieldToSection(index)">
<span v-text="section.name"></span>
</a>
</div>
</asideform>
<asideform v-if="singleSection !== null && singleSection.mode === 'reorder'" heading="Felder ordnen" @close="singleSection = null" @submit="storeSection">
<draggable v-model="singleSection.model.fields" item-key="key" :component-data="{class: 'mt-3 grid gap-3'}">
<template #item="{element}">
@ -70,8 +59,6 @@
@editFields="startReordering($event.detail[0])"
@deleteSection="deleteSection($event.detail[0])"
@active="updateActive($event.detail[0])"
@copy-field="copyField($event.detail[0], $event.detail[1])"
@move-field="moving = {section: $event.detail[0], field: $event.detail[1]}"
></event-form>
</ui-box>
</form>
@ -80,7 +67,7 @@
<script lang="js" setup>
import { watch, computed, ref } from 'vue';
import { snakeCase } from 'change-case';
import '!/adrema-form/dist/assets/main.js';
import '!/adrema-form/dist/main.js';
import Asideform from './Asideform.vue';
import TextareaField from './TextareaField.vue';
import TextField from './TextField.vue';
@ -96,7 +83,6 @@ import Draggable from 'vuedraggable';
const singleSection = ref(null);
const singleField = ref(null);
const moving = ref(null);
const active = ref(null);
async function onReorder() {
@ -138,29 +124,6 @@ function editSection(sectionIndex) {
mode: 'edit',
};
}
function moveFieldToSection(newSectionIndex) {
if (!moving.value) {
return;
}
singleField.value = {
model: JSON.parse(JSON.stringify(inner.value.sections[moving.value.section].fields[moving.value.field])),
sectionIndex: newSectionIndex,
index: null,
};
storeField();
deleteField(moving.value.section, moving.value.field);
moving.value = null;
}
function copyField(sectionIndex, fieldIndex) {
var field = JSON.parse(JSON.stringify(inner.value.sections[sectionIndex].fields[fieldIndex]));
field.name = field.name + ' - Kopie';
singleField.value = {
model: field,
sectionIndex: sectionIndex,
index: null,
};
}
function startReordering(index) {
singleSection.value = {

View File

@ -18,7 +18,6 @@
<f-text id="iban" v-model="inner.iban" label="IBAN"></f-text>
<f-text id="bic" v-model="inner.bic" label="BIC"></f-text>
<f-text id="remember_weeks" v-model="inner.rememberWeeks" type="number" label="Erinnerung alle X Wochen versenden"></f-text>
<f-text id="reply_to" v-model="inner.replyTo" label="Reply-To E-Mail-Adresse"></f-text>
</form>
</setting-layout>
</page-layout>

View File

@ -1,7 +1,7 @@
@component('mail::message')
# {{ $invoice->greeting }},
Im Anhang findet ihr die aktuelle Rechnung an {{$settings->from}} für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
Im Anhang findet ihr die aktuelle Rechnung des Stammes Silva für das laufende Jahr. Bitte begleicht diese bis zum angegebenen Datum.
@component('mail::subcopy')

View File

@ -1,7 +1,7 @@
@component('mail::message')
# {{ $invoice->greeting }},
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge an {{$settings->from}} für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
Hiermit möchten wir euch an die noch ausstehenden Mitgliedsbeiträge des Stammes Silva für das laufende Jahr erinnern. Bitte begleicht diese bis zum angegebenen Datum.
@component('mail::subcopy')

View File

@ -248,6 +248,18 @@ it('notices a few weeks before', function ($date, bool $shouldSend) {
[fn() => now()->subYears(5)->addWeeks(2)->subDay(), false],
]);
it('skips members that are marked as skipped for yearly mail', function (bool $skip) {
Mail::fake();
createMember(['efz' => null, 'skip_yearly_prevention' => $skip]);
sleep(2);
YearlyRememberAction::run();
$skip
? Mail::assertNotSent(YearlyMail::class)
: Mail::assertSent(YearlyMail::class);
})->with([true, false]);
it('sets reply to mail', function () {
Mail::fake();
app(PreventionSettings::class)->fill(['replyToMail' => 'admin@example.com'])->save();
@ -256,7 +268,7 @@ it('sets reply to mail', function () {
sleep(2);
YearlyRememberAction::run();
Mail::assertSent(YearlyMail::class, fn ($message) => $message->hasReplyTo('admin@example.com'));
Mail::assertSent(YearlyMail::class, fn($message) => $message->hasReplyTo('admin@example.com'));
});
it('remembers members yearly', function ($date, $shouldSend) {

View File

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

View File

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