Payment reminder mails

This commit is contained in:
2026-04-18 22:09:57 +02:00
parent ff98f0860c
commit 33a9271013
13 changed files with 157 additions and 10 deletions

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
use App\Mail\ParticipantPaymentMails\ParticipantPaymentMissingPaymentMail;
use Illuminate\Support\Facades\Mail;
class SendMissingPaymentMailsCommand {
function __construct(public SendMissingPaymentMailsRequest $request) {}
public function execute() : SendMissingPaymentMailsResponse {
$response = new SendMissingPaymentMailsResponse();
foreach ($this->request->eventParticipants->getParticipantsWithMissingPayments($this->request->event, $this->request->httpRequest) as $participant) {
$participantResource = $participant->toResource()->toArray($this->request->httpRequest);
if (!$participantResource['needs_payment']) {
continue;
}
Mail::to($participant->email_1)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
if ($participant->email_2 !== null && $participant->email_2 !== $participant->email_1) {
Mail::to($participant->email_2)->send(new ParticipantPaymentMissingPaymentMail(
participant: $participant,
));
}
$response->remindedParticipants++;
}
$response->success = true;
return $response;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
use App\Models\Event;
use App\Repositories\EventParticipantRepository;
use Illuminate\Http\Request;
class SendMissingPaymentMailsRequest {
function __construct(
public Event $event,
public EventParticipantRepository $eventParticipants,
public Request $httpRequest
) {
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Event\Actions\SendMissingPaymentMails;
class SendMissingPaymentMailsResponse {
function __construct(
public bool $success = false,
public int $remindedParticipants = 0
) {}
}

View File

@@ -22,8 +22,8 @@ use Illuminate\Http\Response;
use function Symfony\Component\String\b;
class DetailsController extends CommonController {
public function __invoke(int $eventId) {
$event = $this->events->getById($eventId);
public function __invoke(string $eventId) {
$event = $this->events->getByIdentifier($eventId);
return new InertiaProvider('Event/Details', ['event' => $event])->render();
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\SendMissingPaymentMails\SendMissingPaymentMailsCommand;
use App\Domains\Event\Actions\SendMissingPaymentMails\SendMissingPaymentMailsRequest;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
class PaymentReminderController extends CommonController
{
public function __invoke(string $eventIdentifier, Request $request)
{
$event = $this->events->getByIdentifier($eventIdentifier, true);
$sendPaymentReminderMailsRequest = new SendMissingPaymentMailsRequest(
event: $event,
eventParticipants: $this->eventParticipants,
httpRequest: $request
);
$sendPaymentReminderMailsCommand = new SendMissingPaymentMailsCommand(request: $sendPaymentReminderMailsRequest);
$sendPaymentReminderResponse = $sendPaymentReminderMailsCommand->execute();
return response()->json([
'success' => $sendPaymentReminderResponse->success,
'message' => $sendPaymentReminderResponse->success ?
sprintf('Es wurden %1$s Personen über fehlende Teilnahmebeiträge informiert', $sendPaymentReminderResponse->remindedParticipants) :
'Beim Senden der Benachrichtigungen ist ein Fehler aufgetreten.',
]);
}
}

View File

@@ -9,6 +9,7 @@ use App\Domains\Event\Controllers\ParticipantPaymentController;
use App\Domains\Event\Controllers\ParticipantReSignOnController;
use App\Domains\Event\Controllers\ParticipantSignOffController;
use App\Domains\Event\Controllers\ParticipantUpdateController;
use App\Domains\Event\Controllers\PaymentReminderController;
use App\Domains\Event\Controllers\SignupController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
@@ -28,6 +29,8 @@ Route::prefix('api/v1')
Route::post('/send', SendController::class);
});
Route::get('{eventIdentifier}/send-payment-reminder', PaymentReminderController::class);
Route::prefix('/details/{eventId}') ->group(function () {
Route::get('/summary', [DetailsController::class, 'summary']);

View File

@@ -161,17 +161,16 @@
});
if (data.status !== 'success') {
toas.error(data.message);
toast.error(data.message);
return false;
} else {
console.log(data.event);
newEvent.value = data.event;
showParticipationFees.value = true;
}
}
async function finishCreation() {
window.location.href = '/event/details/' + newEvent.value.id;
window.location.href = '/event/details/' + newEvent.value.identifier;
}

View File

@@ -4,6 +4,7 @@ import {useAjax} from "../../../../../resources/js/components/ajaxHandler.js";
import TextEditor from "../../../../Views/Components/TextEditor.vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue";
import {toast} from "vue3-toastify";
import InfoText from "../../../../Views/Components/InfoText.vue";
const { request } = useAjax();
@@ -37,6 +38,7 @@ onMounted(async () => {
});
const errorMessage = ref(null)
const infoMessage = ref(null)
const emit = defineEmits([
'closeComposer',
@@ -49,6 +51,10 @@ function close() {
async function sendMail() {
document.getElementById('sendMessageButton').style.display = 'none';
infoMessage.value = 'Die Rundmail wird nun gesendet. Dies kann einen Moment dauern. Bitte verlasse diese Seite nicht.'
toast.info('Die Rundmail wird nun gesendet. Dies kann einen Moment dauern. Bitte verlasse diese Seite nicht.')
const response = await request('/api/v1/event/' + props.event.identifier + '/mailing/send', {
method: "POST",
body: {
@@ -59,10 +65,14 @@ async function sendMail() {
});
if (response.success) {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
close();
toast.success(response.message)
} else {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
errorMessage.value = response.message
toast.error(response.message)
}
@@ -94,8 +104,8 @@ const form = reactive({
<strong><ErrorText :message="errorMessage" /></strong>
</div>
<input type="button" @click="sendMail" value="Senden" class="" />
<info-text :message="infoMessage" />
<input type="button" id="sendMessageButton" @click="sendMail" value="Senden" class="" />
</template>

View File

@@ -7,6 +7,7 @@
import Modal from "../../../../Views/Components/Modal.vue";
import MailCompose from "./MailCompose.vue";
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import {toast} from "vue3-toastify";
const props = defineProps({
data: Object,
@@ -54,6 +55,17 @@
function mailToGroup() {
mailCompose.value = true
}
async function sendPaymentReminder() {
toast.info("Die Nachrichten werden gesendet. Bitte verlasse diese Seite nicht.");
const response = await fetch("/api/v1/event/" + props.data.event.identifier + "/send-payment-reminder/" );
const data = await response.json();
if (data.success) {
toast.success(data.message)
} else {
toast.error(data.message)
}
}
</script>
<template>
@@ -90,8 +102,8 @@
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/photo-permission-list'">
<input type="button" value="Foto-Erlaubnis (PDF)" />
</a><br/>
<input type="button" class="fix-button" value="Zahlungserinnerung senden" /><br/>
<input type="button" class="deny-button" value="Letzte Mahnung senden" /><br/>
<input type="button" @click="sendPaymentReminder" class="fix-button" value="Zahlungserinnerung senden" /><br/>
<input type="button" class="deny-button" value="Letzte Mahnung senden" style="display: none" /><br/>
<input type="button" value="Rundmail senden" @click="mailToGroup" /><br/>
</div>
</div>

View File

@@ -126,7 +126,7 @@ class GlobalDataProvider {
foreach ($eventRepository->listAll() as $event) {
$navigation['events'][] = ['url' => '/event/details/' . $event->id, 'display' => $event->name];
$navigation['events'][] = ['url' => '/event/details/' . $event->identifier, 'display' => $event->name];
}
$navigation['events'][] = ['url' => '/event/new', 'display' => 'Neue Veranstaltung'];

View File

@@ -75,6 +75,17 @@ class EventParticipantRepository {
return $participants;
}
public function getParticipantsWithMissingPayments(Event$event, Request $request) : array {
$participants = [];
foreach ($event->participants()->whereNull('unregistered_at')
->whereColumn('amount', '<>', 'amount_paid')
->get() as $participant) {
$participants[] = $participant;
};
return $participants;
}
public function getParticipantsWithIntolerances(Event $event, Request $request) : array {
$participants = [];
foreach ($event->participants()->whereNotNull('intolerances')->whereNot('intolerances' , '=', '')->get() as $participant) {

View File

@@ -0,0 +1,10 @@
<script setup>
const props = defineProps({
message: String,
})
</script>
<template>
<small style="margin-bottom: 20px;" class="info_text" v-if="props.message">{{ props.message }}</small>
</template>