119 Commits

Author SHA1 Message Date
th.guenther e09987f5a8 Fixed permission management 2026-06-23 18:46:01 +02:00
th.guenther 5c514e9ff5 Merge pull request 'Improvements' (#9) from dev-4.5.1 into main
Reviewed-on: #9
2026-06-22 21:08:28 +02:00
th.guenther bc60461dac Improvements 2026-06-22 21:07:53 +02:00
th.guenther 6848fbd95f Merge pull request 'Development 4.4.2' (#8) from development-4.4.2 into main
Fixed Login for Superuser

Display Events in Widgets until their end date is reached
Prevent linking invoice to user account if foreign payment
Direct link to current event in mobile mode
New SEPA logic
Added legal data
Added admin layout
Admin function for tenant
Tenant management
User management
2026-06-21 23:31:26 +02:00
th.guenther 012ebb6538 Prepared for release 2026-06-21 22:13:23 +02:00
th.guenther c1ef1d71ad Code Styling 2026-06-21 22:12:05 +02:00
th.guenther aebb2f9aaa User management 2026-06-21 21:56:35 +02:00
th.guenther cfc7c7eee2 Tenant management 2026-06-21 20:46:16 +02:00
th.guenther 1b9384dad1 Admin function for tenant 2026-06-21 18:02:01 +02:00
th.guenther fed54514c8 Admin function for tenant 2026-06-21 18:00:20 +02:00
th.guenther 12f05ceb09 Added admin layout 2026-06-21 16:45:24 +02:00
th.guenther 5f56ef94a6 Added admin layout 2026-06-21 16:44:58 +02:00
th.guenther 79bb186234 Added admin layout 2026-06-21 16:44:01 +02:00
th.guenther a012c16425 Added admin layout 2026-06-21 16:42:37 +02:00
th.guenther a8205a4f96 Added legal data 2026-06-21 15:18:26 +02:00
th.guenther dbcebbb2c4 Merge pull request 'New SEPA logic' (#7) from new-sepa-procedure into development-4.4.2
Reviewed-on: #7
2026-06-21 01:29:57 +02:00
th.guenther 63c7b8dfb1 New SEPA logic 2026-06-21 01:28:28 +02:00
th.guenther e9aa66a860 Direct liunk to current event in mobile mode 2026-06-21 00:35:25 +02:00
th.guenther 51c4055c47 Prevent linking invoice to user account if foreign payment 2026-06-20 23:20:42 +02:00
th.guenther 2348663fd8 Display Events in Widgets until their end date is reached 2026-06-20 18:06:29 +02:00
th.guenther a83cec94ab Fixed Login for Superuser 2026-06-20 18:02:00 +02:00
th.guenther 710e27c344 Small Bugfixes 2026-05-26 20:29:01 +02:00
th.guenther 83bbd6f7d3 Merge pull request 'Displaying estimates' (#6) from dev-4.4.0 into main
Implemented Event Budgets
2026-05-26 18:13:35 +02:00
th.guenther 28ffbdb696 Implemented Event Budget 2026-05-26 18:12:42 +02:00
th.guenther 575fb27018 Displaying estimates 2026-05-26 11:07:59 +02:00
th.guenther fe3429cd4e Displaying estimates 2026-05-26 11:07:33 +02:00
th.guenther 551b592b3b Fix 2026-05-24 20:06:01 +02:00
th.guenther 6ed0a5b93a DB update 2026-05-23 21:42:21 +02:00
th.guenther 97fd7cd0da Merge pull request 'Small design improvements' (#5) from dev-4.3.1 into main
Small design Improvements
2026-05-23 21:40:54 +02:00
th.guenther f5d7b21671 Small design improvements 2026-05-23 21:40:06 +02:00
th.guenther 444711b049 Fixes 2026-05-23 21:06:40 +02:00
th.guenther 0a7abb1389 Merge pull request 'New Release' (#4) from dev-4.3.0 into main
Responsive Design
Fixes Crons for Tenants
2026-05-23 19:28:37 +02:00
th.guenther 998a799c3a Fixed issue Crons running for tenants 2026-05-23 19:27:20 +02:00
th.guenther ef8f3ebe6c Small improvements 2026-05-23 19:26:59 +02:00
th.guenther 92976fbf27 New Responsive design 2026-05-23 18:55:41 +02:00
th.guenther e7e7f039b8 New Responsive design 2026-05-23 18:08:35 +02:00
th.guenther 0d436d8190 New Responsive design 2026-05-23 18:08:27 +02:00
th.guenther 3fdbaf0285 Release 2026-05-23 15:58:06 +02:00
th.guenther 8a049efe49 Release 2026-05-14 17:59:22 +02:00
th.guenther a62b2214c4 Merge pull request 'Comments for invoices' (#3) from dev-4.2.0 into main
Comments for invoices
Bugfix: Birthday can be stored
Bugfix: Cronjobs  run correctly
2026-05-14 17:00:56 +02:00
th.guenther f00e665a03 Release 2026-05-14 16:59:25 +02:00
th.guenther 454d83de2e Fixed cronjobs 2026-05-14 16:58:41 +02:00
th.guenther 30cc0b79c5 Bugfix: Birthday can be stored 2026-05-14 16:17:44 +02:00
th.guenther 775d9158a6 Comments for invoices 2026-05-14 16:16:36 +02:00
th.guenther c60429ad28 Merge pull request 'Direct payments for invoices' (#2) from devl-4.1.0 into main
Reviewed-on: #2
2026-05-12 16:05:02 +02:00
th.guenther 0cf9602958 Direct payments for invoices
Events can be moved to archive and moved back
Fixed validation
2026-05-12 16:04:15 +02:00
th.guenther e2fb616565 Fixed issue tetanus vaccination 2026-05-04 14:08:06 +02:00
th.guenther faaf12b6a4 Minor improvements 2026-05-02 00:45:30 +02:00
th.guenther 1a69a20750 Minor improvements 2026-05-02 00:33:43 +02:00
th.guenther 71cb09f051 Improvement for Uploading of receipts 2026-05-02 00:21:54 +02:00
th.guenther b668195e21 Merge remote-tracking branch 'origin/main' 2026-05-01 20:41:56 +02:00
th.guenther 94934027fa Last bugfixes 2026-05-01 20:41:07 +02:00
tux 120c8873ef run-mareike executable 2026-05-01 16:09:47 +02:00
th.guenther 188735e4aa Last bugfixes 2026-05-01 14:04:56 +02:00
th.guenther b291c05da7 Last bugfixes 2026-05-01 13:52:53 +02:00
th.guenther adffa6d6b2 Small improvements 2026-05-01 11:40:39 +02:00
th.guenther 4357fd022d Small improvements 2026-05-01 11:22:57 +02:00
th.guenther d6416d9c6b Small improvements 2026-05-01 11:00:09 +02:00
th.guenther 7c1125e213 Small improvements 2026-04-30 20:39:56 +02:00
th.guenther b91af2e889 Small improvements 2026-04-30 20:30:00 +02:00
th.guenther 4a97cf230b Bugfixes 2026-04-29 22:34:40 +02:00
th.guenther 0c7e3ab365 Bugfixes 2026-04-29 22:26:56 +02:00
th.guenther 222113b0d5 Bugfixes 2026-04-29 21:53:19 +02:00
th.guenther 895e44c984 Merge remote-tracking branch 'origin/main' 2026-04-29 21:51:02 +02:00
th.guenther ef3d03200e Bugfixes 2026-04-29 21:50:05 +02:00
tux 6c891ff64a set prod user 2026-04-29 20:07:10 +02:00
th.guenther d9ba5ab4f1 Merge remote-tracking branch 'origin/main' 2026-04-29 19:43:58 +02:00
th.guenther 1816ca5165 Bugfixes 2026-04-29 19:43:45 +02:00
tux dff7d65476 production deployment stuff 2026-04-29 19:35:11 +02:00
th.guenther ce57669263 Bugfixes 2026-04-29 18:23:28 +02:00
tux 2444aab204 prod docker stuff 2026-04-26 17:46:21 +02:00
th.guenther 7b2f2de1f0 New mareike version 2026-04-26 17:44:56 +02:00
th.guenther 94b1d7b2ac Bugfixes 2026-04-26 17:41:39 +02:00
th.guenther 1f5f6bc32e Bugfixes 2026-04-26 17:08:12 +02:00
th.guenther 9e39435818 Bugfixes 2026-04-26 15:58:24 +02:00
th.guenther 096ba07b4c Bugfixes 2026-04-26 01:56:28 +02:00
th.guenther 5bcdc2fb5d Personal data and password change 2026-04-26 01:15:58 +02:00
th.guenther f4ea07d82c Reduction of amount for siblings implemented 2026-04-25 22:33:48 +02:00
th.guenther 21be212129 Reduction of amount for siblings implemented 2026-04-25 22:31:38 +02:00
th.guenther 8348f677a5 Overview of upcoming events 2026-04-25 21:23:32 +02:00
th.guenther f813056bf7 Widget for own participications 2026-04-25 20:47:34 +02:00
th.guenther 1ee6b9968f Handling ICAL import 2026-04-25 16:50:32 +02:00
th.guenther 6f8be58943 Notification after invoice state 2026-04-25 13:28:23 +02:00
th.guenther 2e8daf78e1 Cronjobs implemented 2026-04-25 00:32:15 +02:00
th.guenther 4878f750bd Improved handling for new invoices 2026-04-18 23:42:39 +02:00
th.guenther 33a9271013 Payment reminder mails 2026-04-18 22:09:57 +02:00
th.guenther ff98f0860c Manual mails can be sent 2026-04-18 20:52:13 +02:00
th.guenther ed7f887e3a Participant mangement 2026-04-11 22:17:38 +02:00
th.guenther e6bd8c684d GUI for Participant management 2026-04-07 22:27:47 +02:00
th.guenther 653e85b781 Show participation details 2026-04-06 20:30:52 +02:00
th.guenther 43f8621053 All Lists finalized 2026-03-29 00:37:03 +01:00
th.guenther 2d17e61cc8 First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:28:55 +01:00
th.guenther 7bea223ded First Aid list, amount list and kitchen list PDF download implemented 2026-03-28 22:22:06 +01:00
th.guenther df7c14442e Fixed bug in real income 2026-03-25 20:55:35 +01:00
th.guenther 33b4a249cc WiP 2026-03-25 20:28:04 +01:00
th.guenther 37039f082c Event identifiers for anonymizations 2026-03-22 00:14:00 +01:00
th.guenther 405591d6dd Signup for events implemented 2026-03-22 00:06:03 +01:00
th.guenther b8341890d3 Basic signup for events 2026-03-21 21:02:15 +01:00
th.guenther 23af267896 Running 2026-03-20 22:50:22 +01:00
th.guenther b1c333648a Bugfixes & Model for participants 2026-02-17 21:58:55 +01:00
th.guenther fcf41c5d13 Creation and editing of events 2026-02-16 21:59:21 +01:00
th.guenther 2b458eccd7 Invoice upload by robot 2026-02-14 00:04:00 +01:00
th.guenther 4f4dff2edd Invoice PAIN & CSV can be uploaded 2026-02-13 22:37:27 +01:00
th.guenther cd526231ed Bugfix 2026-02-13 13:55:20 +01:00
th.guenther fa886aad4d Design improvements 2026-02-13 13:51:07 +01:00
th.guenther f468814a2f Managing own invoices 2026-02-13 12:38:48 +01:00
th.guenther ab711109a7 Pdf Viewer 2026-02-13 10:54:17 +01:00
th.guenther 72623df38f Pdf Viewer 2026-02-13 10:52:40 +01:00
th.guenther 9fd6839878 Small fixes 2026-02-13 10:41:20 +01:00
th.guenther fd403f8520 Operation processes on invoices 2026-02-13 00:11:51 +01:00
th.guenther 882752472e Invoice Widgets completed 2026-02-11 19:38:06 +01:00
th.guenther 87531237c7 Show own invoices 2026-02-11 15:44:43 +01:00
th.guenther ee7fc637f1 Invoices can be uploaded 2026-02-11 15:40:06 +01:00
th.guenther bccfc11687 Cost units can be edited 2026-02-08 20:06:38 +01:00
th.guenther 6fc65e195c API-Route to new global variables 2026-02-05 09:18:24 +01:00
th.guenther e9ae850002 Display nicename in dashboard 2026-02-05 07:40:00 +01:00
th.guenther 11108bdfcc Basic user management 2026-02-05 00:46:22 +01:00
th.guenther e280fcfba8 Basic design created 2026-02-03 09:33:18 +01:00
th.guenther 3570f442f5 Basic tenant structure 2026-01-31 20:07:41 +01:00
738 changed files with 209695 additions and 894 deletions
+81
View File
@@ -0,0 +1,81 @@
# 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`
- Die Controller besitzen ausschließlich eine __invoke() - Funktion
- Für die Speichern-Actions werden separate Controller-Klassen erstellt (z. B. `StoreEventParticipantController`)
---
## 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: `<a href="cid:{{ $filename }}">...</a>`
## Actions
- Die Actions sind in `app/Domains/{Domain}/Actions/{ActionName}/`-Verzeichnissen organisiert
- Jede Action besitzt einen Request, einen Command und einen Response
- Der Request besitzt auschließlich einen Konstruktor, der die notwendigen Parameter annimmt
- Die Response-Klasse enthält ausschließlich die notwendigen Daten für die Antwort
- Die Action-Klasse enthält die Logik für die Verarbeitung der Anfrage und die Generierung der Antwort
- Die Logik wird in einer execute() - Funktion innerhalb des Commands impplementiert. Private Funktionen, für ausgelagerte Prozesse sind zulässig, wenn der Code damit lesbarer wird.
View File
+5
View File
@@ -0,0 +1,5 @@
.ai
.junie
.git
storage
tests
+1
View File
@@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
/docker-compose.yaml
+12
View File
@@ -0,0 +1,12 @@
# Project Guidelines
This is a placeholder of the project guidelines for Junie.
Replace this text with any project-level instructions for Junie, e.g.:
* What is the project structure
* Whether Junie should run tests to check the correctness of the proposed solution
* How does Junie run tests (once it requires any non-standard approach)
* Whether Junie should build the project before submitting the result
* Any code-style related instructions
As an option you can ask Junie to create these guidelines for you.
+5
View File
@@ -2,6 +2,11 @@
FRONTEND_DIR ?= .
setup:
rm -f docker-compose.yaml
cp docker-compose.dev docker-compose.yaml
docker-compose up -d
frontend:
@cd $(FRONTEND_DIR) && \
export QT_QPA_PLATFORM=offscreen && \
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Casts;
use App\ValueObjects\Amount;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class AmountCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Amount
{
if ($value === null) {
return null;
}
return new Amount((float) $value, 'Euro');
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?float
{
if ($value === null) {
return null;
}
if ($value instanceof Amount) {
return $value->getAmount();
}
return (float) $value;
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Domains\Admin\Actions\CreateTenant;
use App\Models\Tenant;
class CreateTenantAction
{
public function __construct(private CreateTenantRequest $request)
{
}
public function execute(): CreateTenantResponse
{
$response = new CreateTenantResponse();
$tenant = Tenant::create([
'name' => $this->request->name,
'slug' => $this->request->slug,
'url' => $this->request->url,
'email' => '',
'email_finance' => '',
'account_name' => '',
'account_iban' => '',
'account_bic' => '',
'city' => '',
'postcode' => '',
'is_active_local_group' => true,
'has_active_instance' => true,
]);
$response->success = true;
$response->message = 'Stamm wurde angelegt.';
$response->slug = $tenant->slug;
return $response;
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Domains\Admin\Actions\CreateTenant;
class CreateTenantRequest
{
public function __construct(
public string $name,
public string $slug,
public string $url,
) {
}
}
@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Admin\Actions\CreateTenant;
class CreateTenantResponse
{
public bool $success = false;
public string $message = '';
public ?string $slug = null;
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Actions\ToggleUserActive;
class ToggleUserActiveAction
{
public function __construct(private ToggleUserActiveRequest $request)
{
}
public function execute(): ToggleUserActiveResponse
{
$response = new ToggleUserActiveResponse();
if ($this->request->currentUserId === $this->request->user->id) {
$response->message = 'Du kannst dich nicht selbst deaktivieren.';
return $response;
}
$this->request->user->update(['active' => !$this->request->user->active]);
$status = $this->request->user->active ? 'aktiviert' : 'deaktiviert';
$response->success = true;
$response->message = 'Benutzer*in wurde ' . $status . '.';
$response->active = $this->request->user->active;
return $response;
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Admin\Actions\ToggleUserActive;
use App\Models\User;
class ToggleUserActiveRequest
{
public function __construct(
public User $user,
public int $currentUserId,
) {
}
}
@@ -0,0 +1,10 @@
<?php
namespace App\Domains\Admin\Actions\ToggleUserActive;
class ToggleUserActiveResponse
{
public bool $success = false;
public string $message = '';
public ?bool $active = null;
}
@@ -0,0 +1,27 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantContact;
class UpdateTenantContactAction
{
public function __construct(private UpdateTenantContactRequest $request)
{
}
public function execute(): UpdateTenantContactResponse
{
$response = new UpdateTenantContactResponse();
$this->request->tenant->update([
'email' => $this->request->email,
'email_finance' => $this->request->emailFinance,
'postcode' => $this->request->postcode,
'city' => $this->request->city,
]);
$response->success = true;
$response->message = 'Kontaktdaten wurden gespeichert.';
return $response;
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantContact;
use App\Models\Tenant;
class UpdateTenantContactRequest
{
public function __construct(
public Tenant $tenant,
public string $email,
public string $emailFinance,
public string $postcode,
public string $city,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantContact;
class UpdateTenantContactResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGdpr;
class UpdateTenantGdprAction
{
public function __construct(private UpdateTenantGdprRequest $request)
{
}
public function execute(): UpdateTenantGdprResponse
{
$response = new UpdateTenantGdprResponse();
$this->request->tenant->update([
'gdpr_text' => $this->request->gdprText,
]);
$response->success = true;
$response->message = 'Datenschutzerklärung wurde gespeichert.';
return $response;
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGdpr;
use App\Models\Tenant;
class UpdateTenantGdprRequest
{
public function __construct(
public Tenant $tenant,
public string $gdprText,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGdpr;
class UpdateTenantGdprResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
class UpdateTenantGeneralAction
{
public function __construct(private UpdateTenantGeneralRequest $request)
{
}
public function execute(): UpdateTenantGeneralResponse
{
$response = new UpdateTenantGeneralResponse();
$this->request->tenant->update([
'name' => $this->request->name,
'slug' => $this->request->slug,
'url' => $this->request->url,
]);
$response->success = true;
$response->message = 'Allgemeine Daten wurden gespeichert.';
return $response;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
use App\Models\Tenant;
class UpdateTenantGeneralRequest
{
public function __construct(
public Tenant $tenant,
public string $name,
public string $slug,
public string $url,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
class UpdateTenantGeneralResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantImpress;
class UpdateTenantImpressAction
{
public function __construct(private UpdateTenantImpressRequest $request)
{
}
public function execute(): UpdateTenantImpressResponse
{
$response = new UpdateTenantImpressResponse();
$this->request->tenant->update([
'impress_text' => $this->request->impressText,
]);
$response->success = true;
$response->message = 'Impressum wurde gespeichert.';
return $response;
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantImpress;
use App\Models\Tenant;
class UpdateTenantImpressRequest
{
public function __construct(
public Tenant $tenant,
public string $impressText,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantImpress;
class UpdateTenantImpressResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantPayment;
class UpdateTenantPaymentAction
{
public function __construct(private UpdateTenantPaymentRequest $request)
{
}
public function execute(): UpdateTenantPaymentResponse
{
$response = new UpdateTenantPaymentResponse();
$this->request->tenant->update([
'account_iban' => $this->request->accountIban,
'account_bic' => $this->request->accountBic,
'account_name' => $this->request->accountName,
]);
$response->success = true;
$response->message = 'Bezahldaten wurden gespeichert.';
return $response;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantPayment;
use App\Models\Tenant;
class UpdateTenantPaymentRequest
{
public function __construct(
public Tenant $tenant,
public string $accountIban,
public string $accountBic,
public string $accountName,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateTenantPayment;
class UpdateTenantPaymentResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,39 @@
<?php
namespace App\Domains\Admin\Actions\UpdateUser;
class UpdateUserAction
{
public function __construct(private UpdateUserRequest $request)
{
}
public function execute(): UpdateUserResponse
{
$response = new UpdateUserResponse();
$allowedFields = [
'firstname', 'lastname', 'nickname', 'email', 'phone', 'birthday',
'membership_id', 'address_1', 'address_2', 'postcode', 'city',
'eating_habits', 'swimming_permission', 'first_aid_permission',
'bank_account_owner', 'bank_account_iban',
'medications', 'allergies', 'intolerances',
'user_role_local_group',
];
if ($this->request->isLvTenant) {
$allowedFields[] = 'local_group';
if (!$this->request->isOwnUser) {
$allowedFields[] = 'user_role_main';
}
}
$data = array_intersect_key($this->request->data, array_flip($allowedFields));
$this->request->user->update($data);
$response->success = true;
$response->message = 'Benutzerdaten wurden gespeichert.';
return $response;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Admin\Actions\UpdateUser;
use App\Models\User;
class UpdateUserRequest
{
public function __construct(
public User $user,
public array $data,
public bool $isOwnUser,
public bool $isLvTenant,
) {
}
}
@@ -0,0 +1,9 @@
<?php
namespace App\Domains\Admin\Actions\UpdateUser;
class UpdateUserResponse
{
public bool $success = false;
public string $message = '';
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class AdminDashboardController extends CommonController
{
public function __invoke(Request $request): Response
{
$inertiaProvider = new InertiaProvider('Admin/Dashboard', []);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantContactGetController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
return response()->json([
'email' => $tenant->email,
'email_finance' => $tenant->email_finance,
'postcode' => $tenant->postcode,
'city' => $tenant->city,
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/contact',
]);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantContact\UpdateTenantContactAction;
use App\Domains\Admin\Actions\UpdateTenantContact\UpdateTenantContactRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantContactUpdateController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
$action = new UpdateTenantContactAction(new UpdateTenantContactRequest(
tenant: $tenant,
email: $request->input('email'),
emailFinance: $request->input('email_finance'),
postcode: $request->input('postcode'),
city: $request->input('city'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantGdprGetController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
return response()->json([
'gdpr_text' => $tenant->gdpr_text ?? '',
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/gdpr',
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantGdpr\UpdateTenantGdprAction;
use App\Domains\Admin\Actions\UpdateTenantGdpr\UpdateTenantGdprRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantGdprUpdateController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
$action = new UpdateTenantGdprAction(new UpdateTenantGdprRequest(
tenant: $tenant,
gdprText: $request->input('gdpr_text'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantImpressGetController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
return response()->json([
'impress_text' => $tenant->impress_text ?? '',
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/impress',
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantImpress\UpdateTenantImpressAction;
use App\Domains\Admin\Actions\UpdateTenantImpress\UpdateTenantImpressRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantImpressUpdateController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
$action = new UpdateTenantImpressAction(new UpdateTenantImpressRequest(
tenant: $tenant,
impressText: $request->input('impress_text'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantPaymentGetController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
return response()->json([
'account_iban' => $tenant->account_iban,
'account_bic' => $tenant->account_bic,
'account_name' => $tenant->account_name,
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/payment',
]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantPayment\UpdateTenantPaymentAction;
use App\Domains\Admin\Actions\UpdateTenantPayment\UpdateTenantPaymentRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ManagedTenantPaymentUpdateController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
$action = new UpdateTenantPaymentAction(new UpdateTenantPaymentRequest(
tenant: $tenant,
accountIban: $request->input('account_iban'),
accountBic: $request->input('account_bic'),
accountName: $request->input('account_name'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantContactGetController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'email' => $this->tenant->email,
'email_finance' => $this->tenant->email_finance,
'postcode' => $this->tenant->postcode,
'city' => $this->tenant->city,
'saveEndpoint' => '/api/v1/admin/tenant/contact',
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantContact\UpdateTenantContactAction;
use App\Domains\Admin\Actions\UpdateTenantContact\UpdateTenantContactRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantContactUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateTenantContactAction(new UpdateTenantContactRequest(
tenant: $this->tenant,
email: $request->input('email'),
emailFinance: $request->input('email_finance'),
postcode: $request->input('postcode'),
city: $request->input('city'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\CreateTenant\CreateTenantAction;
use App\Domains\Admin\Actions\CreateTenant\CreateTenantRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantCreateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new CreateTenantAction(new CreateTenantRequest(
name: $request->input('name'),
slug: $request->input('slug'),
url: $request->input('url'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
'slug' => $response->slug,
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class TenantEditPageController extends CommonController
{
public function __invoke(string $slug, Request $request): Response
{
$tenant = $this->adminTenants->findBySlug($slug);
$inertiaProvider = new InertiaProvider('Admin/TenantEdit', [
'tenant' => [
'name' => $tenant->name,
'slug' => $tenant->slug,
'url' => $tenant->url,
'is_active_local_group' => $tenant->is_active_local_group,
],
]);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantGdprGetController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'gdpr_text' => $this->tenant->gdpr_text ?? '',
'saveEndpoint' => '/api/v1/admin/tenant/gdpr',
]);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantGdpr\UpdateTenantGdprAction;
use App\Domains\Admin\Actions\UpdateTenantGdpr\UpdateTenantGdprRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantGdprUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateTenantGdprAction(new UpdateTenantGdprRequest(
tenant: $this->tenant,
gdprText: $request->input('gdpr_text'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantGeneralGetController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
return response()->json([
'name' => $tenant->name,
'slug' => $tenant->slug,
'url' => $tenant->url,
'is_active_local_group' => $tenant->is_active_local_group,
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/general',
]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantGeneral\UpdateTenantGeneralAction;
use App\Domains\Admin\Actions\UpdateTenantGeneral\UpdateTenantGeneralRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantGeneralUpdateController extends CommonController
{
public function __invoke(string $slug, Request $request): JsonResponse
{
$tenant = $this->adminTenants->findBySlug($slug);
$action = new UpdateTenantGeneralAction(new UpdateTenantGeneralRequest(
tenant: $tenant,
name: $request->input('name'),
slug: $request->input('slug'),
url: $request->input('url'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantImpressGetController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'impress_text' => $this->tenant->impress_text ?? '',
'saveEndpoint' => '/api/v1/admin/tenant/impress',
]);
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantImpress\UpdateTenantImpressAction;
use App\Domains\Admin\Actions\UpdateTenantImpress\UpdateTenantImpressRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantImpressUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateTenantImpressAction(new UpdateTenantImpressRequest(
tenant: $this->tenant,
impressText: $request->input('impress_text'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantListApiController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$tenants = $this->adminTenants->getActiveTenants();
$mapped = $tenants->map(function ($tenant) {
return [
'name' => $tenant->name,
'slug' => $tenant->slug,
'url' => $tenant->url,
'is_active_local_group' => $tenant->is_active_local_group,
];
});
return response()->json(['tenants' => $mapped]);
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class TenantListPageController extends CommonController
{
public function __invoke(Request $request): Response
{
$inertiaProvider = new InertiaProvider('Admin/TenantList', []);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class TenantPageController extends CommonController
{
public function __invoke(Request $request): Response
{
$inertiaProvider = new InertiaProvider('Admin/TenantData', [
'tenant' => [
'name' => $this->tenant->name,
'slug' => $this->tenant->slug,
'url' => $this->tenant->url,
'is_active_local_group' => $this->tenant->is_active_local_group,
],
]);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantPaymentGetController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
return response()->json([
'account_iban' => $this->tenant->account_iban,
'account_bic' => $this->tenant->account_bic,
'account_name' => $this->tenant->account_name,
'saveEndpoint' => '/api/v1/admin/tenant/payment',
]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateTenantPayment\UpdateTenantPaymentAction;
use App\Domains\Admin\Actions\UpdateTenantPayment\UpdateTenantPaymentRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TenantPaymentUpdateController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$action = new UpdateTenantPaymentAction(new UpdateTenantPaymentRequest(
tenant: $this->tenant,
accountIban: $request->input('account_iban'),
accountBic: $request->input('account_bic'),
accountName: $request->input('account_name'),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Enumerations\UserRole;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserDetailGetController extends CommonController
{
public function __invoke(int $id, Request $request): JsonResponse
{
$user = $this->adminUsers->findById($id);
$userData = $user->toArray();
unset($userData['password'], $userData['remember_token'], $userData['activation_token'], $userData['activation_token_expires_at']);
$tenantNames = $this->adminTenants->getTenantNames();
$userData['nicename'] = $user->getNicename();
$userData['fullname'] = $user->getFullName();
$userData['local_group_name'] = $tenantNames[$user->local_group] ?? $user->local_group;
return response()->json([
'user' => $userData,
'isOwnUser' => auth()->id() === $user->id,
'isLvTenant' => $this->tenant->slug === 'lv',
'userRoles' => UserRole::all()->map(fn($role) => ['slug' => $role->slug, 'name' => $role->name]),
'localGroups' => $this->adminTenants->getActiveLocalGroups()->map(fn($t) => ['slug' => $t->slug, 'name' => $t->name]),
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserListApiController extends CommonController
{
public function __invoke(Request $request): JsonResponse
{
$tenantNames = $this->adminTenants->getTenantNames();
$users = $this->adminUsers->getListForTenant($this->tenant->slug);
$mapped = $users->map(function ($user) use ($tenantNames) {
return [
'id' => $user->id,
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'nickname' => $user->nickname,
'local_group' => $user->local_group,
'local_group_name' => $tenantNames[$user->local_group] ?? $user->local_group,
'active' => $user->active,
];
});
return response()->json(['users' => $mapped]);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class UserListPageController extends CommonController
{
public function __invoke(Request $request): Response
{
$inertiaProvider = new InertiaProvider('Admin/UserList', [
'isLvTenant' => $this->tenant->slug === 'lv',
]);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\UserManagement\Actions\GenerateActivationToken\GenerateActivationTokenCommand;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserResetPasswordController extends CommonController
{
public function __invoke(int $id, Request $request): JsonResponse
{
$user = $this->adminUsers->findById($id);
if (!$user->email) {
return response()->json([
'status' => 'error',
'message' => 'Benutzer*in hat keine E-Mail-Adresse hinterlegt.',
]);
}
$command = new GenerateActivationTokenCommand($user);
$command->execute();
return response()->json([
'status' => 'success',
'message' => 'Passwort-Reset-Mail wurde gesendet.',
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\ToggleUserActive\ToggleUserActiveAction;
use App\Domains\Admin\Actions\ToggleUserActive\ToggleUserActiveRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserToggleActiveController extends CommonController
{
public function __invoke(int $id, Request $request): JsonResponse
{
$user = $this->adminUsers->findById($id);
$action = new ToggleUserActiveAction(new ToggleUserActiveRequest(
user: $user,
currentUserId: auth()->id(),
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
'active' => $response->active,
]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admin\Controllers;
use App\Domains\Admin\Actions\UpdateUser\UpdateUserAction;
use App\Domains\Admin\Actions\UpdateUser\UpdateUserRequest;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UserUpdateController extends CommonController
{
public function __invoke(int $id, Request $request): JsonResponse
{
$user = $this->adminUsers->findById($id);
$action = new UpdateUserAction(new UpdateUserRequest(
user: $user,
data: $request->all(),
isOwnUser: auth()->id() === $user->id,
isLvTenant: $this->tenant->slug === 'lv',
));
$response = $action->execute();
return response()->json([
'status' => $response->success ? 'success' : 'error',
'message' => $response->message,
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Domains\Admin\Repositories;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection as SupportCollection;
class AdminTenantRepository
{
public function findBySlug(string $slug): Tenant
{
return Tenant::where('slug', $slug)->firstOrFail();
}
public function getActiveTenants(): Collection
{
return Tenant::where('has_active_instance', true)->get();
}
public function getTenantNames(): SupportCollection
{
return Tenant::pluck('name', 'slug');
}
public function getActiveLocalGroups(): Collection
{
return Tenant::where('is_active_local_group', true)->get();
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Domains\Admin\Repositories;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
class AdminUserRepository
{
public function findById(int $id): User
{
return User::findOrFail($id);
}
public function getListForTenant(string $tenantSlug): Collection
{
$query = User::query();
if ($tenantSlug !== 'lv') {
$query->where('local_group', $tenantSlug);
}
return $query->orderBy('lastname')->orderBy('firstname')->get();
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
use App\Domains\Admin\Controllers\UserDetailGetController;
use App\Domains\Admin\Controllers\UserListApiController;
use App\Domains\Admin\Controllers\UserResetPasswordController;
use App\Domains\Admin\Controllers\UserToggleActiveController;
use App\Domains\Admin\Controllers\UserUpdateController;
use App\Domains\Admin\Controllers\ManagedTenantContactGetController;
use App\Domains\Admin\Controllers\ManagedTenantContactUpdateController;
use App\Domains\Admin\Controllers\ManagedTenantGdprGetController;
use App\Domains\Admin\Controllers\ManagedTenantGdprUpdateController;
use App\Domains\Admin\Controllers\ManagedTenantImpressGetController;
use App\Domains\Admin\Controllers\ManagedTenantImpressUpdateController;
use App\Domains\Admin\Controllers\ManagedTenantPaymentGetController;
use App\Domains\Admin\Controllers\ManagedTenantPaymentUpdateController;
use App\Domains\Admin\Controllers\TenantContactGetController;
use App\Domains\Admin\Controllers\TenantContactUpdateController;
use App\Domains\Admin\Controllers\TenantCreateController;
use App\Domains\Admin\Controllers\TenantGdprGetController;
use App\Domains\Admin\Controllers\TenantGdprUpdateController;
use App\Domains\Admin\Controllers\TenantGeneralGetController;
use App\Domains\Admin\Controllers\TenantGeneralUpdateController;
use App\Domains\Admin\Controllers\TenantImpressGetController;
use App\Domains\Admin\Controllers\TenantImpressUpdateController;
use App\Domains\Admin\Controllers\TenantListApiController;
use App\Domains\Admin\Controllers\TenantPaymentGetController;
use App\Domains\Admin\Controllers\TenantPaymentUpdateController;
use App\Middleware\AdminRoleMiddleware;
use App\Middleware\IdentifyTenant;
use App\Middleware\LvOnlyMiddleware;
use Illuminate\Support\Facades\Route;
Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->group(function () {
Route::prefix('api/v1/admin/tenant')->group(function () {
Route::get('/contact', TenantContactGetController::class);
Route::post('/contact', TenantContactUpdateController::class);
Route::get('/payment', TenantPaymentGetController::class);
Route::post('/payment', TenantPaymentUpdateController::class);
Route::get('/impress', TenantImpressGetController::class);
Route::post('/impress', TenantImpressUpdateController::class);
Route::get('/gdpr', TenantGdprGetController::class);
Route::post('/gdpr', TenantGdprUpdateController::class);
});
Route::prefix('api/v1/admin/users')->group(function () {
Route::get('/list', UserListApiController::class);
Route::get('/{id}', UserDetailGetController::class);
Route::post('/{id}', UserUpdateController::class);
Route::post('/{id}/toggle-active', UserToggleActiveController::class);
Route::post('/{id}/reset-password', UserResetPasswordController::class);
});
Route::middleware(LvOnlyMiddleware::class)->group(function () {
Route::prefix('api/v1/admin/tenants')->group(function () {
Route::get('/list', TenantListApiController::class);
Route::post('/create', TenantCreateController::class);
Route::prefix('/{slug}')->group(function () {
Route::get('/general', TenantGeneralGetController::class);
Route::post('/general', TenantGeneralUpdateController::class);
Route::get('/contact', ManagedTenantContactGetController::class);
Route::post('/contact', ManagedTenantContactUpdateController::class);
Route::get('/payment', ManagedTenantPaymentGetController::class);
Route::post('/payment', ManagedTenantPaymentUpdateController::class);
Route::get('/impress', ManagedTenantImpressGetController::class);
Route::post('/impress', ManagedTenantImpressUpdateController::class);
Route::get('/gdpr', ManagedTenantGdprGetController::class);
Route::post('/gdpr', ManagedTenantGdprUpdateController::class);
});
});
});
});
+24
View File
@@ -0,0 +1,24 @@
<?php
use App\Domains\Admin\Controllers\AdminDashboardController;
use App\Domains\Admin\Controllers\TenantEditPageController;
use App\Domains\Admin\Controllers\TenantListPageController;
use App\Domains\Admin\Controllers\TenantPageController;
use App\Domains\Admin\Controllers\UserListPageController;
use App\Middleware\AdminRoleMiddleware;
use App\Middleware\IdentifyTenant;
use App\Middleware\LvOnlyMiddleware;
use Illuminate\Support\Facades\Route;
Route::middleware([IdentifyTenant::class, 'auth', AdminRoleMiddleware::class])->group(function () {
Route::prefix('admin')->group(function () {
Route::get('/', AdminDashboardController::class);
Route::get('/tenant', TenantPageController::class);
Route::get('/users', UserListPageController::class);
Route::middleware(LvOnlyMiddleware::class)->group(function () {
Route::get('/tenants', TenantListPageController::class);
Route::get('/tenants/{slug}', TenantEditPageController::class);
});
});
});
+15
View File
@@ -0,0 +1,15 @@
<script setup>
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
</script>
<template>
<AdminAppLayout title="Administration">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<h2>Administration</h2>
</shadowed-box>
</AdminAppLayout>
</template>
<style scoped>
</style>
@@ -0,0 +1,146 @@
<script setup>
import { ref } from 'vue';
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
})
const { request } = useAjax()
const editing = ref(false)
const form = ref({
email: props.data.email ?? '',
email_finance: props.data.email_finance ?? '',
postcode: props.data.postcode ?? '',
city: props.data.city ?? '',
})
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/contact'
const response = await request(saveUrl, {
method: 'POST',
body: form.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
editing.value = false
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<div v-if="!editing">
<table class="data-table">
<tr><th>Email:</th><td>{{ form.email }}</td></tr>
<tr><th>Email Schatzmeister*in:</th><td>{{ form.email_finance }}</td></tr>
<tr><th>Postleitzahl:</th><td>{{ form.postcode }}</td></tr>
<tr><th>Ort:</th><td>{{ form.city }}</td></tr>
</table>
<button class="btn-edit" @click="editing = true">Bearbeiten</button>
</div>
<div v-else>
<table class="data-table">
<tr>
<th>Email:</th>
<td><input type="email" v-model="form.email" class="form-input" /></td>
</tr>
<tr>
<th>Email Schatzmeister*in:</th>
<td><input type="email" v-model="form.email_finance" class="form-input" /></td>
</tr>
<tr>
<th>Postleitzahl:</th>
<td><input type="text" v-model="form.postcode" class="form-input" /></td>
</tr>
<tr>
<th>Ort:</th>
<td><input type="text" v-model="form.city" class="form-input" /></td>
</tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
<button class="btn-cancel" @click="editing = false">Abbrechen</button>
</div>
</div>
</template>
<style scoped>
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
text-align: left;
padding: 8px 12px;
width: 200px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.data-table td {
padding: 8px 12px;
border-bottom: 1px solid #e5e7eb;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-edit, .btn-save, .btn-cancel {
margin-top: 15px;
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-edit {
background-color: #1d4899;
color: #ffffff;
}
.btn-edit:hover {
background-color: #163a7a;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
margin-left: 10px;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
.btn-group {
display: flex;
gap: 10px;
}
</style>
@@ -0,0 +1,87 @@
<script setup>
import { ref } from 'vue';
import TextEditor from "../../../../Views/Components/TextEditor.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
import gdprTemplate from "../../../../../resources/templates/gdpr-template.html?raw";
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
})
const { request } = useAjax()
const content = ref(props.data.gdpr_text ?? '')
function autoGenerate() {
if (content.value && content.value.trim() !== '') {
toast.error('Der Editor ist nicht leer. Bitte leere den Inhalt zuerst, um die Vorlage zu verwenden.')
return
}
const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
content.value = gdprTemplate.replace('[Datum]', today)
}
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/gdpr'
const response = await request(saveUrl, {
method: 'POST',
body: { gdpr_text: content.value },
})
if (response && response.status === 'success') {
toast.success(response.message)
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<div>
<TextEditor v-model="content" />
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
<button class="btn-generate" @click="autoGenerate">Auto-generieren</button>
</div>
</div>
</template>
<style scoped>
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-save, .btn-generate {
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-generate {
background-color: #1d4899;
color: #ffffff;
}
.btn-generate:hover {
background-color: #163a7a;
}
</style>
@@ -0,0 +1,157 @@
<script setup>
import { ref } from 'vue';
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
})
const { request } = useAjax()
const editing = ref(false)
const form = ref({
name: props.data.name ?? '',
slug: props.data.slug ?? '',
url: props.data.url ?? '',
})
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenants/' + props.data.slug + '/general'
const response = await request(saveUrl, {
method: 'POST',
body: form.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
editing.value = false
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<div v-if="!editing">
<table class="data-table">
<tr><th>Name:</th><td>{{ form.name }}</td></tr>
<tr><th>Slug:</th><td>{{ form.slug }}</td></tr>
<tr><th>URL:</th><td>{{ form.url }}</td></tr>
<tr><th>Status:</th><td>
<span class="badge-active">Aktiv</span>
</td></tr>
</table>
<button class="btn-edit" @click="editing = true">Bearbeiten</button>
</div>
<div v-else>
<table class="data-table">
<tr>
<th>Name:</th>
<td><input type="text" v-model="form.name" class="form-input" /></td>
</tr>
<tr>
<th>Slug:</th>
<td><input type="text" v-model="form.slug" class="form-input" /></td>
</tr>
<tr>
<th>URL:</th>
<td><input type="text" v-model="form.url" class="form-input" /></td>
</tr>
<tr><th>Status:</th><td>
<span class="badge-active">Aktiv</span>
</td></tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
<button class="btn-cancel" @click="editing = false">Abbrechen</button>
</div>
</div>
</template>
<style scoped>
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
text-align: left;
padding: 8px 12px;
width: 200px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.data-table td {
padding: 8px 12px;
border-bottom: 1px solid #e5e7eb;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.badge-active {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #166534;
background-color: #dcfce7;
border: 1px solid #22c55e;
}
.btn-edit, .btn-save, .btn-cancel {
margin-top: 15px;
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-edit {
background-color: #1d4899;
color: #ffffff;
}
.btn-edit:hover {
background-color: #163a7a;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
margin-left: 10px;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
.btn-group {
display: flex;
gap: 10px;
}
</style>
@@ -0,0 +1,56 @@
<script setup>
import { ref } from 'vue';
import TextEditor from "../../../../Views/Components/TextEditor.vue";
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
})
const { request } = useAjax()
const content = ref(props.data.impress_text ?? '')
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/impress'
const response = await request(saveUrl, {
method: 'POST',
body: { impress_text: content.value },
})
if (response && response.status === 'success') {
toast.success(response.message)
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<div>
<TextEditor v-model="content" />
<button class="btn-save" @click="save">Speichern</button>
</div>
</template>
<style scoped>
.btn-save {
margin-top: 15px;
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
</style>
@@ -0,0 +1,140 @@
<script setup>
import { ref } from 'vue';
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
})
const { request } = useAjax()
const editing = ref(false)
const form = ref({
account_iban: props.data.account_iban ?? '',
account_bic: props.data.account_bic ?? '',
account_name: props.data.account_name ?? '',
})
async function save() {
const saveUrl = props.data.saveEndpoint ?? '/api/v1/admin/tenant/payment'
const response = await request(saveUrl, {
method: 'POST',
body: form.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
editing.value = false
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
</script>
<template>
<div v-if="!editing">
<table class="data-table">
<tr><th>IBAN:</th><td>{{ form.account_iban }}</td></tr>
<tr><th>BIC:</th><td>{{ form.account_bic }}</td></tr>
<tr><th>Name Kontoinhaber:</th><td>{{ form.account_name }}</td></tr>
</table>
<button class="btn-edit" @click="editing = true">Bearbeiten</button>
</div>
<div v-else>
<table class="data-table">
<tr>
<th>IBAN:</th>
<td><input type="text" v-model="form.account_iban" class="form-input" /></td>
</tr>
<tr>
<th>BIC:</th>
<td><input type="text" v-model="form.account_bic" class="form-input" /></td>
</tr>
<tr>
<th>Name Kontoinhaber:</th>
<td><input type="text" v-model="form.account_name" class="form-input" /></td>
</tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
<button class="btn-cancel" @click="editing = false">Abbrechen</button>
</div>
</div>
</template>
<style scoped>
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
text-align: left;
padding: 8px 12px;
width: 200px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
}
.data-table td {
padding: 8px 12px;
border-bottom: 1px solid #e5e7eb;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-edit, .btn-save, .btn-cancel {
margin-top: 15px;
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-edit {
background-color: #1d4899;
color: #ffffff;
}
.btn-edit:hover {
background-color: #163a7a;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
margin-left: 10px;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
.btn-group {
display: flex;
gap: 10px;
}
</style>
@@ -0,0 +1,331 @@
<script setup>
import { ref } from 'vue';
import { useAjax } from "../../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const props = defineProps({
data: Object,
})
const emit = defineEmits(['updated', 'closed'])
const { request } = useAjax()
const editing = ref(false)
const user = props.data.user
const isOwnUser = props.data.isOwnUser
const isLvTenant = props.data.isLvTenant
const userRoles = props.data.userRoles
const localGroups = props.data.localGroups
const form = ref({
firstname: user.firstname ?? '',
lastname: user.lastname ?? '',
nickname: user.nickname ?? '',
email: user.email ?? '',
phone: user.phone ?? '',
birthday: user.birthday ?? '',
membership_id: user.membership_id ?? '',
address_1: user.address_1 ?? '',
address_2: user.address_2 ?? '',
postcode: user.postcode ?? '',
city: user.city ?? '',
medications: user.medications ?? '',
allergies: user.allergies ?? '',
intolerances: user.intolerances ?? '',
bank_account_owner: user.bank_account_owner ?? '',
bank_account_iban: user.bank_account_iban ?? '',
user_role_local_group: user.user_role_local_group ?? '',
user_role_main: user.user_role_main ?? '',
local_group: user.local_group ?? '',
})
async function save() {
const response = await request('/api/v1/admin/users/' + user.id, {
method: 'POST',
body: form.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
editing.value = false
emit('updated')
} else {
toast.error(response?.message ?? 'Fehler beim Speichern')
}
}
async function toggleActive() {
const response = await request('/api/v1/admin/users/' + user.id + '/toggle-active', {
method: 'POST',
})
if (response && response.status === 'success') {
toast.success(response.message)
emit('updated')
} else {
toast.error(response?.message ?? 'Fehler')
}
}
async function resetPassword() {
const response = await request('/api/v1/admin/users/' + user.id + '/reset-password', {
method: 'POST',
})
if (response && response.status === 'success') {
toast.success(response.message)
} else {
toast.error(response?.message ?? 'Fehler')
}
}
function getRoleName(slug) {
const role = userRoles.find(r => r.slug === slug)
return role ? role.name : slug
}
function getGroupName(slug) {
const group = localGroups.find(g => g.slug === slug)
return group ? group.name : slug
}
</script>
<template>
<div>
<div class="detail-header">
<h3>{{ user.firstname }} {{ user.lastname }}</h3>
<div class="detail-actions">
<button v-if="!editing" class="btn-edit" @click="editing = true">Bearbeiten</button>
<button class="btn-reset" @click="resetPassword">Passwort zurücksetzen</button>
<button v-if="!isOwnUser" :class="user.active ? 'btn-deactivate' : 'btn-activate'" @click="toggleActive">
{{ user.active ? 'Deaktivieren' : 'Aktivieren' }}
</button>
<button class="btn-close" @click="emit('closed')">Schließen</button>
</div>
</div>
<div v-if="!editing">
<table class="data-table">
<tr><th>Anmeldename:</th><td>{{ user.username }}</td></tr>
<tr><th>Vorname:</th><td>{{ form.firstname }}</td></tr>
<tr><th>Nachname:</th><td>{{ form.lastname }}</td></tr>
<tr><th>Nickname:</th><td>{{ form.nickname }}</td></tr>
<tr><th>E-Mail:</th><td>{{ form.email }}</td></tr>
<tr><th>Telefon:</th><td>{{ form.phone }}</td></tr>
<tr><th>Geburtstag:</th><td>{{ form.birthday }}</td></tr>
<tr><th>Mitgliedsnummer:</th><td>{{ form.membership_id }}</td></tr>
<tr><th>Adresse 1:</th><td>{{ form.address_1 }}</td></tr>
<tr><th>Adresse 2:</th><td>{{ form.address_2 }}</td></tr>
<tr><th>PLZ:</th><td>{{ form.postcode }}</td></tr>
<tr><th>Ort:</th><td>{{ form.city }}</td></tr>
<tr><th>Medikamente:</th><td>{{ form.medications }}</td></tr>
<tr><th>Allergien:</th><td>{{ form.allergies }}</td></tr>
<tr><th>Unverträglichkeiten:</th><td>{{ form.intolerances }}</td></tr>
<tr><th>Kontoinhaber:</th><td>{{ form.bank_account_owner }}</td></tr>
<tr><th>IBAN:</th><td>{{ form.bank_account_iban }}</td></tr>
<tr><th>Stamm:</th><td>{{ getGroupName(form.local_group) }}</td></tr>
<tr><th>Rolle (Stamm):</th><td>{{ getRoleName(form.user_role_local_group) }}</td></tr>
<tr><th>Rolle (LV):</th><td>{{ getRoleName(form.user_role_main) }}</td></tr>
<tr><th>Status:</th><td>
<span :class="user.active ? 'badge-active' : 'badge-inactive'">
{{ user.active ? 'Aktiv' : 'Inaktiv' }}
</span>
</td></tr>
</table>
</div>
<div v-else>
<table class="data-table">
<tr><th>Anmeldename:</th><td>{{ user.username }}</td></tr>
<tr><th>Vorname:</th><td><input type="text" v-model="form.firstname" class="form-input" /></td></tr>
<tr><th>Nachname:</th><td><input type="text" v-model="form.lastname" class="form-input" /></td></tr>
<tr><th>Nickname:</th><td><input type="text" v-model="form.nickname" class="form-input" /></td></tr>
<tr><th>E-Mail:</th><td><input type="email" v-model="form.email" class="form-input" /></td></tr>
<tr><th>Telefon:</th><td><input type="text" v-model="form.phone" class="form-input" /></td></tr>
<tr><th>Geburtstag:</th><td><input type="date" v-model="form.birthday" class="form-input" /></td></tr>
<tr><th>Mitgliedsnummer:</th><td><input type="text" v-model="form.membership_id" class="form-input" /></td></tr>
<tr><th>Adresse 1:</th><td><input type="text" v-model="form.address_1" class="form-input" /></td></tr>
<tr><th>Adresse 2:</th><td><input type="text" v-model="form.address_2" class="form-input" /></td></tr>
<tr><th>PLZ:</th><td><input type="text" v-model="form.postcode" class="form-input" /></td></tr>
<tr><th>Ort:</th><td><input type="text" v-model="form.city" class="form-input" /></td></tr>
<tr><th>Medikamente:</th><td><input type="text" v-model="form.medications" class="form-input" /></td></tr>
<tr><th>Allergien:</th><td><input type="text" v-model="form.allergies" class="form-input" /></td></tr>
<tr><th>Unverträglichkeiten:</th><td><input type="text" v-model="form.intolerances" class="form-input" /></td></tr>
<tr><th>Kontoinhaber:</th><td><input type="text" v-model="form.bank_account_owner" class="form-input" /></td></tr>
<tr><th>IBAN:</th><td><input type="text" v-model="form.bank_account_iban" class="form-input" /></td></tr>
<tr><th>Stamm:</th><td>
<select v-if="isLvTenant" v-model="form.local_group" class="form-input">
<option v-for="group in localGroups" :key="group.slug" :value="group.slug">{{ group.name }}</option>
</select>
<span v-else>{{ getGroupName(form.local_group) }}</span>
</td></tr>
<tr><th>Rolle (Stamm):</th><td>
<select v-model="form.user_role_local_group" class="form-input">
<option v-for="role in userRoles" :key="role.slug" :value="role.slug">{{ role.name }}</option>
</select>
</td></tr>
<tr><th>Rolle (LV):</th><td>
<select v-if="isLvTenant && !isOwnUser" v-model="form.user_role_main" class="form-input">
<option v-for="role in userRoles" :key="role.slug" :value="role.slug">{{ role.name }}</option>
</select>
<span v-else>{{ getRoleName(form.user_role_main) }}</span>
</td></tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="save">Speichern</button>
<button class="btn-cancel" @click="editing = false">Abbrechen</button>
</div>
</div>
</div>
</template>
<style scoped>
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
}
.detail-header h3 {
margin: 0;
color: #1f2937;
}
.detail-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
text-align: left;
padding: 6px 12px;
width: 180px;
color: #374151;
border-bottom: 1px solid #e5e7eb;
font-size: 0.9rem;
}
.data-table td {
padding: 6px 12px;
border-bottom: 1px solid #e5e7eb;
}
.form-input {
width: 100%;
padding: 5px 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
box-sizing: border-box;
}
.badge-active {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #166534;
background-color: #dcfce7;
border: 1px solid #22c55e;
}
.badge-inactive {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #991b1b;
background-color: #fee2e2;
border: 1px solid #ef4444;
}
.btn-edit, .btn-save, .btn-cancel, .btn-reset, .btn-deactivate, .btn-activate, .btn-close {
padding: 6px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.85rem;
}
.btn-edit {
background-color: #1d4899;
color: #ffffff;
}
.btn-edit:hover {
background-color: #163a7a;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
.btn-reset {
background-color: #f59e0b;
color: #ffffff;
}
.btn-reset:hover {
background-color: #d97706;
}
.btn-deactivate {
background-color: #ef4444;
color: #ffffff;
}
.btn-deactivate:hover {
background-color: #dc2626;
}
.btn-activate {
background-color: #16a34a;
color: #ffffff;
}
.btn-activate:hover {
background-color: #15803d;
}
.btn-close {
background-color: #6b7280;
color: #ffffff;
}
.btn-close:hover {
background-color: #4b5563;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<script setup>
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import TenantContact from "./Partials/TenantContact.vue";
import TenantPayment from "./Partials/TenantPayment.vue";
import TenantImpress from "./Partials/TenantImpress.vue";
import TenantGdpr from "./Partials/TenantGdpr.vue";
const props = defineProps({
tenant: Object,
})
const tabs = [
{
title: 'Kontaktdaten',
component: TenantContact,
endpoint: '/api/v1/admin/tenant/contact',
},
{
title: 'Bezahldaten',
component: TenantPayment,
endpoint: '/api/v1/admin/tenant/payment',
},
{
title: 'Impressum',
component: TenantImpress,
endpoint: '/api/v1/admin/tenant/impress',
},
{
title: 'Datenschutzerklärung',
component: TenantGdpr,
endpoint: '/api/v1/admin/tenant/gdpr',
},
]
</script>
<template>
<AdminAppLayout :title="props.tenant.slug === 'lv' ? 'LV-Daten' : 'Stammesdaten'">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<table class="tenant-header">
<tr><th>Name</th><td>{{ props.tenant.name }}</td></tr>
<tr><th>Slug</th><td>{{ props.tenant.slug }}</td></tr>
<tr><th>mareike-URL:</th><td>{{ props.tenant.url }}</td></tr>
<tr><th>Status</th><td>
<span :class="props.tenant.is_active_local_group ? 'badge-active' : 'badge-inactive'">
{{ props.tenant.is_active_local_group ? 'Aktiv' : 'Inaktiv' }}
</span>
</td></tr>
</table>
<div style="margin-top: 30px;">
<tabbed-page :tabs="tabs" />
</div>
</shadowed-box>
</AdminAppLayout>
</template>
<style scoped>
.tenant-header {
width: 100%;
border-collapse: collapse;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.tenant-header th {
text-align: left;
padding: 10px 16px;
width: 200px;
color: #374151;
font-weight: bold;
background-color: #f9fafb;
border-bottom: 1px solid #d1d5db;
}
.tenant-header td {
padding: 10px 16px;
border-bottom: 1px solid #d1d5db;
}
.tenant-header tr:last-child th,
.tenant-header tr:last-child td {
border-bottom: none;
}
.badge-active {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #166534;
background-color: #dcfce7;
border: 1px solid #22c55e;
}
.badge-inactive {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #991b1b;
background-color: #fee2e2;
border: 1px solid #ef4444;
}
</style>
+52
View File
@@ -0,0 +1,52 @@
<script setup>
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import TenantGeneral from "./Partials/TenantGeneral.vue";
import TenantContact from "./Partials/TenantContact.vue";
import TenantPayment from "./Partials/TenantPayment.vue";
import TenantImpress from "./Partials/TenantImpress.vue";
import TenantGdpr from "./Partials/TenantGdpr.vue";
const props = defineProps({
tenant: Object,
})
const slug = props.tenant.slug
const tabs = [
{
title: 'Allgemeines',
component: TenantGeneral,
endpoint: '/api/v1/admin/tenants/' + slug + '/general',
},
{
title: 'Kontaktdaten',
component: TenantContact,
endpoint: '/api/v1/admin/tenants/' + slug + '/contact',
},
{
title: 'Bezahldaten',
component: TenantPayment,
endpoint: '/api/v1/admin/tenants/' + slug + '/payment',
},
{
title: 'Impressum',
component: TenantImpress,
endpoint: '/api/v1/admin/tenants/' + slug + '/impress',
},
{
title: 'Datenschutzerklärung',
component: TenantGdpr,
endpoint: '/api/v1/admin/tenants/' + slug + '/gdpr',
},
]
</script>
<template>
<AdminAppLayout :title="props.tenant.name">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" />
</shadowed-box>
</AdminAppLayout>
</template>
+248
View File
@@ -0,0 +1,248 @@
<script setup>
import { ref, onMounted } from 'vue';
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import { toast } from "vue3-toastify";
const { request } = useAjax()
function openTenant(slug) {
window.location.href = '/admin/tenants/' + slug
}
const tenants = ref([])
const showCreate = ref(false)
const createForm = ref({
name: '',
slug: '',
url: '',
})
onMounted(async () => {
await loadTenants()
})
async function loadTenants() {
const response = await fetch('/api/v1/admin/tenants/list', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
})
const data = await response.json()
tenants.value = data.tenants
}
async function createTenant() {
const response = await request('/api/v1/admin/tenants/create', {
method: 'POST',
body: createForm.value,
})
if (response && response.status === 'success') {
toast.success(response.message)
showCreate.value = false
createForm.value = { name: '', slug: '', url: '' }
await loadTenants()
} else {
toast.error(response?.message ?? 'Fehler beim Anlegen')
}
}
</script>
<template>
<AdminAppLayout title="Stämme">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<table class="tenant-table">
<thead>
<tr>
<th>Name</th>
<th>Slug</th>
<th>URL</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="tenant in tenants" :key="tenant.slug" class="tenant-row" @click="openTenant(tenant.slug)">
<td>{{ tenant.name }}</td>
<td>{{ tenant.slug }}</td>
<td>{{ tenant.url }}</td>
<td>
<span :class="tenant.is_active_local_group ? 'badge-active' : 'badge-inactive'">
{{ tenant.is_active_local_group ? 'Aktiv' : 'Inaktiv' }}
</span>
</td>
</tr>
</tbody>
</table>
<div style="margin-top: 20px;">
<button v-if="!showCreate" class="btn-create" @click="showCreate = true">Neuen Stamm anlegen</button>
<div v-if="showCreate" class="create-form">
<h3>Neuen Stamm anlegen</h3>
<table class="form-table">
<tr>
<th>Name</th>
<td><input type="text" v-model="createForm.name" class="form-input" /></td>
</tr>
<tr>
<th>Slug</th>
<td><input type="text" v-model="createForm.slug" class="form-input" /></td>
</tr>
<tr>
<th>URL</th>
<td><input type="text" v-model="createForm.url" class="form-input" /></td>
</tr>
</table>
<div class="btn-group">
<button class="btn-save" @click="createTenant">Anlegen</button>
<button class="btn-cancel" @click="showCreate = false">Abbrechen</button>
</div>
</div>
</div>
</shadowed-box>
</AdminAppLayout>
</template>
<style scoped>
.tenant-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.tenant-table thead th {
text-align: left;
padding: 10px 16px;
background-color: #f9fafb;
color: #374151;
font-weight: bold;
border-bottom: 2px solid #d1d5db;
}
.tenant-table tbody td {
padding: 10px 16px;
border-bottom: 1px solid #e5e7eb;
}
.tenant-row {
cursor: pointer;
}
.tenant-row:hover {
background-color: #f3f4f6;
}
.badge-active {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #166534;
background-color: #dcfce7;
border: 1px solid #22c55e;
}
.badge-inactive {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #991b1b;
background-color: #fee2e2;
border: 1px solid #ef4444;
}
.create-form {
margin-top: 15px;
padding: 20px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #f9fafb;
}
.create-form h3 {
margin: 0 0 15px 0;
color: #374151;
}
.form-table {
width: 100%;
border-collapse: collapse;
}
.form-table th {
text-align: left;
padding: 8px 12px;
width: 100px;
color: #374151;
}
.form-table td {
padding: 8px 12px;
}
.form-input {
width: 100%;
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.95rem;
box-sizing: border-box;
}
.btn-create {
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
background-color: #1d4899;
color: #ffffff;
}
.btn-create:hover {
background-color: #163a7a;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn-save, .btn-cancel {
padding: 8px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
font-size: 0.9rem;
}
.btn-save {
background-color: #16a34a;
color: #ffffff;
}
.btn-save:hover {
background-color: #15803d;
}
.btn-cancel {
background-color: #e5e7eb;
color: #374151;
}
.btn-cancel:hover {
background-color: #d1d5db;
}
</style>
+167
View File
@@ -0,0 +1,167 @@
<script setup>
import { ref, onMounted } from 'vue';
import AdminAppLayout from "../../../../resources/js/layouts/AdminAppLayout.vue";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import FullScreenModal from "../../../Views/Components/FullScreenModal.vue";
import UserDetail from "./Partials/UserDetail.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
const { request } = useAjax()
const props = defineProps({
isLvTenant: Boolean,
})
const users = ref([])
const selectedUserId = ref(null)
const userDetail = ref(null)
onMounted(async () => {
await loadUsers()
})
async function loadUsers() {
const response = await fetch('/api/v1/admin/users/list', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
})
const data = await response.json()
users.value = data.users
}
async function selectUser(userId) {
if (selectedUserId.value === userId) {
selectedUserId.value = null
userDetail.value = null
return
}
selectedUserId.value = userId
const response = await fetch('/api/v1/admin/users/' + userId, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
})
userDetail.value = await response.json()
}
function onUserUpdated() {
loadUsers()
if (selectedUserId.value) {
selectUser(selectedUserId.value)
}
}
function onDetailClosed() {
selectedUserId.value = null
userDetail.value = null
loadUsers()
}
</script>
<template>
<AdminAppLayout title="Benutzer*innen">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<table class="user-table">
<thead>
<tr>
<th>Nachname</th>
<th>Vorname</th>
<th>Pfadiname</th>
<th v-if="props.isLvTenant">Stamm</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users"
:key="user.id"
:class="['user-row', { 'user-row-selected': selectedUserId === user.id }]"
@click="selectUser(user.id)">
<td>{{ user.lastname }}</td>
<td>{{ user.firstname }}</td>
<td>{{ user.nickname }}</td>
<td v-if="props.isLvTenant">{{ user.local_group_name }}</td>
<td>
<span :class="user.active ? 'badge-active' : 'badge-inactive'">
{{ user.active ? 'Aktiv' : 'Inaktiv' }}
</span>
</td>
</tr>
</tbody>
</table>
</shadowed-box>
<FullScreenModal :show="userDetail !== null" @close="onDetailClosed">
<UserDetail
v-if="userDetail"
:data="userDetail"
@updated="onUserUpdated"
@closed="onDetailClosed"
/>
</FullScreenModal>
</AdminAppLayout>
</template>
<style scoped>
.user-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
.user-table thead th {
text-align: left;
padding: 10px 16px;
background-color: #f9fafb;
color: #374151;
font-weight: bold;
border-bottom: 2px solid #d1d5db;
}
.user-table tbody td {
padding: 10px 16px;
border-bottom: 1px solid #e5e7eb;
}
.user-row {
cursor: pointer;
}
.user-row:hover {
background-color: #f3f4f6;
}
.user-row-selected {
background-color: #eff6ff;
}
.badge-active {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #166534;
background-color: #dcfce7;
border: 1px solid #22c55e;
}
.badge-inactive {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 0.85rem;
font-weight: bold;
color: #991b1b;
background-color: #fee2e2;
border: 1px solid #ef4444;
}
</style>
@@ -0,0 +1,50 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
use App\Models\CostUnitEstimate;
class CreateEstimateAction {
private CreateEstimateResponse $response;
public function __construct(private CreateEstimateRequest $request) {
}
public function execute(): CreateEstimateResponse {
$this->response = new CreateEstimateResponse();
$amount = [];
switch ($this->request->amountType) {
case 'flat':
$amount['flat_amount'] = $this->request->amount;
break;
case 'per_person':
$amount['amount_by_user'] = $this->request->amount;
break;
}
if ($this->request->estimateId === 0) {
$estimate = CostUnitEstimate::create(array_merge([
'tenant' => app('tenant')->slug,
'cost_unit_id' => $this->request->costUnit->id,
'type' => $this->request->estimateType,
'description' => $this->request->description,
], $amount));
} else {
$estimate = CostUnitEstimate::find($this->request->estimateId);
$estimate->update(array_merge([
'tenant' => app('tenant')->slug,
'cost_unit_id' => $this->request->costUnit->id,
'type' => $this->request->estimateType,
'description' => $this->request->description,
], $amount));
}
if ($estimate !== null) {
$this->response->estimateId = $estimate->id;
$this->response->success = true;
}
return $this->response;
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
use App\Enumerations\InvoiceType;
use App\Models\CostUnit;
use App\ValueObjects\Amount;
class CreateEstimateRequest {
function __construct(
public string $amountType,
public string $description,
public Amount $amount,
public string $estimateType,
public CostUnit $costUnit,
public int $estimateId,
) {
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Domains\Budget\Actions\CreateEstimate;
class CreateEstimateResponse {
public bool $success;
public ?int $estimateId;
public function __construct() {
$this->success = false;
$this->estimateId = null;
}
}
@@ -0,0 +1,16 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
class DeleteEstimateAction {
public function __construct(private DeleteEstimateRequest $request) {
}
public function execute() : DeleteEstimateResponse {
$response = new DeleteEstimateResponse();
$this->request->estimate->delete();
$response->success = true;
return $response;
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
use App\Models\CostUnitEstimate;
class DeleteEstimateRequest {
public function __construct(public CostUnitEstimate $estimate)
{
}
}
@@ -0,0 +1,12 @@
<?php
namespace App\Domains\Budget\Actions\DeleteEstimate;
class DeleteEstimateResponse {
public bool $success;
public function __construct()
{
$this->success = false;
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateAction;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateRequest;
use App\Domains\Budget\Actions\DeleteEstimate\DeleteEstimateAction;
use App\Domains\Budget\Actions\DeleteEstimate\DeleteEstimateRequest;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DeleteController extends CommonController
{
public function __invoke(int $costUnitId, int $estimateId, Request $request) : JsonResponse {
$estimate = $this->estimates->getById($estimateId);
if ($estimate === null) {
return response()->json([
'status' => 'error',
'message' => 'Estimate not found'
], 404);
}
$deleteEstimateResponse =
new DeleteEstimateAction(request: new DeleteEstimateRequest($estimate)
)->execute();
if ($deleteEstimateResponse->success) {
return response()->json([
'status' => 'success',
'message' => 'Der Eintrag wurde erfolgreich gelöscht.'
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Beim Löschen des Eintrags ist ein Fehler aufgetreten.'
]);
}
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Enumerations\InvoiceType;
use App\Scopes\CommonController;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListController extends CommonController
{
public function __invoke(int $costUnitId, string $estimateType, Request $request) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
$estimates = $this->estimates->getEstimates($costUnit, $estimateType);
return response()->json([
'status' => 'success',
'costUnitId' => $costUnitId,
'title' => InvoiceType::where('slug', $estimateType)->first()->name,
'estimateType' => $estimateType,
'estimates' => $estimates,
'totalAmountString' => $this->estimates->getTotalAmount($costUnit, $estimateType)->toString(),
]);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Providers\InertiaProvider;
use App\Scopes\CommonController;
use Illuminate\Http\Request;
use Inertia\Response;
class MainController extends CommonController
{
public function __invoke(int $costUnitId, Request $request) : Response
{
$inertiaProvider = new InertiaProvider('Budget/List', [
'cost_unit_id' => $costUnitId
]);
return $inertiaProvider->render();
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Domains\Budget\Controllers;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateAction;
use App\Domains\Budget\Actions\CreateEstimate\CreateEstimateRequest;
use App\Domains\CostUnit\Actions\CreateCostUnit\CreateCostUnitRequest;
use App\Scopes\CommonController;
use App\ValueObjects\Amount;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SaveController extends CommonController
{
public function __invoke(int $costUnitId, Request $request) : JsonResponse {
$costUnit = $this->costUnits->getById($costUnitId);
if ($costUnit === null) {
return response()->json([
'status' => 'error',
'message' => 'Cost unit not found'
], 404);
}
$createCostUniResponse =
new CreateEstimateAction(request: new CreateEstimateRequest(
description: $request->input('description'),
amount: Amount::fromString($request->input('amount')),
amountType: $request->input('amount_type'),
estimateType: $request->input('estimateType'),
costUnit: $costUnit,
estimateId: $request->input('estimateId'),
))->execute();
if ($createCostUniResponse->success) {
return response()->json([
'status' => 'success',
'message' => 'Der Eintrag wurde erfolgreich angelegt.'
]);
} else {
return response()->json([
'status' => 'error',
'message' => 'Beim Anlegen des Eintrags ist ein Fehler aufgetreten.'
]);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
use App\Domains\Budget\Controllers\DeleteController;
use App\Domains\Budget\Controllers\SaveController;
use App\Domains\Budget\Controllers\ListController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::prefix('api/v1')->group(function () {
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('budget')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::prefix('/{costUnitId}')->group(function () {
Route::get('/list/{estimateType}', ListController::class);
Route::get('{estimateId}/delete', DeleteController::class);
Route::post('/save-estimate', SaveController::class);
});
});
});
});
});
+20
View File
@@ -0,0 +1,20 @@
<?php
use App\Domains\Budget\Controllers\MainController;
use App\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Route;
Route::middleware(IdentifyTenant::class)->group(function () {
Route::prefix('budget')->group(function () {
Route::middleware(['auth'])->group(function () {
Route::prefix('/{costUnitId}')->group(function() {
Route::get('/', MainController::class);
});
});
});
});
@@ -0,0 +1,100 @@
<script setup>
import Modal from "../../../Views/Components/Modal.vue";
import {reactive, ref} from "vue";
import AmountInput from "../../../Views/Components/AmountInput.vue";
import {toast} from "vue3-toastify";
import {useAjax} from "../../../../resources/js/components/ajaxHandler.js";
const { request } = useAjax()
const props = defineProps({
showAddEstimate: Boolean,
type: String,
title: String,
costUnitId: Number,
amount: Number,
amount_type: String,
estimateId: Number,
description: String,
})
console.log(props)
const form = reactive({
amount_type: props.amount_type,
amount: props.amount,
description: props.description,
})
async function save() {
const data = await request('/api/v1/budget/' + props.costUnitId + '/save-estimate', {
method: "POST",
body: {
estimateId: props.estimateId,
amount_type: form.amount_type,
amount: form.amount,
description: form.description,
estimateType: props.type,
}
});
if (data.status === 'success') {
toast.success(data.message);
} else {
toast.error(data.message);
}
emit('closeAddEstimate')
}
const emit = defineEmits(['closeAddEstimate'])
</script>
<template>
<Modal
:show="showAddEstimate"
@close="emit('closeAddEstimate')"
title="Ausgabenschätzung hinzufügen"
width="600px"
>
<table>
<tr>
<th>Kostenstelle</th>
<td>{{title}}</td>
</tr>
<tr>
<th>Verwendungszweck</th>
<td><input type="text" v-model="form.description" style="width: 250px;" /></td>
</tr>
<tr>
<th>Betrag</th>
<td><AmountInput v-model="form.amount" style="width: 100px;" /> Euro</td>
</tr>
<tr>
<th>Kostentyp</th>
<td style="vertical-align: top;">
<input type="radio" v-model="form.amount_type" value="flat"
id="amount_type_flat" />
<label for="amount_type_flat">Pauschal</label><br />
<input type="radio" v-model="form.amount_type" value="per_person" id="amount_type_per_person" />
<label for="amount_type_per_person">Pro Person</label><br />
</td>
</tr>
<tr>
<td colspan="2">
<input type="button" value="Speichern" class="button" @click="save" />
</td>
</tr>
</table>
</Modal>
</template>
<style scoped>
</style>
+93
View File
@@ -0,0 +1,93 @@
<script setup>
import {reactive, inject, onMounted} from 'vue';
import AppLayout from '../../../../resources/js/layouts/AppLayout.vue';
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import ShadowedBox from "../../../Views/Components/ShadowedBox.vue";
import TabbedPage from "../../../Views/Components/TabbedPage.vue";
import {toast} from "vue3-toastify";
import ListBudgets from "./ListBudgetTypes.vue";
const props = defineProps({
message: String,
data: {
type: [Array, Object],
default: () => []
},
cost_unit_id: {
type: Number,
default: 0
},
})
// Prüfen, ob ein ?id= Parameter in der URL übergeben wurde
const urlParams = new URLSearchParams(window.location.search)
const initialCostUnitId = props.cost_unit_id
const tabs = [
{
title: 'Verpflegung',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/catering",
deep_jump_id: initialCostUnitId,
},
{
title: 'Unterkunft',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/accommodation",
deep_jump_id: initialCostUnitId,
},
{
title: 'Programm',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/program",
deep_jump_id: initialCostUnitId,
},
{
title: 'Logistik',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/logistic",
deep_jump_id: initialCostUnitId,
},
{
title: 'Technik',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/technical",
deep_jump_id: initialCostUnitId,
},
{
title: 'Reisekosten',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/travelling",
deep_jump_id: initialCostUnitId,
},
{
title: 'Verwaltung',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/management",
deep_jump_id: initialCostUnitId,
},
{
title: 'Sonstiges',
component: ListBudgets,
endpoint: "/api/v1/budget/" + props.cost_unit_id + "/list/other",
deep_jump_id: initialCostUnitId,
},
]
onMounted(() => {
if (undefined !== props.message) {
toast.success(props.message)
}
})
</script>
<template>
<AppLayout title="Veranstaltungsbudget">
<shadowed-box style="width: 95%; margin: 20px auto; padding: 20px; overflow-x: hidden;">
<tabbed-page :tabs="tabs" :initial-tab-id="initialCostUnitId" />
</shadowed-box>
</AppLayout>
</template>
@@ -0,0 +1,121 @@
<script setup>
import {createApp, ref} from 'vue'
import LoadingModal from "../../../Views/Components/LoadingModal.vue";
import { useAjax } from "../../../../resources/js/components/ajaxHandler.js";
import {toast} from "vue3-toastify";
import AddOrUpdateEstimate from "./AddOrUpdateEstimate.vue";
const props = defineProps({
data: {
type: [Array, Object],
default: () => []
},
})
const localData = ref(props.data)
const showAddEstimate = ref(false)
const estimateId = ref(null)
const description = ref(null)
const amount = ref(null)
const amountType = ref(null)
const { data, loading, error, request, download } = useAjax()
async function reload() {
const url = "/api/v1/budget/" + props.data.costUnitId + "/list/" + props.data.estimateType
try {
const response = await fetch(url, { method: 'GET' })
if (!response.ok) throw new Error('Fehler beim Laden')
const result = await response.json()
localData.value = result
} catch (err) {
console.error('Error fetching estimates:', err)
}
}
async function openAddEstimate() {
estimateId.value = 0
amount.value = 0.00
amountType.value = 'flat'
description.value = ''
showAddEstimate.value = true
}
async function openEditEstimate(localEstimateId, localDescription, localAmount, localAmountType, localEstimateType) {
estimateId.value = localEstimateId
description.value = localDescription
amount.value = localAmount
amountType.value = localAmountType
console.log(localEstimateId, localDescription, localAmount, localAmountType, localEstimateType)
console.log(estimateId.value, description.value, amount.value, amountType.value, localEstimateType)
showAddEstimate.value = true
}
async function deleteEstimate(currentEstimateId) {
const data = await request('/api/v1/budget/' + props.data.costUnitId + '/' + currentEstimateId + '/delete', {
method: "GET",
});
if (data.status === 'success') {
toast.success(data.message);
reload()
} else {
toast.error(data.message);
}
}
</script>
<template>
<div v-if="localData.estimates && localData.estimates.length > 0">
<h2>{{ props.data.title }}</h2>
<h3>Gesamtkosten: {{ localData.totalAmountString }}</h3>
<span v-for="estimate in localData.estimates">
<table style="width: 100%;">
<tr><th style="width: 200px;">
{{ estimate.title }}
</th>
<td>{{ estimate.singleAmountString }}</td>
</tr>
<tr>
<td></td>
<td style="padding-bottom: 30px">
<label class="link" style="font-size: 10pt; margin-right: 20px;" @click="openEditEstimate(estimate.id, estimate.title, estimate.amountValue, estimate.amountType, props.data.estimateType)">Bearbeiten</label>
<label class="link" style="font-size: 10pt; margin-right: 20px; color: #ff0000;" @click="deleteEstimate(estimate.id)">Löschen</label>
</td>
</tr>
</table>
</span>
</div>
<div v-else>
<strong style="width: 100%; text-align: center; display: block; margin-top: 20px;">
Noch keine geschätzten Ausgaben vorhanden
</strong>
</div>
<label class="link" @click="openAddEstimate()">
Hinzufügen
</label>
<LoadingModal :show="showLoading" />
<AddOrUpdateEstimate
:amount="amount"
:amount_type="amountType"
:description="description"
:estimateId="estimateId"
:costUnitId="props.data.costUnitId"
:title="props.data.title"
:type="props.data.estimateType"
:showAddEstimate="showAddEstimate"
v-if="showAddEstimate"
@closeAddEstimate="showAddEstimate = false; reload()" />
</template>
<style scoped>
.costunit-list {
width: 96% !important;
}
</style>
@@ -0,0 +1,22 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
class ChangeCostUnitDetailsCommand {
private ChangeCostUnitDetailsRequest $request;
public function __construct(ChangeCostUnitDetailsRequest $request) {
$this->request = $request;
}
public function execute(): ChangeCostUnitDetailsResponse {
$response = new ChangeCostUnitDetailsResponse();
$this->request->costUnit->distance_allowance = $this->request->distanceAllowance->getAmount();
$this->request->costUnit->mail_on_new = $this->request->mailOnNew;
$this->request->costUnit->billing_deadline = $this->request->billingDeadline;
$response->success = $this->request->costUnit->save();
return $response;
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
use App\Models\CostUnit;
use App\ValueObjects\Amount;
use DateTime;
class ChangeCostUnitDetailsRequest {
public CostUnit $costUnit;
public Amount $distanceAllowance;
public bool $mailOnNew;
public ?DateTime $billingDeadline;
public function __construct(CostUnit $costUnit, Amount $distanceAllowance, bool $mailOnNew, ?DateTime $billingDeadline = null) {
$this->costUnit = $costUnit;
$this->distanceAllowance = $distanceAllowance;
$this->mailOnNew = $mailOnNew;
$this->billingDeadline = $billingDeadline;
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitDetails;
class ChangeCostUnitDetailsResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
class ChangeCostUnitStateCommand {
private ChangeCostUnitStateRequest $request;
public function __construct(ChangeCostUnitStateRequest $request) {
$this->request = $request;
}
public function execute() : ChangeCostUnitStateResponse {
$response = new ChangeCostUnitStateResponse();
$this->request->costUnit->allow_new = $this->request->allowNew;
$this->request->costUnit->archived = $this->request->isArchived;
$response->success = $this->request->costUnit->save();
return $response;
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
use App\Models\CostUnit;
class ChangeCostUnitStateRequest {
public bool $allowNew;
public bool $isArchived;
public CostUnit $costUnit;
public function __construct(CostUnit $costUnit, bool $allowNew, bool $isArchived) {
$this->costUnit = $costUnit;
$this->allowNew = $allowNew;
$this->isArchived = $isArchived;
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitState;
class ChangeCostUnitStateResponse {
public bool $success;
public function __construct() {
$this->success = false;
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
use App\Models\User;
class ChangeCostUnitTreasurersCommand {
private ChangeCostUnitTreasurersRequest $request;
public function __construct(ChangeCostUnitTreasurersRequest $request) {
$this->request = $request;
}
public function execute() : ChangeCostUnitTreasurersResponse {
$response = new ChangeCostUnitTreasurersResponse();
try {
$this->request->costUnit->resetTreasurers();
foreach ($this->request->treasurers as $treasurer) {
$this->request->costUnit->treasurers()->attach($treasurer);
}
$this->request->costUnit->save();
} catch (\Throwable $th) {
$response->success = false;
}
return $response;
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
use App\Models\CostUnit;
class ChangeCostUnitTreasurersRequest {
public array $treasurers;
public CostUnit $costUnit;
public function __construct(CostUnit $costUnit, array $treasurers) {
$this->treasurers = $treasurers;
$this->costUnit = $costUnit;
}
}
@@ -0,0 +1,7 @@
<?php
namespace App\Domains\CostUnit\Actions\ChangeCostUnitTreasurers;
class ChangeCostUnitTreasurersResponse {
}

Some files were not shown because too many files have changed in this diff Show More