Compare commits
	
		
			6 Commits
		
	
	
		
			fc2b3d6885
			...
			94f91cbda7
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 94f91cbda7 | |
|  | 5c577acdbf | |
|  | b6bb7d4113 | |
|  | e03c3a7ed3 | |
|  | 65a6614831 | |
|  | 7c656afce8 | 
|  | @ -2,6 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| namespace App\Member; | namespace App\Member; | ||||||
| 
 | 
 | ||||||
|  | use stdClass; | ||||||
| use App\Confession; | use App\Confession; | ||||||
| use App\Country; | use App\Country; | ||||||
| use App\Course\Models\CourseMember; | use App\Course\Models\CourseMember; | ||||||
|  | @ -13,6 +14,7 @@ use App\Nami\HasNamiField; | ||||||
| use App\Nationality; | use App\Nationality; | ||||||
| use App\Payment\Subscription; | use App\Payment\Subscription; | ||||||
| use App\Pdf\Sender; | use App\Pdf\Sender; | ||||||
|  | use App\Prevention\Contracts\YearlyPreventable; | ||||||
| use App\Region; | use App\Region; | ||||||
| use App\Setting\NamiSettings; | use App\Setting\NamiSettings; | ||||||
| use Carbon\Carbon; | use Carbon\Carbon; | ||||||
|  | @ -40,7 +42,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; | ||||||
|  * @property string $subscription_name |  * @property string $subscription_name | ||||||
|  * @property int    $pending_payment |  * @property int    $pending_payment | ||||||
|  */ |  */ | ||||||
| class Member extends Model implements Geolocatable | class Member extends Model implements Geolocatable, YearlyPreventable | ||||||
| { | { | ||||||
|     use Notifiable; |     use Notifiable; | ||||||
|     use HasNamiField; |     use HasNamiField; | ||||||
|  | @ -191,7 +193,7 @@ class Member extends Model implements Geolocatable | ||||||
| 
 | 
 | ||||||
|     protected function getAusstand(): int |     protected function getAusstand(): int | ||||||
|     { |     { | ||||||
|         return (int) $this->invoicePositions()->whereHas('invoice', fn ($query) => $query->whereNeedsPayment())->sum('price'); |         return (int) $this->invoicePositions()->whereHas('invoice', fn($query) => $query->whereNeedsPayment())->sum('price'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // ---------------------------------- Relations ----------------------------------
 |     // ---------------------------------- Relations ----------------------------------
 | ||||||
|  | @ -339,7 +341,7 @@ class Member extends Model implements Geolocatable | ||||||
|         return $query->addSelect([ |         return $query->addSelect([ | ||||||
|             'pending_payment' => InvoicePosition::selectRaw('SUM(price)') |             'pending_payment' => InvoicePosition::selectRaw('SUM(price)') | ||||||
|                 ->whereColumn('invoice_positions.member_id', 'members.id') |                 ->whereColumn('invoice_positions.member_id', 'members.id') | ||||||
|                 ->whereHas('invoice', fn ($query) => $query->whereNeedsPayment()), |                 ->whereHas('invoice', fn($query) => $query->whereNeedsPayment()), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -350,7 +352,7 @@ class Member extends Model implements Geolocatable | ||||||
|      */ |      */ | ||||||
|     public function scopeWhereHasPendingPayment(Builder $query): Builder |     public function scopeWhereHasPendingPayment(Builder $query): Builder | ||||||
|     { |     { | ||||||
|         return $query->whereHas('invoicePositions', fn ($q) => $q->whereHas('invoice', fn ($q) => $q->whereNeedsPayment())); |         return $query->whereHas('invoicePositions', fn($q) => $q->whereHas('invoice', fn($q) => $q->whereNeedsPayment())); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | @ -499,7 +501,7 @@ class Member extends Model implements Geolocatable | ||||||
|             'name' => $this->fullname, |             'name' => $this->fullname, | ||||||
|             'address' => $this->address, |             'address' => $this->address, | ||||||
|             'zipLocation' => $this->zip . ' ' . $this->location, |             'zipLocation' => $this->zip . ' ' . $this->location, | ||||||
|             'mglnr' => Lazy::create(fn () => 'Mglnr.: ' . $this->nami_id), |             'mglnr' => Lazy::create(fn() => 'Mglnr.: ' . $this->nami_id), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -508,7 +510,7 @@ class Member extends Model implements Geolocatable | ||||||
|      */ |      */ | ||||||
|     public static function forSelect(): array |     public static function forSelect(): array | ||||||
|     { |     { | ||||||
|         return static::select(['id', 'firstname', 'lastname'])->get()->map(fn ($member) => ['id' => $member->id, 'name' => $member->fullname])->toArray(); |         return static::select(['id', 'firstname', 'lastname'])->get()->map(fn($member) => ['id' => $member->id, 'name' => $member->fullname])->toArray(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // -------------------------------- Geolocation --------------------------------
 |     // -------------------------------- Geolocation --------------------------------
 | ||||||
|  | @ -567,7 +569,24 @@ class Member extends Model implements Geolocatable | ||||||
|             'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug, |             'age_group_icon' => $this->ageGroupMemberships->first()?->subactivity->slug, | ||||||
|             'is_leader' => $this->leaderMemberships()->count() > 0, |             'is_leader' => $this->leaderMemberships()->count() > 0, | ||||||
|             'memberships' => $this->memberships()->active()->get() |             'memberships' => $this->memberships()->active()->get() | ||||||
|                 ->map(fn ($membership) => [...$membership->only('activity_id', 'subactivity_id'), 'both' => $membership->activity_id . '|' . $membership->subactivity_id, 'with_group' => $membership->group_id . '|' . $membership->activity_id . '|' . $membership->subactivity_id]), |                 ->map(fn($membership) => [...$membership->only('activity_id', 'subactivity_id'), 'both' => $membership->activity_id . '|' . $membership->subactivity_id, 'with_group' => $membership->group_id . '|' . $membership->activity_id . '|' . $membership->subactivity_id]), | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // -------------------------------- Prevention ---------------------------------
 | ||||||
|  |     // *****************************************************************************
 | ||||||
|  |     public function preventableSubject(): string | ||||||
|  |     { | ||||||
|  |         return 'Nachweise erforderlich'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function preventableLayout(): string | ||||||
|  |     { | ||||||
|  |         return 'mail.prevention.prevention-remember-participant'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getMailRecipient(): stdClass | ||||||
|  |     { | ||||||
|  |         return (object) ['name' => $this->fullname, 'email' => $this->email]; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ class SettingStoreAction | ||||||
|     { |     { | ||||||
|         return [ |         return [ | ||||||
|             'formmail' => 'array', |             'formmail' => 'array', | ||||||
|  |             'yearlymail' => 'array', | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -26,6 +27,7 @@ class SettingStoreAction | ||||||
|     { |     { | ||||||
|         $settings = app(PreventionSettings::class); |         $settings = app(PreventionSettings::class); | ||||||
|         $settings->formmail = EditorData::from($request->formmail); |         $settings->formmail = EditorData::from($request->formmail); | ||||||
|  |         $settings->yearlymail = EditorData::from($request->yearlymail); | ||||||
|         $settings->save(); |         $settings->save(); | ||||||
| 
 | 
 | ||||||
|         Succeeded::message('Einstellungen gespeichert.')->dispatch(); |         Succeeded::message('Einstellungen gespeichert.')->dispatch(); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,20 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Prevention\Contracts; | ||||||
|  | 
 | ||||||
|  | use App\Prevention\Enums\Prevention; | ||||||
|  | use stdClass; | ||||||
|  | 
 | ||||||
|  | interface YearlyPreventable | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  |     public function preventableLayout(): string; | ||||||
|  |     public function preventableSubject(): string; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return array<int, Prevention> | ||||||
|  |      */ | ||||||
|  |     public function preventions(): array; | ||||||
|  | 
 | ||||||
|  |     public function getMailRecipient(): stdClass; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,65 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Prevention\Mails; | ||||||
|  | 
 | ||||||
|  | use App\Invoice\InvoiceSettings; | ||||||
|  | use App\Lib\Editor\EditorData; | ||||||
|  | use App\Prevention\Contracts\Preventable; | ||||||
|  | use App\Prevention\Contracts\YearlyPreventable; | ||||||
|  | use Illuminate\Bus\Queueable; | ||||||
|  | use Illuminate\Mail\Attachment; | ||||||
|  | use Illuminate\Mail\Mailable; | ||||||
|  | use Illuminate\Mail\Mailables\Content; | ||||||
|  | use Illuminate\Mail\Mailables\Envelope; | ||||||
|  | use Illuminate\Queue\SerializesModels; | ||||||
|  | 
 | ||||||
|  | class YearlyMail extends Mailable | ||||||
|  | { | ||||||
|  |     use Queueable, SerializesModels; | ||||||
|  | 
 | ||||||
|  |     public InvoiceSettings $settings; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new message instance. | ||||||
|  |      */ | ||||||
|  |     public function __construct(public YearlyPreventable $preventable, public EditorData $bodyText) | ||||||
|  |     { | ||||||
|  |         $this->settings = app(InvoiceSettings::class); | ||||||
|  |         $this->bodyText = $this->bodyText | ||||||
|  |             ->replaceWithList('wanted', collect($preventable->preventions())->map(fn($prevention) => $prevention->text())->toArray()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the message envelope. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Mail\Mailables\Envelope | ||||||
|  |      */ | ||||||
|  |     public function envelope() | ||||||
|  |     { | ||||||
|  |         return (new Envelope( | ||||||
|  |             subject: $this->preventable->preventableSubject(), | ||||||
|  |         ))->to($this->preventable->getMailRecipient()->email, $this->preventable->getMailRecipient()->name); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the message content definition. | ||||||
|  |      * | ||||||
|  |      * @return \Illuminate\Mail\Mailables\Content | ||||||
|  |      */ | ||||||
|  |     public function content() | ||||||
|  |     { | ||||||
|  |         return new Content( | ||||||
|  |             markdown: $this->preventable->preventableLayout(), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the attachments for the message. | ||||||
|  |      * | ||||||
|  |      * @return array<int, Attachment> | ||||||
|  |      */ | ||||||
|  |     public function attachments(): array | ||||||
|  |     { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -9,6 +9,7 @@ class PreventionSettings extends LocalSettings | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
|     public EditorData $formmail; |     public EditorData $formmail; | ||||||
|  |     public EditorData $yearlymail; | ||||||
| 
 | 
 | ||||||
|     public static function group(): string |     public static function group(): string | ||||||
|     { |     { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | use Spatie\LaravelSettings\Migrations\SettingsMigration; | ||||||
|  | 
 | ||||||
|  | return new class extends SettingsMigration | ||||||
|  | { | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         $this->migrator->add('prevention.yearlymail', ['time' => 1, 'blocks' => [], 'version' => '1.0']); | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | @ -91,6 +91,8 @@ services: | ||||||
|             - ./data/db:/var/lib/mysql |             - ./data/db:/var/lib/mysql | ||||||
| 
 | 
 | ||||||
|     socketi: |     socketi: | ||||||
|  |         ports: | ||||||
|  |             - '6001:6001' | ||||||
|         image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian |         image: quay.io/soketi/soketi:89604f268623cf799573178a7ba56b7491416bde-16-debian | ||||||
|         environment: |         environment: | ||||||
|             SOKETI_DEFAULT_APP_ID: adremaid |             SOKETI_DEFAULT_APP_ID: adremaid | ||||||
|  | @ -103,6 +105,8 @@ services: | ||||||
|             - ./data/redis:/data |             - ./data/redis:/data | ||||||
| 
 | 
 | ||||||
|     meilisearch: |     meilisearch: | ||||||
|  |         ports: | ||||||
|  |             - '7700:7700' | ||||||
|         image: getmeili/meilisearch:v1.6 |         image: getmeili/meilisearch:v1.6 | ||||||
|         volumes: |         volumes: | ||||||
|             - ./data/meilisearch:/meili_data |             - ./data/meilisearch:/meili_data | ||||||
|  |  | ||||||
|  | @ -1,47 +1,19 @@ | ||||||
| <template> | <template> | ||||||
|     <ui-popup v-if="visible === true" heading="Filtern" @close="visible = false"> |     <page-filter-sidebar v-if="visible === true" @close="visible = false"> | ||||||
|         <div class="grid gap-3 md:grid-cols-2"> |         <slot name="fields"></slot> | ||||||
|             <slot name="fields"></slot> |     </page-filter-sidebar> | ||||||
|         </div> |     <div class="px-6 py-2 border-b border-gray-600 items-center space-x-3"> | ||||||
|     </ui-popup> |  | ||||||
|     <div class="px-6 py-2 border-b border-gray-600" :class="visibleDesktopBlock"> |  | ||||||
|         <div class="flex items-end space-x-3"> |  | ||||||
|             <slot name="buttons"></slot> |  | ||||||
|             <ui-icon-button v-if="filterable" icon="filter" @click="filterVisible = !filterVisible">Filtern</ui-icon-button> |  | ||||||
|         </div> |  | ||||||
|         <ui-box v-if="filterVisible" class="mt-3"> |  | ||||||
|             <div class="grid grid-cols-4 gap-3 items-end"> |  | ||||||
|                 <slot name="fields"></slot> |  | ||||||
|                 <ui-icon-button class="col-start-1" icon="close" @click="filterVisible = false">Schließen</ui-icon-button> |  | ||||||
|             </div> |  | ||||||
|         </ui-box> |  | ||||||
|     </div> |  | ||||||
|     <div class="px-6 py-2 border-b border-gray-600 items-center space-x-3" :class="visibleMobile"> |  | ||||||
|         <div class="flex flex-col sm:flex-row items-stretch sm:items-end space-y-1 sm:space-y-0 sm:space-x-3"> |         <div class="flex flex-col sm:flex-row items-stretch sm:items-end space-y-1 sm:space-y-0 sm:space-x-3"> | ||||||
|             <slot name="buttons"></slot> |             <slot name="buttons"></slot> | ||||||
|             <ui-icon-button v-if="filterable" icon="filter" @click="visible = true">Filtern</ui-icon-button> |             <ui-icon-button v-if="!!$slots.fields" icon="filter" @click="visible = true">Filtern</ui-icon-button> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup> | <script setup> | ||||||
| import {defineProps, ref} from 'vue'; | import {ref} from 'vue'; | ||||||
| import useBreakpoints from '../../composables/useBreakpoints.js'; | 
 | ||||||
|  | defineEmits(['close']); | ||||||
| 
 | 
 | ||||||
| const visible = ref(false); | const visible = ref(false); | ||||||
| 
 |  | ||||||
| const filterVisible = ref(false); |  | ||||||
| 
 |  | ||||||
| const props = defineProps({ |  | ||||||
|     breakpoint: { |  | ||||||
|         type: String, |  | ||||||
|         required: true, |  | ||||||
|     }, |  | ||||||
|     filterable: { |  | ||||||
|         type: Boolean, |  | ||||||
|         default: () => true, |  | ||||||
|     }, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const {visibleDesktopBlock, visibleMobile} = useBreakpoints(props); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | <template> | ||||||
|  |     <ui-sidebar :max="0" @close="$emit('close')"> | ||||||
|  |         <page-header title="Filtern" @close="$emit('close')"></page-header> | ||||||
|  |         <div class="grid gap-6 p-6"> | ||||||
|  |             <slot></slot> | ||||||
|  |         </div> | ||||||
|  |     </ui-sidebar> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script setup> | ||||||
|  | import {ref} from 'vue'; | ||||||
|  | 
 | ||||||
|  | const visible = ref(false); | ||||||
|  | 
 | ||||||
|  | defineEmits(['close']); | ||||||
|  | </script> | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| <template> | <template> | ||||||
|     <div |     <div class="fixed shadow-2xl bg-gray-600 right-0 top-0 h-full flex flex-col group is-bright" :class="widths[max]"> | ||||||
|         class="fixed w-full w-[80vw] max-w-[40rem] shadow-2xl bg-gray-600 right-0 top-0 h-full flex flex-col group is-bright"> |  | ||||||
|         <suspense> |         <suspense> | ||||||
|             <slot></slot> |             <slot></slot> | ||||||
| 
 | 
 | ||||||
|  | @ -18,4 +17,20 @@ | ||||||
| 
 | 
 | ||||||
| <script setup> | <script setup> | ||||||
| defineEmits(['close']); | defineEmits(['close']); | ||||||
|  | 
 | ||||||
|  | const widths = { | ||||||
|  |     40: 'w-full w-[80vw] max-w-[40rem]', | ||||||
|  |     30: 'w-full w-[80vw] max-w-[30rem]', | ||||||
|  |     20: 'w-full w-[80vw] max-w-[20rem]', | ||||||
|  |     10: 'w-full w-[80vw] max-w-[10rem]', | ||||||
|  |     0: '', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | defineProps({ | ||||||
|  |     max: { | ||||||
|  |         default: () => 40, | ||||||
|  |         type: Number, | ||||||
|  |         required: false, | ||||||
|  |     }, | ||||||
|  | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,9 +1,15 @@ | ||||||
| <template> | <template> | ||||||
|     <div class="flex-none w-maxc flex flex-col justify-between border-b-2 group-[.is-popup]:border-zinc-500 mb-3"> |     <div class="flex-none w-maxc flex flex-col justify-between border-b-2 border-gray-500 group-[.is-popup]:border-zinc-500 mb-3"> | ||||||
|         <div class="flex space-x-1 px-2"> |         <div class="flex space-x-1 px-2"> | ||||||
|             <a v-for="(item, index) in entries" :key="index" href="#" class="rounded-t-lg py-1 px-3 text-zinc-300" |             <a | ||||||
|                 :class="index === modelValue ? `group-[.is-popup]:bg-zinc-600` : ''" @click.prevent="openMenu(index)" |                 v-for="(item, index) in entries" | ||||||
|                 v-text="item.title"></a> |                 :key="index" | ||||||
|  |                 href="#" | ||||||
|  |                 class="rounded-t-lg py-1 px-3 text-zinc-300" | ||||||
|  |                 :class="index === modelValue ? `bg-gray-700 group-[.is-popup]:bg-zinc-600` : ''" | ||||||
|  |                 @click.prevent="openMenu(index)" | ||||||
|  |                 v-text="item.title" | ||||||
|  |             ></a> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,46 +0,0 @@ | ||||||
| import {computed} from 'vue'; |  | ||||||
| 
 |  | ||||||
| export default function (props) { |  | ||||||
|     const visibleMobile = computed(() => { |  | ||||||
|         return { |  | ||||||
|             sm: 'flex sm:hidden', |  | ||||||
|             md: 'flex md:hidden', |  | ||||||
|             lg: 'flex lg:hidden', |  | ||||||
|             xl: 'flex xl:hidden', |  | ||||||
|         }[props.breakpoint]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const visibleDesktop = computed(() => { |  | ||||||
|         return { |  | ||||||
|             sm: 'hidden sm:flex', |  | ||||||
|             md: 'hidden md:flex', |  | ||||||
|             lg: 'hidden lg:flex', |  | ||||||
|             xl: 'hidden xl:flex', |  | ||||||
|         }[props.breakpoint]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const visibleMobileBlock = computed(() => { |  | ||||||
|         return { |  | ||||||
|             sm: 'block sm:hidden', |  | ||||||
|             md: 'block md:hidden', |  | ||||||
|             lg: 'block lg:hidden', |  | ||||||
|             xl: 'block xl:hidden', |  | ||||||
|         }[props.breakpoint]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const visibleDesktopBlock = computed(() => { |  | ||||||
|         return { |  | ||||||
|             sm: 'hidden sm:block', |  | ||||||
|             md: 'hidden md:block', |  | ||||||
|             lg: 'hidden lg:block', |  | ||||||
|             xl: 'hidden xl:block', |  | ||||||
|         }[props.breakpoint]; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|         visibleMobile, |  | ||||||
|         visibleDesktop, |  | ||||||
|         visibleDesktopBlock, |  | ||||||
|         visibleMobileBlock, |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  | @ -144,7 +144,7 @@ | ||||||
|             <conditions-form id="filesettings" :single="single" :value="fileSettingPopup.properties.conditions" @save="saveFileConditions"> </conditions-form> |             <conditions-form id="filesettings" :single="single" :value="fileSettingPopup.properties.conditions" @save="saveFileConditions"> </conditions-form> | ||||||
|         </ui-popup> |         </ui-popup> | ||||||
| 
 | 
 | ||||||
|         <page-filter breakpoint="xl" :filterable="false"> |         <page-filter> | ||||||
|             <template #buttons> |             <template #buttons> | ||||||
|                 <f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> |                 <f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> | ||||||
|                 <f-switch id="past" :model-value="getFilter('past')" label="vergangene zeigen" name="past" size="sm" @update:model-value="setFilter('past', $event)"></f-switch> |                 <f-switch id="past" :model-value="getFilter('past')" label="vergangene zeigen" name="past" size="sm" @update:model-value="setFilter('past', $event)"></f-switch> | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|         </ui-popup> |         </ui-popup> | ||||||
|         <page-filter breakpoint="lg"> |         <page-filter> | ||||||
|             <template #buttons> |             <template #buttons> | ||||||
|                 <f-text id="search" v-model="innerFilter.search" name="search" label="Suchen" size="sm"></f-text> |                 <f-text id="search" v-model="innerFilter.search" name="search" label="Suchen" size="sm"></f-text> | ||||||
|                 <ui-icon-button icon="plus" @click="editing = {participant: null, preview: JSON.stringify(meta.form_config)}">Hinzufügen</ui-icon-button> |                 <ui-icon-button icon="plus" @click="editing = {participant: null, preview: JSON.stringify(meta.form_config)}">Hinzufügen</ui-icon-button> | ||||||
|  |  | ||||||
|  | @ -72,7 +72,7 @@ | ||||||
|                 </section> |                 </section> | ||||||
|             </form> |             </form> | ||||||
|         </ui-popup> |         </ui-popup> | ||||||
|         <page-filter breakpoint="xl" :filterable="false"> |         <page-filter> | ||||||
|             <template #buttons> |             <template #buttons> | ||||||
|                 <f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> |                 <f-text id="search" :model-value="getFilter('search')" label="Suchen …" size="sm" @update:model-value="setFilter('search', $event)"></f-text> | ||||||
|                 <f-multipleselect |                 <f-multipleselect | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | <template></template> | ||||||
|  | 
 | ||||||
|  | <script setup></script> | ||||||
|  | @ -37,53 +37,14 @@ | ||||||
|                 <span class="hidden xl:inline">Anwenden</span> |                 <span class="hidden xl:inline">Anwenden</span> | ||||||
|             </button> |             </button> | ||||||
|         </ui-popup> |         </ui-popup> | ||||||
|         <page-filter breakpoint="xl"> | 
 | ||||||
|  |         <page-filter> | ||||||
|             <template #fields> |             <template #fields> | ||||||
|                 <f-switch |                 <f-switch v-show="hasModule('bill')" id="ausstand" name="ausstand" v-model="filter.ausstand" label="Nur Ausstände" size="sm"></f-switch> | ||||||
|                     v-show="hasModule('bill')" |                 <f-select id="has_vk" name="has_vk" v-model="filter.has_vk" label="Verhaltenskodex unterschrieben" size="sm" :options="meta.boolean_filter"></f-select> | ||||||
|                     id="ausstand" |                 <f-select id="has_svk" name="has_svk" v-model="filter.has_svk" label="SVK unterschrieben" size="sm" :options="meta.boolean_filter"></f-select> | ||||||
|                     name="ausstand" |                 <f-multipleselect id="group_ids" :options="meta.groups" v-model="filter.group_ids" label="Gruppierungen" size="sm"></f-multipleselect> | ||||||
|                     :model-value="getFilter('ausstand')" |                 <f-select v-show="hasModule('bill')" id="billKinds" name="billKinds" :options="meta.billKinds" v-model="filter.bill_kind" label="Rechnung" size="sm"></f-select> | ||||||
|                     label="Nur Ausstände" |  | ||||||
|                     size="sm" |  | ||||||
|                     @update:model-value="setFilter('ausstand', $event)" |  | ||||||
|                 ></f-switch> |  | ||||||
|                 <f-select |  | ||||||
|                     id="has_vk" |  | ||||||
|                     name="has_vk" |  | ||||||
|                     :model-value="getFilter('has_vk')" |  | ||||||
|                     label="Verhaltenskodex unterschrieben" |  | ||||||
|                     size="sm" |  | ||||||
|                     :options="meta.boolean_filter" |  | ||||||
|                     @update:model-value="setFilter('has_vk', $event)" |  | ||||||
|                 ></f-select> |  | ||||||
|                 <f-select |  | ||||||
|                     id="has_svk" |  | ||||||
|                     name="has_svk" |  | ||||||
|                     :model-value="getFilter('has_svk')" |  | ||||||
|                     label="SVK unterschrieben" |  | ||||||
|                     size="sm" |  | ||||||
|                     :options="meta.boolean_filter" |  | ||||||
|                     @update:model-value="setFilter('has_svk', $event)" |  | ||||||
|                 ></f-select> |  | ||||||
|                 <f-multipleselect |  | ||||||
|                     id="group_ids" |  | ||||||
|                     :options="meta.groups" |  | ||||||
|                     :model-value="getFilter('group_ids')" |  | ||||||
|                     label="Gruppierungen" |  | ||||||
|                     size="sm" |  | ||||||
|                     @update:model-value="setFilter('group_ids', $event)" |  | ||||||
|                 ></f-multipleselect> |  | ||||||
|                 <f-select |  | ||||||
|                     v-show="hasModule('bill')" |  | ||||||
|                     id="billKinds" |  | ||||||
|                     name="billKinds" |  | ||||||
|                     :options="meta.billKinds" |  | ||||||
|                     :model-value="getFilter('bill_kind')" |  | ||||||
|                     label="Rechnung" |  | ||||||
|                     size="sm" |  | ||||||
|                     @update:model-value="setFilter('bill_kind', $event)" |  | ||||||
|                 ></f-select> |  | ||||||
|                 <button class="btn btn-primary label mr-2" @click.prevent="membershipFilters = getFilter('memberships')"> |                 <button class="btn btn-primary label mr-2" @click.prevent="membershipFilters = getFilter('memberships')"> | ||||||
|                     <ui-sprite class="w-3 h-3 xl:mr-2" src="filter"></ui-sprite> |                     <ui-sprite class="w-3 h-3 xl:mr-2" src="filter"></ui-sprite> | ||||||
|                     <span class="hidden xl:inline">Mitgliedschaften</span> |                     <span class="hidden xl:inline">Mitgliedschaften</span> | ||||||
|  | @ -174,6 +135,11 @@ const single = ref(null); | ||||||
| const deleting = ref(null); | const deleting = ref(null); | ||||||
| const membershipFilters = ref(null); | const membershipFilters = ref(null); | ||||||
| 
 | 
 | ||||||
|  | var filter = ref({ | ||||||
|  |     ausstand: false, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| const props = defineProps(indexProps); | const props = defineProps(indexProps); | ||||||
| var { router, data, meta, getFilter, setFilter, filterString, reloadPage } = useIndex(props.data, 'member'); | var { router, data, meta, getFilter, setFilter, filterString, reloadPage } = useIndex(props.data, 'member'); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,24 +3,31 @@ | ||||||
|         <template #right> |         <template #right> | ||||||
|             <f-save-button form="preventionform"></f-save-button> |             <f-save-button form="preventionform"></f-save-button> | ||||||
|         </template> |         </template> | ||||||
|  | 
 | ||||||
|         <setting-layout v-if="loaded"> |         <setting-layout v-if="loaded"> | ||||||
|             <form id="preventionform" class="grow p-6" @submit.prevent="submit"> |             <form id="preventionform" class="grow p-6" @submit.prevent="submit"> | ||||||
|                 <div class="col-span-full text-gray-100 mb-3"> |                 <div class="col-span-full text-gray-100 mb-3"> | ||||||
|                     <p class="text-sm">Hier kannst du Einstellungen zu Prävention setzen.</p> |                     <p class="text-sm">Hier kannst du Einstellungen zu Prävention setzen.</p> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="grid gap-4 mt-2"> |                 <ui-tabs v-model="active" class="mt-2" :entries="tabs"></ui-tabs> | ||||||
|                     <f-editor id="frommail" v-model="data.formmail" label="E-Mail für Veranstaltungs-TN"></f-editor> |                 <f-editor v-if="active === 0" id="formmail" v-model="data.formmail" label="E-Mail für Veranstaltungs-TN"></f-editor> | ||||||
|                 </div> |                 <f-editor v-if="active === 1" id="yearlymail" v-model="data.yearlymail" label="Jährliche Präventions-Erinnerung"></f-editor> | ||||||
|             </form> |             </form> | ||||||
|         </setting-layout> |         </setting-layout> | ||||||
|     </page-layout> |     </page-layout> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="js" setup> | <script lang="js" setup> | ||||||
| import { ref } from 'vue'; | import { reactive, ref } from 'vue'; | ||||||
| import { useApiIndex } from '../../composables/useApiIndex.js'; | import { useApiIndex } from '../../composables/useApiIndex.js'; | ||||||
| import SettingLayout from '../setting/Layout.vue'; | import SettingLayout from '../setting/Layout.vue'; | ||||||
| 
 | 
 | ||||||
|  | const tabs = [ | ||||||
|  |     { title: 'für Veranstaltungen' }, | ||||||
|  |     { title: 'Jährlich' }, | ||||||
|  | ]; | ||||||
|  | const active = ref(0); | ||||||
|  | 
 | ||||||
| const { axios, data, reload } = useApiIndex('/api/prevention', 'prevention'); | const { axios, data, reload } = useApiIndex('/api/prevention', 'prevention'); | ||||||
| const loaded = ref(false); | const loaded = ref(false); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ use App\Lib\Editor\Condition; | ||||||
| use App\Prevention\Mails\PreventionRememberMail; | use App\Prevention\Mails\PreventionRememberMail; | ||||||
| use App\Member\Member; | use App\Member\Member; | ||||||
| use App\Member\Membership; | use App\Member\Membership; | ||||||
|  | use App\Prevention\Mails\YearlyMail; | ||||||
| use App\Prevention\PreventionSettings; | use App\Prevention\PreventionSettings; | ||||||
| use Generator; | use Generator; | ||||||
| use Illuminate\Foundation\Testing\DatabaseTransactions; | use Illuminate\Foundation\Testing\DatabaseTransactions; | ||||||
|  | @ -22,285 +23,272 @@ use Tests\Lib\CreatesFormFields; | ||||||
| use Tests\RequestFactories\EditorRequestFactory; | use Tests\RequestFactories\EditorRequestFactory; | ||||||
| use Tests\TestCase; | use Tests\TestCase; | ||||||
| 
 | 
 | ||||||
| class PreventionTest extends TestCase | uses(DatabaseTransactions::class); | ||||||
|  | uses(CreatesFormFields::class); | ||||||
|  | 
 | ||||||
|  | function createForm(): Form | ||||||
| { | { | ||||||
| 
 |     return Form::factory()->fields([ | ||||||
|     use DatabaseTransactions; |         test()->textField('vorname')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::FIRSTNAME), | ||||||
|     use CreatesFormFields; |         test()->textField('nachname')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::LASTNAME), | ||||||
| 
 |         test()->textField('email')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::EMAIL), | ||||||
|     public function testItRemembersWhenNotRememberedYet(): void |     ])->create(['needs_prevention' => true]); | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberPastEvents(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $form->update(['from' => now()->subDay()]); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertNull($participant->fresh()->last_remembered_at); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberWhenConditionDoesntMatch(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $form->update(['prevention_conditions' => Condition::from(['mode' => 'all', 'ifs' => [['field' => 'vorname', 'comparator' => 'isEqual', 'value' => 'Max']]])]); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $participant->update(['data' => [...$participant->data, 'vorname' => 'Jane']]); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertNull($participant->fresh()->last_remembered_at); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItRemembersWhenRememberIsDue(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = tap($this->createParticipant($form), fn ($p) => $p->update(['last_remembered_at' => now()->subWeeks(3)])); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberWhenRememberingIsNotDue(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = tap($this->createParticipant($form), fn ($p) => $p->update(['last_remembered_at' => now()->subWeeks(1)])); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertEquals(now()->subWeeks(1)->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberWhenFormDoesntNeedPrevention(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = tap($this->createForm(), fn ($form) => $form->update(['needs_prevention' => false])); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertNull($participant->fresh()->last_remembered_at); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberWhenParticipantDoesntHaveMember(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $participant->member->delete(); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertNull($participant->fresh()->last_remembered_at); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItRemembersNonLeaders(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $participant->member->memberships->each->delete(); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         $this->assertNotNull($participant->fresh()->last_remembered_at); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public static function attributes(): Generator |  | ||||||
|     { |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => null, 'ps_at' => now()], |  | ||||||
|             'preventions' => [Prevention::EFZ] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => null], |  | ||||||
|             'preventions' => [Prevention::PS] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now()->subDay(), 'ps_at' => now()], |  | ||||||
|             'preventions' => [] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subDay()], |  | ||||||
|             'preventions' => [] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now()->subYears(5)->subDay(), 'ps_at' => now()], |  | ||||||
|             'preventions' => [Prevention::EFZ] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => null], |  | ||||||
|             'preventions' => [Prevention::PS] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(5)->subDay()], |  | ||||||
|             'preventions' => [Prevention::MOREPS] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(5)->subDay(), 'more_ps_at' => now()], |  | ||||||
|             'preventions' => [] |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(15), 'more_ps_at' => now()->subYears(5)->subDay()], |  | ||||||
|             'preventions' => [Prevention::MOREPS], |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => false, 'efz' => now(), 'ps_at' => now()], |  | ||||||
|             'preventions' => [Prevention::VK], |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         yield [ |  | ||||||
|             'attrs' => ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(7)], |  | ||||||
|             'preventions' => [Prevention::MOREPS], |  | ||||||
|         ]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * @param array<int, Prevention> $preventions |  | ||||||
|      * @param array<string, mixed> $memberAttributes |  | ||||||
|      */ |  | ||||||
|     #[DataProvider('attributes')]
 |  | ||||||
|     public function testItRemembersMember(array $attrs, array $preventions): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $participant->member->update($attrs); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         if (count($preventions)) { |  | ||||||
|             Mail::assertSent(PreventionRememberMail::class, fn ($mail) => $mail->preventable->preventions() === $preventions); |  | ||||||
|             $this->assertNotNull($participant->fresh()->last_remembered_at); |  | ||||||
|         } else { |  | ||||||
|             Mail::assertNotSent(PreventionRememberMail::class); |  | ||||||
|             $this->assertNull($participant->fresh()->last_remembered_at); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntRememberParticipantThatHasNoMail(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         $participant->update(['data' => [...$participant->data, 'email' => '']]); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         Mail::assertNotSent(PreventionRememberMail::class); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItRendersMail(): void |  | ||||||
|     { |  | ||||||
|         InvoiceSettings::fake(['from_long' => 'Stamm Beispiel']); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
|         (new PreventionRememberMail($participant, app(PreventionSettings::class)->formmail)) |  | ||||||
|             ->assertSeeInText($participant->member->firstname) |  | ||||||
|             ->assertSeeInText($participant->member->lastname) |  | ||||||
|             ->assertSeeInText('Stamm Beispiel'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItRendersSetttingMail(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         app(PreventionSettings::class)->fill([ |  | ||||||
|             'formmail' => EditorRequestFactory::new()->paragraphs(["lorem lala {formname} g", "{wanted}", "bbb"])->toData() |  | ||||||
|         ])->save(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $this->createParticipant($form); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         Mail::assertSent(PreventionRememberMail::class, fn ($mail) => $mail->bodyText->hasAll([ |  | ||||||
|             'lorem lala ' . $form->name, |  | ||||||
|             'erweitertes' |  | ||||||
|         ])); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItAppendsTextOfForm(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         app(PreventionSettings::class)->fill([ |  | ||||||
|             'formmail' => EditorRequestFactory::new()->paragraphs(["::first::"])->toData() |  | ||||||
|         ])->save(); |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $form->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['event'])->toData()]); |  | ||||||
|         $this->createParticipant($form); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         Mail::assertSent(PreventionRememberMail::class, fn ($mail) => $mail->bodyText->hasAll([ |  | ||||||
|             'event' |  | ||||||
|         ])); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDoesntAppendTextTwice(): void |  | ||||||
|     { |  | ||||||
|         Mail::fake(); |  | ||||||
|         app(PreventionSettings::class)->fill(['frommail' => EditorRequestFactory::new()->paragraphs(["::first::"])->toData()])->save(); |  | ||||||
|         tap($this->createForm(), function ($f) { |  | ||||||
|             $f->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['oberhausen'])->toData()]); |  | ||||||
|             $this->createParticipant($f); |  | ||||||
|         }); |  | ||||||
|         tap($this->createForm(), function ($f) { |  | ||||||
|             $f->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['siegburg'])->toData()]); |  | ||||||
|             $this->createParticipant($f); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         PreventionRememberAction::run(); |  | ||||||
| 
 |  | ||||||
|         Mail::assertSent(PreventionRememberMail::class, fn ($mail) => $mail->bodyText->hasAll(['oberhausen']) && !$mail->bodyText->hasAll(['siegburg'])); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public function testItDisplaysBodyTextInMail(): void |  | ||||||
|     { |  | ||||||
|         $form = $this->createForm(); |  | ||||||
|         $participant = $this->createParticipant($form); |  | ||||||
| 
 |  | ||||||
|         $mail = new PreventionRememberMail($participant, EditorRequestFactory::new()->paragraphs(['ggtt'])->toData()); |  | ||||||
|         $mail->assertSeeInText('ggtt'); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected function createForm(): Form |  | ||||||
|     { |  | ||||||
|         return Form::factory()->fields([ |  | ||||||
|             $this->textField('vorname')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::FIRSTNAME), |  | ||||||
|             $this->textField('nachname')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::LASTNAME), |  | ||||||
|             $this->textField('email')->namiType(NamiType::FIRSTNAME)->specialType(SpecialType::EMAIL), |  | ||||||
|         ])->create(['needs_prevention' => true]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected function createParticipant(Form $form): Participant |  | ||||||
|     { |  | ||||||
|         return Participant::factory()->for($form)->data([ |  | ||||||
|             'vorname' => 'Max', |  | ||||||
|             'nachname' => 'Muster', |  | ||||||
|             'email' => 'mail@a.de', |  | ||||||
|         ])->for(Member::factory()->defaults()->has(Membership::factory()->inLocal('€ LeiterIn', 'Wölfling')))->create(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function createParticipant(Form $form): Participant | ||||||
|  | { | ||||||
|  |     return Participant::factory()->for($form)->data([ | ||||||
|  |         'vorname' => 'Max', | ||||||
|  |         'nachname' => 'Muster', | ||||||
|  |         'email' => 'mail@a.de', | ||||||
|  |     ])->for(Member::factory()->defaults()->has(Membership::factory()->inLocal('€ LeiterIn', 'Wölfling')))->create(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createMember(array $attributes): Member | ||||||
|  | { | ||||||
|  |     return Member::factory()->defaults()->has(Membership::factory()->inLocal('€ LeiterIn', 'Wölfling'))->create($attributes); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | dataset('attributes', fn() => [ | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => null, 'ps_at' => now()], | ||||||
|  |         [Prevention::EFZ] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => null], | ||||||
|  |         [Prevention::PS] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now()->subDay(), 'ps_at' => now()], | ||||||
|  |         [] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subDay()], | ||||||
|  |         [] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now()->subYears(5)->subDay(), 'ps_at' => now()], | ||||||
|  |         [Prevention::EFZ] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => null], | ||||||
|  |         [Prevention::PS] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(5)->subDay()], | ||||||
|  |         [Prevention::MOREPS] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(5)->subDay(), 'more_ps_at' => now()], | ||||||
|  |         [] | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(15), 'more_ps_at' => now()->subYears(5)->subDay()], | ||||||
|  |         [Prevention::MOREPS], | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => false, 'efz' => now(), 'ps_at' => now()], | ||||||
|  |         [Prevention::VK], | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     [ | ||||||
|  |         ['has_vk' => true, 'efz' => now(), 'ps_at' => now()->subYears(7)], | ||||||
|  |         [Prevention::MOREPS], | ||||||
|  |     ] | ||||||
|  | ]); | ||||||
|  | 
 | ||||||
|  | it('testItRemembersWhenNotRememberedYet', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberPastEvents', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $form->update(['from' => now()->subDay()]); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertNull($participant->fresh()->last_remembered_at); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberWhenConditionDoesntMatch', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $form->update(['prevention_conditions' => Condition::from(['mode' => 'all', 'ifs' => [['field' => 'vorname', 'comparator' => 'isEqual', 'value' => 'Max']]])]); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $participant->update(['data' => [...$participant->data, 'vorname' => 'Jane']]); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertNull($participant->fresh()->last_remembered_at); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItRemembersWhenRememberIsDue', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = tap(createParticipant($form), fn($p) => $p->update(['last_remembered_at' => now()->subWeeks(3)])); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertEquals(now()->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberWhenRememberingIsNotDue', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = tap(createParticipant($form), fn($p) => $p->update(['last_remembered_at' => now()->subWeeks(1)])); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertEquals(now()->subWeeks(1)->format('Y-m-d'), $participant->fresh()->last_remembered_at->format('Y-m-d')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberWhenFormDoesntNeedPrevention', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = tap(createForm(), fn($form) => $form->update(['needs_prevention' => false])); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertNull($participant->fresh()->last_remembered_at); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberWhenParticipantDoesntHaveMember', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $participant->member->delete(); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertNull($participant->fresh()->last_remembered_at); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItRemembersNonLeaders', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $participant->member->memberships->each->delete(); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     $this->assertNotNull($participant->fresh()->last_remembered_at); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | it('testItRemembersMember', function ($attrs, $preventions) { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $participant->member->update($attrs); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     if (count($preventions)) { | ||||||
|  |         Mail::assertSent(PreventionRememberMail::class, fn($mail) => $mail->preventable->preventions() === $preventions); | ||||||
|  |         $this->assertNotNull($participant->fresh()->last_remembered_at); | ||||||
|  |     } else { | ||||||
|  |         Mail::assertNotSent(PreventionRememberMail::class); | ||||||
|  |         $this->assertNull($participant->fresh()->last_remembered_at); | ||||||
|  |     } | ||||||
|  | })->with('attributes'); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntRememberParticipantThatHasNoMail', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     $participant->update(['data' => [...$participant->data, 'email' => '']]); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     Mail::assertNotSent(PreventionRememberMail::class); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItRendersMail', function () { | ||||||
|  |     InvoiceSettings::fake(['from_long' => 'Stamm Beispiel']); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  |     (new PreventionRememberMail($participant, app(PreventionSettings::class)->formmail)) | ||||||
|  |         ->assertSeeInText($participant->member->firstname) | ||||||
|  |         ->assertSeeInText($participant->member->lastname) | ||||||
|  |         ->assertSeeInText('Stamm Beispiel'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItRendersSetttingMail', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     app(PreventionSettings::class)->fill([ | ||||||
|  |         'formmail' => EditorRequestFactory::new()->paragraphs(["lorem lala {formname} g", "{wanted}", "bbb"])->toData() | ||||||
|  |     ])->save(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     createParticipant($form); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     Mail::assertSent(PreventionRememberMail::class, fn($mail) => $mail->bodyText->hasAll([ | ||||||
|  |         'lorem lala ' . $form->name, | ||||||
|  |         'erweitertes' | ||||||
|  |     ])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItAppendsTextOfForm', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     app(PreventionSettings::class)->fill([ | ||||||
|  |         'formmail' => EditorRequestFactory::new()->paragraphs(["::first::"])->toData() | ||||||
|  |     ])->save(); | ||||||
|  |     $form = createForm(); | ||||||
|  |     $form->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['event'])->toData()]); | ||||||
|  |     createParticipant($form); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     Mail::assertSent(PreventionRememberMail::class, fn($mail) => $mail->bodyText->hasAll([ | ||||||
|  |         'event' | ||||||
|  |     ])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDoesntAppendTextTwice', function () { | ||||||
|  |     Mail::fake(); | ||||||
|  |     app(PreventionSettings::class)->fill(['frommail' => EditorRequestFactory::new()->paragraphs(["::first::"])->toData()])->save(); | ||||||
|  |     tap(createForm(), function ($f) { | ||||||
|  |         $f->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['oberhausen'])->toData()]); | ||||||
|  |         createParticipant($f); | ||||||
|  |     }); | ||||||
|  |     tap(createForm(), function ($f) { | ||||||
|  |         $f->update(['prevention_text' => EditorRequestFactory::new()->paragraphs(['siegburg'])->toData()]); | ||||||
|  |         createParticipant($f); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     PreventionRememberAction::run(); | ||||||
|  | 
 | ||||||
|  |     Mail::assertSent(PreventionRememberMail::class, fn($mail) => $mail->bodyText->hasAll(['oberhausen']) && !$mail->bodyText->hasAll(['siegburg'])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('testItDisplaysBodyTextInMail', function () { | ||||||
|  |     $form = createForm(); | ||||||
|  |     $participant = createParticipant($form); | ||||||
|  | 
 | ||||||
|  |     $mail = new PreventionRememberMail($participant, EditorRequestFactory::new()->paragraphs(['ggtt'])->toData()); | ||||||
|  |     $mail->assertSeeInText('ggtt'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('displays text in yearly mail', function () { | ||||||
|  |     $member = createMember([]); | ||||||
|  |     $mail = new YearlyMail($member, EditorRequestFactory::new()->paragraphs(['ggtt'])->toData()); | ||||||
|  |     $mail->assertSeeInText('ggtt'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | @ -5,36 +5,33 @@ namespace Tests\Feature\Prevention; | ||||||
| use App\Prevention\PreventionSettings; | use App\Prevention\PreventionSettings; | ||||||
| use Illuminate\Foundation\Testing\DatabaseTransactions; | use Illuminate\Foundation\Testing\DatabaseTransactions; | ||||||
| use Tests\RequestFactories\EditorRequestFactory; | use Tests\RequestFactories\EditorRequestFactory; | ||||||
| use Tests\TestCase; |  | ||||||
| 
 | 
 | ||||||
| class SettingTest extends TestCase | uses(DatabaseTransactions::class); | ||||||
| { |  | ||||||
| 
 | 
 | ||||||
|     use DatabaseTransactions; | it('testItOpensSettingsPage', function () { | ||||||
|  |     test()->login()->loginNami(); | ||||||
| 
 | 
 | ||||||
|     public function testItOpensSettingsPage(): void |     test()->get('/setting/prevention')->assertComponent('setting/Prevention')->assertOk(); | ||||||
|     { | }); | ||||||
|         $this->login()->loginNami(); |  | ||||||
| 
 | 
 | ||||||
|         $this->get('/setting/prevention')->assertComponent('setting/Prevention')->assertOk(); | it('receives settings', function () { | ||||||
|     } |     test()->login()->loginNami(); | ||||||
| 
 | 
 | ||||||
|     public function testItReceivesSettings(): void |     $text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData(); | ||||||
|     { |     $yearlyMail = EditorRequestFactory::new()->text(50, 'lala dd')->toData(); | ||||||
|         $this->login()->loginNami(); |     app(PreventionSettings::class)->fill(['formmail' => $text, 'yearlymail' => $yearlyMail])->save(); | ||||||
| 
 | 
 | ||||||
|         $text = EditorRequestFactory::new()->text(50, 'lorem ipsum')->toData(); |     test()->get('/api/prevention') | ||||||
|         app(PreventionSettings::class)->fill(['formmail' => $text])->save(); |         ->assertJsonPath('data.formmail.blocks.0.data.text', 'lorem ipsum') | ||||||
|  |         ->assertJsonPath('data.yearlymail.blocks.0.data.text', 'lala dd'); | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
|         $this->get('/api/prevention') | it('testItStoresSettings', function () { | ||||||
|             ->assertJsonPath('data.formmail.blocks.0.data.text', 'lorem ipsum'); |     test()->login()->loginNami(); | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public function testItStoresSettings(): void |     $formmail = EditorRequestFactory::new()->text(50, 'new lorem')->create(); | ||||||
|     { |     $yearlyMail = EditorRequestFactory::new()->text(50, 'lala dd')->create(); | ||||||
|         $this->login()->loginNami(); |     test()->post('/api/prevention', ['formmail' => $formmail, 'yearlymail' => $yearlyMail])->assertOk(); | ||||||
| 
 |     test()->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem'])); | ||||||
|         $this->post('/api/prevention', ['formmail' => EditorRequestFactory::new()->text(50, 'new lorem')->create()])->assertOk(); |     test()->assertTrue(app(PreventionSettings::class)->yearlymail->hasAll(['lala dd'])); | ||||||
|         $this->assertTrue(app(PreventionSettings::class)->formmail->hasAll(['new lorem'])); | }); | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue