Handling ICAL import

This commit is contained in:
2026-04-25 16:50:32 +02:00
parent 6f8be58943
commit 1ee6b9968f
22 changed files with 510 additions and 19 deletions

View File

@@ -20,7 +20,8 @@ class DashboardController extends CommonController {
private function renderForLoggedInUser(Request $request) {
$authCheckProvider = new AuthCheckProvider;
$inertiaProvider = new InertiaProvider('Dashboard/Dashboard', [
'myInvoices' => $this->invoices->getMyInvoicesWidget()
'myInvoices' => $this->invoices->getMyInvoicesWidget(),
'myParticipations' => $this->eventParticipants->getMyParticipations(),
]);
return $inertiaProvider->render();
@@ -38,4 +39,9 @@ class DashboardController extends CommonController {
$costUnits = $this->costUnits->listForSummary(5);
return response()->json(['openCostUnits' => $costUnits]);
}
public function getMyParticipations() : JsonResponse {
//dd($this->eventParticipants->getMyParticipations());
return response()->json(['myParticipations' => $this->eventParticipants->getMyParticipations()]);
}
}

View File

@@ -9,6 +9,7 @@ Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('api/v1/dashboard')->group(function () {
Route::get('/my-invoices', [DashboardController::class, 'getMyInvoices']);
Route::get('/open-cost-units', [DashboardController::class, 'getOpenCostUnits']);
Route::get('/my-participations', [DashboardController::class, 'getMyParticipations']);
});
});

View File

