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 () => {