From 1ee6b9968fdd181368ec10ffc3f8592bbd945b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sat, 25 Apr 2026 16:50:32 +0200 Subject: [PATCH] Handling ICAL import --- .ai/conventions.md | 71 +++++++++++++++++++ .../Controllers/DashboardController.php | 8 ++- app/Domains/Dashboard/Routes/api.php | 1 + app/Domains/Dashboard/Views/Dashboard.vue | 17 ++++- .../Views/Partials/Widgets/MyInvoices.vue | 1 + .../Partials/Widgets/MyParticipations.vue | 68 ++++++++++++++++++ .../GenerateIcal/GenerateIcalCommand.php | 55 ++++++++++++++ .../GenerateIcal/GenerateIcalRequest.php | 12 ++++ .../GenerateIcal/GenerateIcalResponse.php | 12 ++++ .../GenerateIcalForDeadlineCommand.php | 51 +++++++++++++ .../GenerateIcalForDeadlineRequest.php | 11 +++ .../GenerateIcalForDeadlineResponse.php | 12 ++++ .../Controllers/ParticipantIcalController.php | 31 ++++++++ .../ParticipantIcalForPaymentController.php | 33 +++++++++ .../Event/Controllers/SignupController.php | 6 -- app/Domains/Event/Routes/api.php | 4 ++ .../SignUpForm/after-submit/SubmitSuccess.vue | 6 ++ .../EventSignUpSuccessfullMail.php | 66 +++++++++++++++-- .../EventParticipantRepository.php | 43 +++++++++++ app/Resources/EventParticipantResource.php | 11 ++- .../Partials/GlobalWidgets/GlobalWidgets.vue | 4 ++ .../emails/events/signup_complete.blade.php | 6 +- 22 files changed, 510 insertions(+), 19 deletions(-) create mode 100644 .ai/conventions.md create mode 100644 app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue create mode 100644 app/Domains/Event/Actions/GenerateIcal/GenerateIcalCommand.php create mode 100644 app/Domains/Event/Actions/GenerateIcal/GenerateIcalRequest.php create mode 100644 app/Domains/Event/Actions/GenerateIcal/GenerateIcalResponse.php create mode 100644 app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineCommand.php create mode 100644 app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineRequest.php create mode 100644 app/Domains/Event/Actions/GenerateIcalForDeadline/GenerateIcalForDeadlineResponse.php create mode 100644 app/Domains/Event/Controllers/ParticipantIcalController.php create mode 100644 app/Domains/Event/Controllers/ParticipantIcalForPaymentController.php diff --git a/.ai/conventions.md b/.ai/conventions.md new file mode 100644 index 0000000..45ac0f3 --- /dev/null +++ b/.ai/conventions.md @@ -0,0 +1,71 @@ +# Projektkonventionen + +## Architektur: Actions (Request-Command-Response) + +Jede fachliche Operation wird in eine eigene Action ausgelagert, die aus drei Klassen besteht. +Pfad: `app/Domains/{Domain}/Actions/{ActionName}/` + +### Struktur +{ActionName}Request.php → Eingabedaten (Konstruktor oder Factory-Methoden) {ActionName}Command.php → Logik, ruft execute(): {ActionName}Response auf {ActionName}Response.php → Rückgabedaten (public Properties) + + +### Regeln +- Der Controller enthält **keine** fachliche Logik – nur Absicherung, Action-Aufruf und HTTP-Response +- Commands sind nicht statisch und werden immer instanziiert +- Hat ein Request mehrere Varianten, werden **Factory-Methoden** (`forX()`) statt mehrerer Konstruktoren verwendet +- Aufrufreihenfolge im Controller: `new Request → new Command(request) → command->execute() → Response verwenden` + +--- + +## Controller + +- Alle Controller erben von `App\Scopes\CommonController` +- `CommonController` stellt folgende Repositories bereit (keine eigene Instanziierung nötig): + - `$this->eventParticipants` → `EventParticipantRepository` + - `$this->events` → `EventRepository` + - `$this->invoices` → `InvoiceRepository` + - `$this->costUnits` → `CostUnitRepository` + - `$this->users` → `UserRepository` + - `$this->tenant` → aktueller `Tenant` + +--- + +## Repositories + +- Datenbankzugriffe gehören **immer** ins Repository, nie direkt in Controller oder Actions +- Sicherheitschecks (z. B. „gehört diese Teilnahme dem eingeloggten User?") werden als eigene Repository-Methoden gekapselt +- Tenant-Filter: `app('tenant')->slug` +- Eingeloggter User: `auth()->user()` + +--- + +## Models / Ressourcen + +- Models erben von `App\Scopes\InstancedModel` (mit globalem `SiteScope`) +- `$model->toResource()->toArray($request)` liefert das aufbereitete Array über die zugehörige Resource-Klasse +- Resource-Klassen liegen in `app/Resources/{ModelName}Resource.php` + +--- + +## Tenant + +- Der aktuelle Tenant ist per `app('tenant')` verfügbar (gesetzt durch `IdentifyTenant`-Middleware) +- Tenant-Slug: `app('tenant')->slug` +- Jede tenant-spezifische DB-Abfrage filtert auf `['tenant' => app('tenant')->slug]` + +--- + +## Routing + +- API-Routen liegen in `app/Domains/{Domain}/Routes/api.php` +- Alle Routen sind in `IdentifyTenant::class`-Middleware gewrappt +- Authentifizierte Routen zusätzlich in `['auth']`-Middleware + +--- + +## Mails + +- Mails erben von `Illuminate\Mail\Mailable` +- Attachments werden über `Attachment::fromData(fn () => $content, $filename)->withMime(...)` angehängt +- Werden Daten sowohl in `content()` als auch in `attachments()` benötigt, wird eine **private Hilfsmethode mit Lazy-Caching** verwendet (einmaliges Berechnen, Ergebnis in private Property speichern) +- Blade-Templates referenzieren Mail-Attachments per `cid:`-Link: `...` diff --git a/app/Domains/Dashboard/Controllers/DashboardController.php b/app/Domains/Dashboard/Controllers/DashboardController.php index 9572fd5..60e933d 100644 --- a/app/Domains/Dashboard/Controllers/DashboardController.php +++ b/app/Domains/Dashboard/Controllers/DashboardController.php @@ -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()]); + } } diff --git a/app/Domains/Dashboard/Routes/api.php b/app/Domains/Dashboard/Routes/api.php index 2ebe3fa..ff48e23 100644 --- a/app/Domains/Dashboard/Routes/api.php +++ b/app/Domains/Dashboard/Routes/api.php @@ -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']); }); }); diff --git a/app/Domains/Dashboard/Views/Dashboard.vue b/app/Domains/Dashboard/Views/Dashboard.vue index e59a5a3..7acfa70 100644 --- a/app/Domains/Dashboard/Views/Dashboard.vue +++ b/app/Domains/Dashboard/Views/Dashboard.vue @@ -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() {
- Meine Anmeldungen + - + @@ -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; +} + diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue index 2a3f5c7..74020f4 100644 --- a/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue +++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyInvoices.vue @@ -16,6 +16,7 @@ onMounted(async () => {