@@ -2,9 +2,11 @@
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue'
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import MyInvoices from "./Partials/Widgets/MyInvoices.vue";
import MyParticipations from "./Partials/Widgets/MyParticipations.vue";
const props = defineProps({
myInvoices: Object,
myParticipations: Object,
})
@@ -18,10 +20,10 @@ function newInvoice() {
<AppLayout title='Dashboard'>
<diV class="dashboard-widget-container">
<shadowed-box class="dashboard-widget-box" style="width: 60%;">
Meine Anmeldungen
<MyParticipations />
</shadowed-box>
<shadowed-box class="dashboard-widget-box">
<shadowed-box class="dashboard-widget-box" style="height: 275px;">
<MyInvoices />
<input type="button" value="Neue Abrechnung" @click="newInvoice" style="margin-top: 20px;">
@@ -45,4 +47,15 @@ function newInvoice() {
flex-grow: 1; display: inline-block;
margin: 0 10px;
}
.dashboard-widget-box h2 {
border-color: #c0c0c0;
border-left-width: 40px;
border-left-style: solid;
border-bottom-style: solid;
border-bottom-width: 1px;
padding: 5px 10px;
font-size: 13pt;
}
</style>

View File

@@ -16,6 +16,7 @@ onMounted(async () => {
</script>
<template>
<h2>Meine Abrechnungen</h2>
<p v-for="invoice in myInvoices.myInvoices" class="widget-content-item">
<a :href="'/invoice/my-invoices/' + invoice.slug" class="link">{{invoice.title}} ({{invoice.count}})</a>
<label>

View File

@@ -0,0 +1,68 @@
<script setup>
import {onMounted, reactive} from "vue";
import Icon from "../../../../../Views/Components/Icon.vue";
const myParticipations = reactive({
'myParticipations': '',
})
onMounted(async () => {
const response = await fetch('/api/v1/dashboard/my-participations');
const data = await response.json();
Object.assign(myParticipations, data);
});
</script>
<template>
<h2>Meine Anmeldungen</h2>
<p v-for="participation in myParticipations.myParticipations" class="widget-content-item">
<table>
<tr>
<td style="width: 40%; font-weight: bold;">{{participation.eventName}}</td>
<td style="width: 30%; font-weight: bold;">{{participation.arrivalDateReadable}} - {{participation.departureDateReadable}}</td>
<td style="width: 30%;">
<Icon name="euro-sign" style="padding: 5px; font-size: 11pt; color: #ffffff; margin-right: 5px;" :class="participation.needs_payment ? 'bg-red' : 'bg-green'" />
<Icon name="award" style="padding: 5px; font-size: 11pt; color: #ffffff; margin-right: 5px;" :class="participation.cocColor" />
</td>
</tr>
<tr>
<td>
{{participation.event.postal_code}} {{participation.event.location}}<br />
</td>
<td>
<a class="link" :href="`/api/v1/event/participant/${participation.identifier}/ical`">In Kalender importieren</a>
</td>
<td>
{{participation.amountPaid.readable}} / {{participation.amountExpected.readable}}
</td>
</tr>
<tr>
<td colspan="2">&nbsp;</td>
<td>eFZ-Status: {{participation.efzStatusReadable}}</td>
</tr>
</table>
</p>
<p v-if="myParticipations.myParticipations.length === 0">Du bist aktuelle für keine Veranstaltung angemeldet.</p>
<p>
<input type="button" value="Für weitere Veranstaltung anmelden" class="button" />
</p>
</template>
<style scoped>
.bg-red {
background-color: red;
}
.bg-green {
background-color: green;
}
.bg-yellow {
background-color: #f3f353;
}
</style>

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
class GenerateIcalCommand
{
public function __construct(public GenerateIcalRequest $request)
{
}
public function execute(): GenerateIcalResponse
{
$participant = $this->request->participant;
$event = $participant->event;
$uid = $participant->identifier . '@' . app('tenant')->slug;
$dtStart = $event->start_date->format('Ymd');
$dtEnd = $event->end_date->copy()->addDay()->format('Ymd');
$now = now()->format('Ymd\THis\Z');
$summary = $this->escapeIcal($event->name);
$location = $this->escapeIcal(trim($event->postal_code . ' ' . $event->location));
$description = $this->escapeIcal('Teilnahme als: ' . $participant->getOfficialName());
$icalContent = implode("\r\n", [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//' . app('tenant')->name . '//Veranstaltungskalender//DE',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VEVENT',
'UID:' . $uid,
'DTSTAMP:' . $now,
'DTSTART;VALUE=DATE:' . $dtStart,
'DTEND;VALUE=DATE:' . $dtEnd,
'SUMMARY:' . $summary,
'LOCATION:' . $location,
'DESCRIPTION:' . $description,
'END:VEVENT',
'END:VCALENDAR',
]) . "\r\n";
$filename = 'veranstaltung-' . $participant->event()->first()->name . '.ics';
return new GenerateIcalResponse($icalContent, $filename);
}
private function escapeIcal(string $value): string
{
return str_replace(
['\\', ';', ',', "\n"],
['\\\\', '\\;', '\\,', '\\n'],
$value
);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
use App\Models\EventParticipant;
class GenerateIcalRequest
{
public function __construct(public EventParticipant $participant)
{
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcal;
class GenerateIcalResponse
{
public function __construct(
public string $icalContent,
public string $filename,
) {
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
class GenerateIcalForDeadlineCommand {
public function __construct(public GenerateIcalForDeadlineRequest $request)
{
}
public function execute(): GenerateIcalForDeadlineResponse
{
$event = $this->request->event;
$deadlineDate = $event->registration_final_end->copy()->subDays(2);
$dtDate = $deadlineDate->format('Ymd');
$now = now()->format('Ymd\THis\Z');
$summary = $this->escapeIcal('Zahlungsfrist: ' . $event->name);
$description = $this->escapeIcal(
'Bitte überweise den Teilnahmebeitrag für "' . $event->name . '" bis zu diesem Datum.'
);
$icalContent = implode("\r\n", [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//' . app('tenant')->name . '//Veranstaltungskalender//DE',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'BEGIN:VEVENT',
'UID:payment-deadline-' . $event->identifier . '@' . app('tenant')->slug,
'DTSTAMP:' . $now,
'DTSTART;VALUE=DATE:' . $dtDate,
'DTEND;VALUE=DATE:' . $dtDate,
'SUMMARY:' . $summary,
'DESCRIPTION:' . $description,
'END:VEVENT',
'END:VCALENDAR',
]) . "\r\n";
$filename = 'zahlungsziel-' . $event->name . '.ics';
return new GenerateIcalForDeadlineResponse($icalContent, $filename);
}
private function escapeIcal(string $value): string
{
return str_replace(
['\\', ';', ',', "\n"],
['\\\\', '\\;', '\\,', '\\n'],
$value
);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
use App\Models\Event;
class GenerateIcalForDeadlineRequest {
public function __construct(public Event $event) {
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Event\Actions\GenerateIcalForDeadline;
class GenerateIcalForDeadlineResponse
{
public function __construct(
public string $icalContent,
public string $filename,
) {
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalCommand;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalRequest;
use App\Models\EventParticipant;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ParticipantIcalController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request): Response
{
$participant = $this->eventParticipants->getMyParticipationByIdentifier($participantIdentifier);
if ($participant === null) {
abort(403, 'Zugriff verweigert.');
}
$icalRequest = new GenerateIcalRequest($participant);
$icalCommand = new GenerateIcalCommand($icalRequest);
$icalResponse = $icalCommand->execute();
return response($icalResponse->icalContent, 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"',
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Domains\Event\Controllers;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalCommand;
use App\Domains\Event\Actions\GenerateIcal\GenerateIcalRequest;
use App\Domains\Event\Actions\GenerateIcalForDeadline\GenerateIcalForDeadlineCommand;
use App\Domains\Event\Actions\GenerateIcalForDeadline\GenerateIcalForDeadlineRequest;
use App\Models\EventParticipant;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ParticipantIcalForPaymentController extends CommonController
{
public function __invoke(string $participantIdentifier, Request $request): Response
{
$participant = $this->eventParticipants->getMyParticipationByIdentifier($participantIdentifier);
if ($participant === null) {
abort(403, 'Zugriff verweigert.');
}
$icalRequest = new GenerateIcalForDeadlineRequest($participant->event()->first());
$icalCommand = new GenerateIcalForDeadlineCommand($icalRequest);
$icalResponse = $icalCommand->execute();
return response($icalResponse->icalContent, 200, [
'Content-Type' => 'text/calendar; charset=utf-8',
'Content-Disposition' => 'attachment; filename="' . $icalResponse->filename . '"',
]);
}
}

View File

@@ -159,12 +159,6 @@ class SignupController extends CommonController {
'status' => 'success',
]
);
dd($eventId, $registrationData, $amount);
}
public function calculateAmount(int $eventId, Request $request, bool $forDisplay = true) : JsonResponse | float {

View File

@@ -5,6 +5,8 @@ use App\Domains\Event\Controllers\DetailsController;
use App\Domains\Event\Controllers\MailCompose\ByGroupController;
use App\Domains\Event\Controllers\MailCompose\SendController;
use App\Domains\Event\Controllers\ParticipantController;
use App\Domains\Event\Controllers\ParticipantIcalController;
use App\Domains\Event\Controllers\ParticipantIcalForPaymentController;
use App\Domains\Event\Controllers\ParticipantPaymentController;
use App\Domains\Event\Controllers\ParticipantReSignOnController;
use App\Domains\Event\Controllers\ParticipantSignOffController;
@@ -45,6 +47,8 @@ Route::prefix('api/v1')
Route::prefix('/participant/{participantIdentifier}')->group(function () {
Route::get('/', ParticipantController::class);
Route::get('/ical', ParticipantIcalController::class);
Route::get('/ical-payment', ParticipantIcalForPaymentController::class);
Route::post('/payment-complete', [ParticipantPaymentController::class, 'paymentComplete']);
Route::post('/partial-payment', [ParticipantPaymentController::class, 'partialPaymentComplete']);
Route::post('/mark-coc-existing', [ParticipantController::class, 'markCocExisting']);

View File

@@ -17,6 +17,9 @@ console.log(props.participant)
<tr><td>Anreise:</td><td>{{ props.participant.arrival }}</td></tr>
<tr><td>Abreise:</td><td>{{ props.participant.departure }}</td></tr>
<tr><td>Teilnahmegruppe:</td><td>{{ props.participant.participationType }}</td></tr>
<tr><td>&nbsp;</td><td>
<a class="link" :href="`/api/v1/event/participant/${props.participant.identifier}/ical`">In Kalender importieren</a>
</td></tr>
</table>
<div v-if="props.participant.efz_status === 'NOT_CHECKED'" style="font-weight: bold; color: #b45309; margin-bottom: 20px;">
@@ -38,6 +41,9 @@ console.log(props.participant)
<tr><td>IBAN:</td><td>{{ props.event.accountIban }}</td></tr>
<tr><td>Verwendungszweck:</td><td>{{ props.participant.payment_purpose }}</td></tr>
<tr><td>Betrag:</td><td><strong>{{ props.participant.amount_left_string }}</strong></td></tr>
<tr><td>&nbsp;</td><td>
<a class="link" :href="`/api/v1/event/participant/${props.participant.identifier}/ical-payment`">Erinnerung in Kalender setzen</a>
</td></tr>
</table>
<p>