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; use function Symfony\Component\String\b;
class DetailsController extends CommonController { class DetailsController extends CommonController {
public function __invoke(int $eventId) { public function __invoke(string $eventId) {
$event = $this->events->getById($eventId); $event = $this->events->getByIdentifier($eventId);
return new InertiaProvider('Event/Details', ['event' => $event])->render(); 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\ParticipantReSignOnController;
use App\Domains\Event\Controllers\ParticipantSignOffController; use App\Domains\Event\Controllers\ParticipantSignOffController;
use App\Domains\Event\Controllers\ParticipantUpdateController; use App\Domains\Event\Controllers\ParticipantUpdateController;
use App\Domains\Event\Controllers\PaymentReminderController;
use App\Domains\Event\Controllers\SignupController; use App\Domains\Event\Controllers\SignupController;
use App\Middleware\IdentifyTenant; use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -28,6 +29,8 @@ Route::prefix('api/v1')
Route::post('/send', SendController::class); Route::post('/send', SendController::class);
}); });
Route::get('{eventIdentifier}/send-payment-reminder', PaymentReminderController::class);
Route::prefix('/details/{eventId}') ->group(function () { Route::prefix('/details/{eventId}') ->group(function () {
Route::get('/summary', [DetailsController::class, 'summary']); Route::get('/summary', [DetailsController::class, 'summary']);

View File

@@ -161,17 +161,16 @@
}); });
if (data.status !== 'success') { if (data.status !== 'success') {
toas.error(data.message); toast.error(data.message);
return false; return false;
} else { } else {
console.log(data.event);
newEvent.value = data.event; newEvent.value = data.event;
showParticipationFees.value = true; showParticipationFees.value = true;
} }
} }
async function finishCreation() { 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 TextEditor from "../../../../Views/Components/TextEditor.vue";
import ErrorText from "../../../../Views/Components/ErrorText.vue"; import ErrorText from "../../../../Views/Components/ErrorText.vue";
import {toast} from "vue3-toastify"; import {toast} from "vue3-toastify";
import InfoText from "../../../../Views/Components/InfoText.vue";
const { request } = useAjax(); const { request } = useAjax();
@@ -37,6 +38,7 @@ onMounted(async () => {
}); });
const errorMessage = ref(null) const errorMessage = ref(null)
const infoMessage = ref(null)
const emit = defineEmits([ const emit = defineEmits([
'closeComposer', 'closeComposer',
@@ -49,6 +51,10 @@ function close() {
async function sendMail() { 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', { const response = await request('/api/v1/event/' + props.event.identifier + '/mailing/send', {
method: "POST", method: "POST",
body: { body: {
@@ -59,10 +65,14 @@ async function sendMail() {
}); });
if (response.success) { if (response.success) {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
close(); close();
toast.success(response.message) toast.success(response.message)
} else { } else {
infoMessage.value = null
document.getElementById('sendMessageButton').style.display = 'block';
errorMessage.value = response.message errorMessage.value = response.message
toast.error(response.message) toast.error(response.message)
} }
@@ -94,8 +104,8 @@ const form = reactive({
<strong><ErrorText :message="errorMessage" /></strong> <strong><ErrorText :message="errorMessage" /></strong>
</div> </div>
<info-text :message="infoMessage" />
<input type="button" @click="sendMail" value="Senden" class="" /> <input type="button" id="sendMessageButton" @click="sendMail" value="Senden" class="" />
</template> </template>

View File

@@ -7,6 +7,7 @@
import Modal from "../../../../Views/Components/Modal.vue"; import Modal from "../../../../Views/Components/Modal.vue";
import MailCompose from "./MailCompose.vue"; import MailCompose from "./MailCompose.vue";
import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue"; import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue";
import {toast} from "vue3-toastify";
const props = defineProps({ const props = defineProps({
data: Object, data: Object,
@@ -54,6 +55,17 @@
function mailToGroup() { function mailToGroup() {
mailCompose.value = true 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> </script>
<template> <template>
@@ -90,8 +102,8 @@
<a :href="'/event/details/' + props.data.event.identifier + '/pdf/photo-permission-list'"> <a :href="'/event/details/' + props.data.event.identifier + '/pdf/photo-permission-list'">
<input type="button" value="Foto-Erlaubnis (PDF)" /> <input type="button" value="Foto-Erlaubnis (PDF)" />
</a><br/> </a><br/>
<input type="button" class="fix-button" value="Zahlungserinnerung senden" /><br/> <input type="button" @click="sendPaymentReminder" class="fix-button" value="Zahlungserinnerung senden" /><br/>
<input type="button" class="deny-button" value="Letzte Mahnung 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/> <input type="button" value="Rundmail senden" @click="mailToGroup" /><br/>
</div> </div>
</div> </div>

View File

@@ -126,7 +126,7 @@ class GlobalDataProvider {
foreach ($eventRepository->listAll() as $event) { 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']; $navigation['events'][] = ['url' => '/event/new', 'display' => 'Neue Veranstaltung'];

View File

@@ -75,6 +75,17 @@ class EventParticipantRepository {
return $participants; 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 { public function getParticipantsWithIntolerances(Event $event, Request $request) : array {
$participants = []; $participants = [];
foreach ($event->participants()->whereNotNull('intolerances')->whereNot('intolerances' , '=', '')->get() as $participant) { 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>

View File

@@ -68,6 +68,12 @@ input[type="submit"]:hover,
color: #ffffff !important; color: #ffffff !important;
} }
.info_text {
color: #3369d3;
display: block;
font-weight: bold;
}
.error_text { .error_text {
color: red; color: red;
display: block; display: block;