Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d511dbdd66 | ||
|
|
9e9ea0cfd5 | ||
|
|
bcebe2ebed | ||
|
|
a43fd9cedf | ||
|
|
a5e4e55df5 | ||
|
|
c4ca101594 | ||
|
|
985c9ce07e | ||
|
|
790e8e1dbc | ||
|
|
ef6284d5cc | ||
|
|
328825293a | ||
|
|
4e981c140b | ||
|
|
8940cb275b | ||
|
|
b668c7e805 | ||
|
|
ddfaab1501 | ||
|
|
70a57d10f5 | ||
|
|
6301c342e4 | ||
|
|
025035190d | ||
|
|
651b6147bf | ||
|
|
ae13841699 | ||
|
|
91dca31c11 | ||
|
|
40d5634764 | ||
|
|
350c8dd0d0 | ||
|
|
d07980dd1f | ||
|
|
b9795f08f0 | ||
|
|
e730d6db63 | ||
|
|
a6bddf9fa5 | ||
|
|
749d8588da | ||
|
|
4bb64b0053 | ||
|
|
d23e6c3914 | ||
|
|
15690b1da9 | ||
|
|
c1c1a4f143 | ||
|
|
5beb2b1d97 | ||
|
|
efee20c16b | ||
|
|
b01288b5e0 | ||
|
|
6a183d6498 | ||
|
|
9c4c28e566 | ||
|
|
a61344395a | ||
|
|
6e4e6b645c | ||
|
|
085299336e | ||
|
|
81912d2433 | ||
|
|
ea57f9400e | ||
|
|
cd51eb3f97 | ||
|
|
1cc84716a1 | ||
|
|
0ee952212b | ||
|
|
e95de52768 | ||
|
|
9581176a0c | ||
|
|
554166803b | ||
|
|
5031d3bde0 | ||
|
|
09f9767eb7 | ||
|
|
b3bbeb5b0b | ||
|
|
c17facf063 | ||
|
|
6b33c1b4dd | ||
|
|
72e0502bb9 | ||
|
|
3b47979265 | ||
|
|
b15573cda6 | ||
|
|
67c4e69f7b | ||
|
|
7f8417e915 | ||
|
|
f8dba18854 | ||
|
|
daff70b4fe | ||
|
|
7919d04dc3 | ||
|
|
cea00f9679 | ||
|
|
a0d26648ee | ||
|
|
e55cdd1988 | ||
|
|
d1ef092bc3 | ||
|
|
78f4d813f9 | ||
|
|
8d6a4041c1 | ||
|
|
29db141b84 | ||
|
|
ab3d526217 | ||
|
|
6c9281516e | ||
|
|
afc91545a9 | ||
|
|
12d98b4d7e | ||
|
|
6c7fe56579 | ||
|
|
e09987f5a8 | ||
|
|
5c514e9ff5 | ||
|
|
bc60461dac | ||
|
|
6848fbd95f | ||
|
|
012ebb6538 | ||
|
|
c1ef1d71ad | ||
|
|
aebb2f9aaa | ||
|
|
cfc7c7eee2 | ||
|
|
1b9384dad1 | ||
|
|
fed54514c8 | ||
|
|
12f05ceb09 | ||
|
|
5f56ef94a6 | ||
|
|
79bb186234 | ||
|
|
a012c16425 | ||
|
|
a8205a4f96 | ||
|
|
dbcebbb2c4 | ||
|
|
63c7b8dfb1 | ||
|
|
e9aa66a860 | ||
|
|
51c4055c47 | ||
|
|
2348663fd8 | ||
|
|
a83cec94ab | ||
|
|
710e27c344 | ||
|
|
83bbd6f7d3 | ||
|
|
28ffbdb696 | ||
|
|
575fb27018 | ||
|
|
fe3429cd4e | ||
|
|
551b592b3b | ||
|
|
6ed0a5b93a | ||
|
|
97fd7cd0da | ||
|
|
f5d7b21671 | ||
|
|
444711b049 | ||
|
|
0a7abb1389 | ||
|
|
998a799c3a | ||
|
|
ef8f3ebe6c | ||
|
|
92976fbf27 | ||
|
|
e7e7f039b8 | ||
|
|
0d436d8190 | ||
|
|
3fdbaf0285 | ||
|
|
8a049efe49 | ||
|
|
a62b2214c4 | ||
|
|
f00e665a03 | ||
|
|
454d83de2e | ||
|
|
30cc0b79c5 | ||
|
|
775d9158a6 | ||
|
|
c60429ad28 |
No files matched your search
@@ -56,6 +56,8 @@ MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
APP_ADMIN_MAIL=
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
|
||||
@@ -23,3 +23,6 @@ Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
/docker-compose.yaml
|
||||
|
||||
# HTML-Report von composer test:coverage
|
||||
/storage/coverage
|
||||
@@ -0,0 +1,100 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Laravel 12 + Inertia/Vue, mandantenfähig (custom `tenant`-Scoping über `app('tenant')`, gesetzt in
|
||||
`app/Middleware/IdentifyTenant.php`). PHP 8.5 — Tests/Artisan laufen im Container:
|
||||
`docker exec mareike-mareike-app-1 php artisan test`.
|
||||
|
||||
## Projektkonventionen (verbindlich)
|
||||
|
||||
Die verbindlichen Konventionen (Actions/Request-Command-Response, Controller, Repositories, Models/Resources, Tenant,
|
||||
Routing, Mails) stehen in `.ai/conventions.md` und werden hier eingebunden — **immer beachten**:
|
||||
|
||||
@.ai/conventions.md
|
||||
|
||||
## Bereichs-Dokumentation
|
||||
|
||||
- [Zahlungsmodule](app/EventPaymentModules/CLAUDE.md) — interface-gesteuerte Zahlungsarten (Optionen, Config,
|
||||
Anmelde-Zusammenfassung, GiroCode/Fähigkeits-Interfaces, neues Modul hinzufügen).
|
||||
|
||||
## Local Group / Parent Group (Stamm / Bund)
|
||||
|
||||
Technisch heißt es **Local Group** (Mandant/Tenant) und **Parent Group** (übergeordnete Gruppe); „Stamm"/„Bund" sind nur
|
||||
UI-Bezeichnungen.
|
||||
|
||||
- **ParentGroup** (`App\Models\ParentGroup`, Tabelle `parent_groups`, app-weit): `tenants.parent_group_id` ist Pflicht
|
||||
(n:1). `is_fallback` markiert „Ohne Zuordnung" — `Tenant::booted()` setzt sie, wenn nichts angegeben ist; sie ist nie
|
||||
löschbar. Andere nur, solange weder Tenant noch Anmeldung sie nutzt (`AdminParentGroupRepository::isInUse()`).
|
||||
Verwaltung LV-only unter `/admin/parent-groups`; Zuordnung am Tenant im Tab „Allgemeines" und bei der Anlage.
|
||||
- **Event**: `participant_group_type` (Enum `App\Enumerations\ParticipantGroupType`: `local_group` | `parent_group`),
|
||||
gewählt bei der Anlage, in den Einstellungen änderbar nur ohne Anmeldungen. **Einzige Quelle für die Auswahl**:
|
||||
`Event::participantGroupOptions()` — teilnehmende Stämme oder deren Bünde (distinct). Gespeichert wird
|
||||
`event_participants.local_group` (Slug) **oder** `event_participants.parent_group_id`.
|
||||
- **Anzeige**: `EventParticipant::participantGroupName()` → Resource-Feld `participantGroup`; `EventResource` liefert
|
||||
`participantGroupType`, `participantGroupLabel`, `participantGroupOptions` (`{id, name, value}`).
|
||||
- **Bezeichnungen** in `global_settings` (Model `GlobalSetting`, Repository `GlobalSettingRepository`, im Container
|
||||
`scoped` — immer über `app(...)` beziehen), Singular + Plural je Ebene, gepflegt von Hauptadmins unter
|
||||
`/admin/settings`. Gelesen über `ParticipantGroupType::label()/pluralLabel()/labels()`. Texte ohne Artikel formulieren
|
||||
(„Bitte bei „{label}" eine Auswahl treffen."), weil der Artikel vom Wort abhängt.
|
||||
|
||||
## eFZ-Prüfung pro Veranstaltung
|
||||
|
||||
`events.coc_check_required` (Default `true`, Checkbox in `CommonSettings.vue`, Resource-Feld `cocCheckRequired`). Ist
|
||||
sie aus, liefert `CertificateOfConductionCheckCommand` ohne API-Aufruf `not_required`, und `SignUpCommand` belegt
|
||||
`efz_status` mit `not_required` vor. Wirkt nur auf neue Anmeldungen, bestehende Status bleiben.
|
||||
|
||||
## Umsatzsteuer / Steuerinformationen
|
||||
|
||||
Rechtlich korrekter Begriff ist **Umsatzsteuer (USt)**, nicht MwSt. Ziel: tagesbasierte Anmelde-Rechnungen („Teilnahme
|
||||
Veranstaltung X in Gruppe Y" × Anwesenheitstage × Beitrag/Tag). Grundlage ist gebaut, die Rechnungs-PDF/-berechnung
|
||||
folgt noch.
|
||||
|
||||
### Tenant-Einstellungen (Tab „Steuerinformationen")
|
||||
|
||||
Felder auf `tenants` (Self-Service `TenantData.vue` + verwaltet `TenantEdit.vue`, Partial `TenantTaxInfo.vue`,
|
||||
Controller `TenantTax*` / `ManagedTenantTax*`, Action `UpdateTenantTax`):
|
||||
|
||||
- `tax_liable` (bool) — ob USt-Pflicht besteht.
|
||||
- `vat_rate` (int, %) — USt-Satz, nur relevant wenn `tax_liable` (0–100, geclamped).
|
||||
- `vat_pricing_mode` (Enum `App\Enumerations\VatPricingMode`, natives PHP-Enum: `inclusive` | `add_on`).
|
||||
- `tax_exemption_reason` (FK → `tax_exemption_reasons.slug`, nullable) — Grund wenn **nicht** steuerpflichtig.
|
||||
DB-Default neuer Tenants: `ustg_4_25a` (Jugendhilfe).
|
||||
- `tax_exemption_note` (text, nullable) — Freitext, nur bei Grund `custom` (`requires_note`).
|
||||
|
||||
### Preismodus — **das Kernverhalten**
|
||||
|
||||
Es wird **immer der finale Brutto-Beitrag** pro Anmeldung gespeichert (`EventParticipant.amount`, keine USt-Spalten am
|
||||
Teilnehmer). Der Modus wirkt **nur bei der Preisermittlung im Anmeldeprozess**:
|
||||
|
||||
- **`inclusive`** (Brutto / „zusammenfassen", Supermarkt-Prinzip, Default): der konfigurierte Beitrag **ist** der
|
||||
Endpreis, USt ist bereits enthalten → Preis bleibt gleich; die USt wird erst **auf der Rechnung herausgerechnet**:
|
||||
`USt = brutto × Satz / (100 + Satz)`, `netto = brutto − USt`.
|
||||
- **`add_on`** (Netto / „aufaddieren"): der konfigurierte Beitrag ist netto → Endpreis = `Beitrag × (1 + Satz/100)`. Die
|
||||
USt wird bei der Anmeldung **aufgeschlagen** → Teilnehmer zahlt mehr.
|
||||
|
||||
Umsetzung in `App\Resources\EventResource::calculateAmount()`: nach allen Multiplikatoren (Early-Bird, `pay_per_day`,
|
||||
Geschwister) gilt
|
||||
`if ($event->tax_liable && $event->vat_pricing_mode === VatPricingMode::AddOn->value) $fee->multiply(1 + $event->vat_rate/100);`
|
||||
danach `round(…, 2)`. Bei `inclusive`/nicht steuerpflichtig bleibt der Beitrag unverändert.
|
||||
|
||||
### Event-Snapshot (unveränderlich)
|
||||
|
||||
Bei Event-Anlage (`CreateEventCommand::execute()`) werden `tax_liable`, `vat_rate`, `vat_pricing_mode`,
|
||||
`tax_exemption_reason`, `tax_exemption_note` aus dem Tenant **auf das Event kopiert und dort eingefroren** (Spalten auf
|
||||
`events`). Preisermittlung und spätere Rechnungen lesen vom Event, nicht vom Tenant → spätere Tenant-Änderungen berühren
|
||||
bestehende Events/Rechnungen nicht.
|
||||
|
||||
### Befreiungsgründe (DB-gestützt)
|
||||
|
||||
Tabelle `tax_exemption_reasons`, Model `App\Enumerations\TaxExemptionReason` (Eloquent, `slug` PK; `name`,
|
||||
`invoice_text`, `requires_note`, `sort_order`). `options()` liefert `{value,label,text}` sortiert nach `sort_order`. FK
|
||||
von `tenants` **und** `events`. Seed in Migration `170010`, verfeinert in `170020`. Gründe (in Reihenfolge):
|
||||
`ustg_19` (§ 19 Kleinunternehmer), `ustg_4_24` (§ 4 Nr. 24 Jugendherbergen),
|
||||
`ustg_4_25a` (§ 4 Nr. 25 Buchst. a — Jugendhilfe-Veranstaltungen, Default), `ustg_4_22a` (§ 4 Nr. 22 Buchst. a —
|
||||
Vorträge/Kurse), `custom` (Freitext). Jeder Grund kapselt den Rechnungs-Pflichthinweis nach **§ 14 Abs. 4 Nr. 8 UStG**
|
||||
(`invoice_text`; bei `custom` der Tenant-Freitext via `invoiceText(?note)`). Gemeinnützigkeit allein befreit **nicht** —
|
||||
es braucht immer einen konkreten Rechtsgrund.
|
||||
|
||||
### Noch offen (nächster Schritt)
|
||||
|
||||
Die eigentliche **Rechnung**: tagesbasierte Positionen, USt aus dem gespeicherten Brutto herausrechnen (Satz vom
|
||||
Event-Snapshot); bei Befreiung kein USt-Ausweis, stattdessen der Pflichthinweis des Grundes.
|
||||
+29010
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
use App\Models\ParentGroup;
|
||||
|
||||
class CreateParentGroupAction
|
||||
{
|
||||
public function __construct(private CreateParentGroupRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): CreateParentGroupResponse
|
||||
{
|
||||
$response = new CreateParentGroupResponse();
|
||||
|
||||
$name = trim($this->request->name);
|
||||
|
||||
if ($name === '') {
|
||||
$response->message = 'Bitte einen Namen angeben.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->parentGroup = ParentGroup::create([
|
||||
'name' => $name,
|
||||
'is_fallback' => false,
|
||||
]);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = sprintf('%s „%s" wurde angelegt.', ParticipantGroupType::ParentGroup->label(), $name);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||
|
||||
class CreateParentGroupRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\CreateParentGroup;
|
||||
|
||||
use App\Models\ParentGroup;
|
||||
|
||||
class CreateParentGroupResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
public ?ParentGroup $parentGroup = null;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\CreateTenant;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Models\Tenant;
|
||||
|
||||
class CreateTenantAction
|
||||
{
|
||||
public function __construct(private CreateTenantRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): CreateTenantResponse
|
||||
{
|
||||
$response = new CreateTenantResponse();
|
||||
|
||||
if ($this->request->parentGroup === null) {
|
||||
$response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$tenant = Tenant::create([
|
||||
'name' => $this->request->name,
|
||||
'slug' => $this->request->slug,
|
||||
'url' => $this->request->url,
|
||||
'parent_group_id' => $this->request->parentGroup->id,
|
||||
'email' => '',
|
||||
'email_finance' => '',
|
||||
'account_name' => '',
|
||||
'account_iban' => '',
|
||||
'account_bic' => '',
|
||||
'city' => '',
|
||||
'postcode' => '',
|
||||
'is_active_local_group' => true,
|
||||
'has_active_instance' => true,
|
||||
]);
|
||||
|
||||
$paymentMethodDefaults = PaymentMethod::defaults();
|
||||
foreach (PaymentMethod::all() as $paymentMethod) {
|
||||
$defaults = $paymentMethodDefaults[$paymentMethod->slug] ?? ['name' => $paymentMethod->slug, 'description' => null, 'configuration' => []];
|
||||
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $defaults['configuration'] ?? []);
|
||||
|
||||
// Aktiv nur, was schon vollständig konfiguriert ist -- derselbe Maßstab wie beim Aktivieren
|
||||
// von Hand. Ein neuer Stamm hat noch keine Bankdaten; seine Zahlungsarten schaltet er frei,
|
||||
// sobald sie gepflegt sind.
|
||||
AvailablePaymentMethod::create([
|
||||
'tenant' => $tenant->slug,
|
||||
'slug' => $paymentMethod->slug,
|
||||
'name' => $defaults['name'],
|
||||
'description' => $defaults['description'],
|
||||
'active' => PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration),
|
||||
'configuration' => $configuration,
|
||||
]);
|
||||
}
|
||||
|
||||
$response->success = true;
|
||||
$response->message = sprintf('%s wurde angelegt.', ParticipantGroupType::LocalGroup->label());
|
||||
$response->slug = $tenant->slug;
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\CreateTenant;
|
||||
|
||||
use App\Models\ParentGroup;
|
||||
|
||||
class CreateTenantRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $url,
|
||||
/** Pflicht; null heißt, die übermittelte ID war unbekannt. */
|
||||
public ?ParentGroup $parentGroup,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||
|
||||
class DeleteParentGroupAction
|
||||
{
|
||||
public function __construct(private DeleteParentGroupRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): DeleteParentGroupResponse
|
||||
{
|
||||
$response = new DeleteParentGroupResponse();
|
||||
|
||||
// Die Fallback-Gruppe nimmt Mandanten ohne Zuordnung auf und muss deshalb bestehen bleiben.
|
||||
if ($this->request->parentGroup->is_fallback) {
|
||||
$response->message = sprintf('„%s" kann nicht gelöscht werden.', $this->request->parentGroup->name);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($this->request->inUse) {
|
||||
$response->message = sprintf(
|
||||
'„%s" ist noch zugeordnet und kann deshalb nicht gelöscht werden.',
|
||||
$this->request->parentGroup->name,
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->request->parentGroup->delete();
|
||||
|
||||
$response->success = true;
|
||||
$response->message = sprintf('„%s" wurde gelöscht.', $this->request->parentGroup->name);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||
|
||||
use App\Models\ParentGroup;
|
||||
|
||||
class DeleteParentGroupRequest
|
||||
{
|
||||
public function __construct(
|
||||
public ParentGroup $parentGroup,
|
||||
/** Ob die Gruppe noch von einem Mandanten oder einer Anmeldung verwendet wird. */
|
||||
public bool $inUse,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\DeleteParentGroup;
|
||||
|
||||
class DeleteParentGroupResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\RemovePaymentMethodUsage;
|
||||
|
||||
use App\Mail\EventReportMails\PaymentMethodsExhaustedMail;
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class RemovePaymentMethodUsageAction
|
||||
{
|
||||
public function execute(AvailablePaymentMethod $availablePaymentMethod): void
|
||||
{
|
||||
foreach ($availablePaymentMethod->events()->get() as $event) {
|
||||
$event->paymentMethods()->detach($availablePaymentMethod->slug);
|
||||
|
||||
$remainingActive = $event->paymentMethods()->where('active', true)->count();
|
||||
if ($remainingActive === 0 && $event->registration_allowed) {
|
||||
$event->registration_allowed = false;
|
||||
$event->save();
|
||||
|
||||
$recipients = $event->eventManagers;
|
||||
if ($event->costUnit !== null) {
|
||||
$recipients = $recipients->merge($event->costUnit->treasurers);
|
||||
}
|
||||
|
||||
foreach ($recipients->unique('id') as $recipient) {
|
||||
Mail::to($recipient->email)->send(new PaymentMethodsExhaustedMail($event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||
|
||||
class RenameParentGroupAction
|
||||
{
|
||||
public function __construct(private RenameParentGroupRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): RenameParentGroupResponse
|
||||
{
|
||||
$response = new RenameParentGroupResponse();
|
||||
|
||||
$name = trim($this->request->name);
|
||||
|
||||
if ($name === '') {
|
||||
$response->message = 'Bitte einen Namen angeben.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Umbenennen ist jederzeit möglich: Mandanten und Anmeldungen verweisen über die ID.
|
||||
$this->request->parentGroup->update(['name' => $name]);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Der Name wurde gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||
|
||||
use App\Models\ParentGroup;
|
||||
|
||||
class RenameParentGroupRequest
|
||||
{
|
||||
public function __construct(
|
||||
public ParentGroup $parentGroup,
|
||||
public string $name,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\RenameParentGroup;
|
||||
|
||||
class RenameParentGroupResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateAvailablePaymentMethod;
|
||||
|
||||
use App\Domains\Admin\Actions\RemovePaymentMethodUsage\RemovePaymentMethodUsageAction;
|
||||
use App\Models\PaymentMethod;
|
||||
|
||||
class UpdateAvailablePaymentMethodAction
|
||||
{
|
||||
public function __construct(private UpdateAvailablePaymentMethodRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateAvailablePaymentMethodResponse
|
||||
{
|
||||
$response = new UpdateAvailablePaymentMethodResponse();
|
||||
$paymentMethod = $this->request->availablePaymentMethod;
|
||||
$wasActive = $paymentMethod->active;
|
||||
|
||||
// Nur bekannte Options-Keys übernehmen -- das Options-Schema des Zahlungsmoduls ist die Autorität.
|
||||
$configuration = PaymentMethod::sanitizeConfiguration($paymentMethod->slug, $this->request->configuration);
|
||||
|
||||
// Guard: Aktivierung nur erlaubt, wenn alle Pflicht-Optionen befüllt und gültig sind. Inaktiv
|
||||
// gespeichert werden darf auch eine unfertige Konfiguration -- als Entwurf.
|
||||
if ($this->request->active && !PaymentMethod::isConfigurationComplete($paymentMethod->slug, $configuration)) {
|
||||
$errors = PaymentMethod::configurationErrors($paymentMethod->slug, $configuration);
|
||||
|
||||
$response->success = false;
|
||||
$response->message = $errors !== []
|
||||
? implode(' ', $errors) . ' Die Zahlungsmethode kann so nicht aktiviert werden.'
|
||||
: 'Bitte zuerst alle Pflichtfelder ausfüllen, bevor die Zahlungsmethode aktiviert wird.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$paymentMethod->update([
|
||||
'name' => $this->request->name,
|
||||
'description' => $this->request->description,
|
||||
'active' => $this->request->active,
|
||||
'configuration' => $configuration,
|
||||
]);
|
||||
|
||||
if ($wasActive && !$this->request->active) {
|
||||
(new RemovePaymentMethodUsageAction())->execute($paymentMethod);
|
||||
}
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Zahlungsmethode wurde gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateAvailablePaymentMethod;
|
||||
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
|
||||
class UpdateAvailablePaymentMethodRequest
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $configuration
|
||||
*/
|
||||
public function __construct(
|
||||
public AvailablePaymentMethod $availablePaymentMethod,
|
||||
public string $name,
|
||||
public ?string $description,
|
||||
public bool $active,
|
||||
public array $configuration = [],
|
||||
) {
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateAvailablePaymentMethod;
|
||||
|
||||
class UpdateAvailablePaymentMethodResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentAsset;
|
||||
|
||||
use App\Models\DocumentAsset;
|
||||
|
||||
/**
|
||||
* Legt ein Bild für die Dokumentvorlagen an oder ersetzt es.
|
||||
*
|
||||
* Gespeichert wird der base64-Payload in der Datenbank -- so überlebt das Bild ein Release (das als
|
||||
* `git checkout` alles unter `public/` überschreibt) und wandert mit jedem Dump mit.
|
||||
*/
|
||||
class UpdateDocumentAssetAction
|
||||
{
|
||||
/** Über 2 MB wird die Vorlage träge und das PDF unnötig groß. */
|
||||
private const int MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
/** @var array<string, string> */
|
||||
private const array ALLOWED_MIME_TYPES = [
|
||||
'image/png' => 'png',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/gif' => 'gif',
|
||||
'image/svg+xml' => 'svg',
|
||||
];
|
||||
|
||||
public function __construct(private readonly UpdateDocumentAssetRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateDocumentAssetResponse
|
||||
{
|
||||
$response = new UpdateDocumentAssetResponse();
|
||||
|
||||
$name = $this->normalizeName($this->request->name);
|
||||
|
||||
if ($name === null) {
|
||||
$response->message = 'Bitte einen Namen aus Kleinbuchstaben, Ziffern, Bindestrich oder Unterstrich angeben.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$asset = DocumentAsset::where('name', $name)->first();
|
||||
$file = $this->request->file;
|
||||
|
||||
if ($file === null && $asset === null) {
|
||||
$response->message = 'Für ein neues Bild wird eine Datei benötigt.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$attributes = ['name' => $name, 'label' => $this->request->label];
|
||||
|
||||
if ($file !== null) {
|
||||
if (!array_key_exists($file->getMimeType(), self::ALLOWED_MIME_TYPES)) {
|
||||
$response->message = 'Nur PNG, JPEG, GIF oder SVG sind zulässig.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($file->getSize() > self::MAX_BYTES) {
|
||||
$response->message = 'Das Bild darf höchstens 2 MB groß sein.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$attributes['mime'] = $file->getMimeType();
|
||||
$attributes['data'] = base64_encode(file_get_contents($file->getRealPath()));
|
||||
}
|
||||
|
||||
$response->asset = $asset === null
|
||||
? DocumentAsset::create($attributes)
|
||||
: tap($asset)->update($attributes);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Das Bild wurde gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/** Namen sind Slugs, damit `{asset:name}` eindeutig erkennbar bleibt. */
|
||||
private function normalizeName(string $name): ?string
|
||||
{
|
||||
$name = strtolower(trim($name));
|
||||
|
||||
return preg_match('/^[a-z0-9_-]+$/', $name) === 1 ? $name : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentAsset;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class UpdateDocumentAssetRequest
|
||||
{
|
||||
public function __construct(
|
||||
/** Slug, unter dem das Bild in der Vorlage als `{asset:name}` referenziert wird. */
|
||||
public readonly string $name,
|
||||
public readonly ?string $label = null,
|
||||
public readonly ?UploadedFile $file = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentAsset;
|
||||
|
||||
use App\Models\DocumentAsset;
|
||||
|
||||
class UpdateDocumentAssetResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
|
||||
public ?string $message = null;
|
||||
|
||||
public ?DocumentAsset $asset = null;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentTemplate;
|
||||
|
||||
use App\Models\DocumentTemplate;
|
||||
|
||||
/**
|
||||
* Speichert die Blockinhalte einer Dokumentvorlage.
|
||||
*
|
||||
* Es werden nur bereits angelegte, als editierbar markierte Blöcke geschrieben. Neue Blöcke entstehen
|
||||
* nicht über das Formular -- welche Blöcke eine Dokumentart hat, gibt die Vorlage vor (das Layout
|
||||
* verweist mit `{block:...}` auf sie).
|
||||
*/
|
||||
class UpdateDocumentTemplateAction
|
||||
{
|
||||
public function __construct(private readonly UpdateDocumentTemplateRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateDocumentTemplateResponse
|
||||
{
|
||||
$response = new UpdateDocumentTemplateResponse();
|
||||
|
||||
$existing = DocumentTemplate::forType($this->request->documentType);
|
||||
|
||||
// Erst prüfen, dann schreiben -- sonst bliebe bei einem Fehler ein halb gespeicherter Stand übrig.
|
||||
$rejection = $this->rejectBareAssetInStyle();
|
||||
if ($rejection !== null) {
|
||||
$response->message = $rejection;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
foreach ($this->request->blocks as $block => $content) {
|
||||
$template = $existing->get($block);
|
||||
|
||||
if ($template === null || !$template->editable) {
|
||||
$response->skipped[] = $block;
|
||||
continue;
|
||||
}
|
||||
|
||||
$template->update(['content' => (string) $content]);
|
||||
}
|
||||
|
||||
$response->success = true;
|
||||
$response->message = $response->skipped === []
|
||||
? 'Die Vorlage wurde gespeichert.'
|
||||
: 'Die Vorlage wurde gespeichert; nicht editierbare Blöcke blieben unverändert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Bild-Platzhalter, der im CSS nicht in `url(...)` steht, expandiert zu einem Data-URI, das
|
||||
* dompdf nicht auslagern kann -- der Parser läuft dann minutenlang und der FPM-Worker stirbt am
|
||||
* Zeitlimit (502). Solches CSS wird gar nicht erst gespeichert.
|
||||
*
|
||||
* @return string|null Fehlermeldung, oder null wenn nichts zu beanstanden ist.
|
||||
*/
|
||||
private function rejectBareAssetInStyle(): ?string
|
||||
{
|
||||
$style = $this->request->blocks[DocumentTemplate::BLOCK_STYLE] ?? null;
|
||||
|
||||
if ($style === null || preg_match('/(?<![("\'])\{asset:([a-z0-9_-]+)}/i', (string) $style, $match) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'Im CSS-Block steht der Bild-Platzhalter {asset:%1$s} direkt im Text. Dort gehört er in url("{asset:%1$s}").',
|
||||
$match[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentTemplate;
|
||||
|
||||
class UpdateDocumentTemplateRequest
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $blocks Blockinhalte, adressiert über den Block-Slug.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $documentType,
|
||||
public readonly array $blocks,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateDocumentTemplate;
|
||||
|
||||
class UpdateDocumentTemplateResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
|
||||
public ?string $message = null;
|
||||
|
||||
/** @var array<int, string> Blöcke, die nicht gespeichert wurden (unbekannt oder nicht editierbar). */
|
||||
public array $skipped = [];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||
|
||||
use App\Models\GlobalSetting;
|
||||
|
||||
class UpdateGlobalSettingsAction
|
||||
{
|
||||
public function __construct(private UpdateGlobalSettingsRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateGlobalSettingsResponse
|
||||
{
|
||||
$response = new UpdateGlobalSettingsResponse();
|
||||
|
||||
$values = [
|
||||
GlobalSetting::LOCAL_GROUP_LABEL => trim($this->request->localGroupLabel),
|
||||
GlobalSetting::LOCAL_GROUP_LABEL_PLURAL => trim($this->request->localGroupLabelPlural),
|
||||
GlobalSetting::PARENT_GROUP_LABEL => trim($this->request->parentGroupLabel),
|
||||
GlobalSetting::PARENT_GROUP_LABEL_PLURAL => trim($this->request->parentGroupLabelPlural),
|
||||
];
|
||||
|
||||
// Eine leere Bezeichnung ließe Überschriften und Auswahlfelder ohne Beschriftung zurück.
|
||||
if (in_array('', $values, true)) {
|
||||
$response->message = 'Bitte alle Bezeichnungen ausfüllen.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->request->globalSettings->setMany($values);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Einstellungen wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||
|
||||
use App\Repositories\GlobalSettingRepository;
|
||||
|
||||
class UpdateGlobalSettingsRequest
|
||||
{
|
||||
public function __construct(
|
||||
public GlobalSettingRepository $globalSettings,
|
||||
public string $localGroupLabel,
|
||||
public string $localGroupLabelPlural,
|
||||
public string $parentGroupLabel,
|
||||
public string $parentGroupLabelPlural,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateGlobalSettings;
|
||||
|
||||
class UpdateGlobalSettingsResponse
|
||||
{
|
||||
public bool $success = false;
|
||||
public string $message = '';
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantContact;
|
||||
|
||||
use App\Support\Text;
|
||||
|
||||
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,
|
||||
'invoice_sender_name' => Text::nullIfBlank($this->request->invoiceSenderName),
|
||||
// Leergelassene optionale Felder als NULL, damit sie im Briefkopf sauber wegfallen.
|
||||
'address_1' => Text::nullIfBlank($this->request->address1),
|
||||
'address_2' => Text::nullIfBlank($this->request->address2),
|
||||
'address_3' => Text::nullIfBlank($this->request->address3),
|
||||
'postcode' => $this->request->postcode,
|
||||
'city' => $this->request->city,
|
||||
'phone' => Text::nullIfBlank($this->request->phone),
|
||||
]);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Kontaktdaten wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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,
|
||||
/** Bezeichnung des Rechnungsstellers; leer bedeutet "Name des Mandanten verwenden". */
|
||||
public ?string $invoiceSenderName = null,
|
||||
/** Straße und Hausnummer -- Pflichtangabe auf Rechnungen nach § 14 Abs. 4 UStG. */
|
||||
public ?string $address1 = null,
|
||||
/** Optionale Zusatzzeile, z.B. "c/o ...". */
|
||||
public ?string $address2 = null,
|
||||
public ?string $address3 = null,
|
||||
public ?string $phone = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
|
||||
class UpdateTenantGeneralAction
|
||||
{
|
||||
public function __construct(private UpdateTenantGeneralRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateTenantGeneralResponse
|
||||
{
|
||||
$response = new UpdateTenantGeneralResponse();
|
||||
|
||||
if ($this->request->parentGroup === null) {
|
||||
$response->message = sprintf('Bitte bei „%s" eine Auswahl treffen.', ParticipantGroupType::ParentGroup->label());
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->request->tenant->update([
|
||||
'name' => $this->request->name,
|
||||
'slug' => $this->request->slug,
|
||||
'url' => $this->request->url,
|
||||
'parent_group_id' => $this->request->parentGroup->id,
|
||||
]);
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Allgemeine Daten wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantGeneral;
|
||||
|
||||
use App\Models\ParentGroup;
|
||||
use App\Models\Tenant;
|
||||
|
||||
class UpdateTenantGeneralRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Tenant $tenant,
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $url,
|
||||
/** Pflicht; null heißt, die übermittelte ID war unbekannt. */
|
||||
public ?ParentGroup $parentGroup,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantPayment;
|
||||
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use App\Models\PaymentMethod;
|
||||
use App\Scopes\SiteScope;
|
||||
|
||||
class UpdateTenantPaymentAction
|
||||
{
|
||||
/** Zahlungsarten, deren Konto das Konto des Stammes ist (Überweisung: Ziel, Lastschrift: Gläubiger). */
|
||||
private const array BANK_ACCOUNT_SLUGS = [
|
||||
PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION,
|
||||
PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT,
|
||||
];
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
foreach (self::BANK_ACCOUNT_SLUGS as $slug) {
|
||||
$this->syncPaymentConfiguration($slug);
|
||||
}
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'IBAN-Informationen wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Übernimmt die eingegebenen IBAN-Daten direkt in die Tenant-Config der Zahlungsart
|
||||
* (Kontoinhaber/IBAN/BIC), damit sie nicht doppelt gepflegt werden müssen. Bestehende weitere
|
||||
* Config-Werte (z.B. Symbol, Gläubiger-ID) bleiben erhalten. Der Ziel-Tenant kann ein verwalteter
|
||||
* sein, daher ohne SiteScope explizit auf den Tenant-Slug gefiltert.
|
||||
*/
|
||||
private function syncPaymentConfiguration(string $slug): void
|
||||
{
|
||||
$method = AvailablePaymentMethod::withoutGlobalScope(SiteScope::class)
|
||||
->where('tenant', $this->request->tenant->slug)
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
|
||||
if ($method === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method->configuration = PaymentMethod::sanitizeConfiguration($slug, array_merge(
|
||||
$method->configuration ?? [],
|
||||
[
|
||||
'account_owner' => $this->request->accountName,
|
||||
'iban' => $this->request->accountIban,
|
||||
'bic' => $this->request->accountBic,
|
||||
],
|
||||
));
|
||||
$method->save();
|
||||
}
|
||||
}
|
||||
@@ -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,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
||||
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Models\Tenant;
|
||||
use App\Support\Text;
|
||||
|
||||
class UpdateTenantTaxAction
|
||||
{
|
||||
public function __construct(private UpdateTenantTaxRequest $request)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): UpdateTenantTaxResponse
|
||||
{
|
||||
$response = new UpdateTenantTaxResponse();
|
||||
|
||||
// Präfixe sind großgeschrieben; null heißt "nicht mitgeschickt" und damit "unverändert".
|
||||
$prefix = Text::nullIfBlank(strtoupper((string) $this->request->invoicePrefix));
|
||||
|
||||
if ($prefix !== null && $this->prefixTakenByAnotherTenant($prefix)) {
|
||||
$response->message = sprintf('Das Rechnungs-Präfix "%s" wird bereits von einem anderen Mandanten genutzt.', $prefix);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($this->request->taxLiable) {
|
||||
$this->request->tenant->update($this->withInvoiceDetails([
|
||||
'tax_liable' => true,
|
||||
'vat_rate' => $this->clampVatRate($this->request->vatRate),
|
||||
'vat_pricing_mode' => $this->request->vatPricingMode->value,
|
||||
'tax_exemption_reason' => null,
|
||||
'tax_exemption_note' => null,
|
||||
], $prefix));
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Steuerinformationen wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$reason = $this->request->exemptionReason;
|
||||
|
||||
if ($reason === null) {
|
||||
$response->message = 'Bitte einen gültigen Befreiungsgrund auswählen.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$note = trim((string)$this->request->exemptionNote);
|
||||
|
||||
if ($reason->requires_note && $note === '') {
|
||||
$response->message = 'Bitte einen Text für den Befreiungsgrund angeben.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->request->tenant->update($this->withInvoiceDetails([
|
||||
'tax_liable' => false,
|
||||
'vat_rate' => 0,
|
||||
'vat_pricing_mode' => VatPricingMode::Inclusive->value,
|
||||
'tax_exemption_reason' => $reason->slug,
|
||||
'tax_exemption_note' => $reason->requires_note ? $note : null,
|
||||
], $prefix));
|
||||
|
||||
$response->success = true;
|
||||
$response->message = 'Steuerinformationen wurden gespeichert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function clampVatRate(int $vatRate): int
|
||||
{
|
||||
return max(0, min(100, $vatRate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ergänzt die Rechnungsstellerangaben. Sie gelten unabhängig davon, ob Steuerpflicht besteht -- eine
|
||||
* Steuernummer gehört auch auf eine steuerfreie Rechnung.
|
||||
*
|
||||
* @param array<string, mixed> $attributes
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function withInvoiceDetails(array $attributes, ?string $prefix): array
|
||||
{
|
||||
$attributes['tax_number'] = Text::nullIfBlank($this->request->taxNumber);
|
||||
$attributes['vat_id'] = Text::nullIfBlank($this->request->vatId);
|
||||
|
||||
// Nicht mitgeschickt heißt "unverändert", nicht "leeren".
|
||||
if ($prefix !== null) {
|
||||
$attributes['invoice_prefix'] = $prefix;
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
private function prefixTakenByAnotherTenant(string $prefix): bool
|
||||
{
|
||||
return Tenant::where('invoice_prefix', $prefix)
|
||||
->where('id', '!=', $this->request->tenant->id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
||||
|
||||
use App\Enumerations\TaxExemptionReason;
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Models\Tenant;
|
||||
|
||||
class UpdateTenantTaxRequest
|
||||
{
|
||||
public function __construct(
|
||||
public Tenant $tenant,
|
||||
public bool $taxLiable,
|
||||
public int $vatRate,
|
||||
public ?TaxExemptionReason $exemptionReason,
|
||||
public ?string $exemptionNote,
|
||||
public VatPricingMode $vatPricingMode,
|
||||
/** Steuernummer bzw. USt-IdNr. -- Pflichtangabe auf Rechnungen (§ 14 Abs. 4 Nr. 2 UStG). */
|
||||
public ?string $taxNumber = null,
|
||||
public ?string $vatId = null,
|
||||
/**
|
||||
* Erster Block der Rechnungsnummer. `null` heißt "unverändert lassen" -- im Self-Service wird das
|
||||
* Feld gar nicht erst mitgeschickt, damit ein bestehender Nummernkreis nicht bricht.
|
||||
*/
|
||||
public ?string $invoicePrefix = null,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Actions\UpdateTenantTax;
|
||||
|
||||
class UpdateTenantTaxResponse
|
||||
{
|
||||
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,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Models\DocumentAsset;
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DocumentAssetDeleteController extends CommonController
|
||||
{
|
||||
public function __invoke(string $name): JsonResponse
|
||||
{
|
||||
$asset = DocumentAsset::where('name', $name)->first();
|
||||
|
||||
// Durchweg 200 mit Status im Body -- wie die übrigen Admin-Endpunkte, damit der Grund im Frontend
|
||||
// ankommt (der Ajax-Helfer verwirft den Body bei Fehler-Statuscodes).
|
||||
if ($asset === null) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Das Bild existiert nicht.']);
|
||||
}
|
||||
|
||||
// Ein noch referenziertes Bild zu löschen würde stillschweigend eine Lücke in jede künftige
|
||||
// Rechnung reißen -- der Platzhalter löst dann zu einem Leerstring auf.
|
||||
if ($this->isReferenced($asset->name)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => sprintf('Das Bild wird noch als {asset:%s} in der Vorlage verwendet.', $asset->name),
|
||||
]);
|
||||
}
|
||||
|
||||
$asset->delete();
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => 'Das Bild wurde gelöscht.']);
|
||||
}
|
||||
|
||||
private function isReferenced(string $name): bool
|
||||
{
|
||||
return DocumentTemplate::query()
|
||||
->where('content', 'like', '%{asset:' . $name . '}%')
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateDocumentAsset\UpdateDocumentAssetAction;
|
||||
use App\Domains\Admin\Actions\UpdateDocumentAsset\UpdateDocumentAssetRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DocumentAssetUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$action = new UpdateDocumentAssetAction(new UpdateDocumentAssetRequest(
|
||||
name: (string) $request->input('name'),
|
||||
label: $request->input('label'),
|
||||
file: $request->file('file'),
|
||||
));
|
||||
|
||||
$response = $action->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
'token' => $response->asset !== null ? '{asset:' . $response->asset->name . '}' : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Models\DocumentAsset;
|
||||
use App\Models\DocumentTemplate;
|
||||
use App\Models\DocumentTypeCatalog;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DocumentTemplatesGetController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$documentType = (string) $request->input('type', DocumentTypeCatalog::default());
|
||||
|
||||
if (!DocumentTypeCatalog::has($documentType)) {
|
||||
abort(422, 'Unbekannte Dokumentart.');
|
||||
}
|
||||
|
||||
$blocks = DocumentTemplate::forType($documentType)
|
||||
->values()
|
||||
->map(fn(DocumentTemplate $block): array => [
|
||||
'block' => $block->block,
|
||||
'label' => DocumentTypeCatalog::blockLabel($documentType, $block->block),
|
||||
'content' => (string) $block->content,
|
||||
'editable' => $block->editable,
|
||||
'source' => in_array($block->block, DocumentTypeCatalog::SOURCE_BLOCKS, true),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'documentType' => $documentType,
|
||||
'documentTypes' => DocumentTypeCatalog::options(),
|
||||
'blocks' => $blocks,
|
||||
// Die Bilder gelten für alle Dokumentarten -- sie hängen nicht am Typ.
|
||||
'assets' => DocumentAsset::orderBy('name')->get()->map(fn(DocumentAsset $asset): array => [
|
||||
'name' => $asset->name,
|
||||
'label' => $asset->label,
|
||||
'mime' => $asset->mime,
|
||||
'token' => '{asset:' . $asset->name . '}',
|
||||
'preview' => $asset->toDataUri(),
|
||||
]),
|
||||
'tokenGroups' => DocumentTypeCatalog::tokenGroups($documentType),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Inertia\Response;
|
||||
|
||||
class DocumentTemplatesPageController extends CommonController
|
||||
{
|
||||
public function __invoke(): Response
|
||||
{
|
||||
return new InertiaProvider('Admin/DocumentTemplates', [])->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Models\DocumentTypeCatalog;
|
||||
use App\Providers\DocumentTemplateRenderProvider;
|
||||
use App\Providers\PdfGenerateAndDownloadProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
/**
|
||||
* Rendert die Vorlage mit Beispieldaten, ohne sie zu speichern -- damit niemand blind HTML editiert.
|
||||
*/
|
||||
class DocumentTemplatesPreviewController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$documentType = (string) $request->input('type', DocumentTypeCatalog::default());
|
||||
|
||||
if (!DocumentTypeCatalog::has($documentType)) {
|
||||
abort(422, 'Unbekannte Dokumentart.');
|
||||
}
|
||||
|
||||
$html = new DocumentTemplateRenderProvider(
|
||||
$documentType,
|
||||
(array) $request->input('blocks', []),
|
||||
)->render(DocumentTypeCatalog::sampleTokens($documentType));
|
||||
|
||||
return response(PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait'), 200, [
|
||||
'Content-Type' => 'application/pdf',
|
||||
'Content-Disposition' => 'inline; filename="vorschau.pdf"',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateDocumentTemplate\UpdateDocumentTemplateAction;
|
||||
use App\Domains\Admin\Actions\UpdateDocumentTemplate\UpdateDocumentTemplateRequest;
|
||||
use App\Models\DocumentTypeCatalog;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DocumentTemplatesUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$documentType = (string) $request->input('type', DocumentTypeCatalog::default());
|
||||
|
||||
// Die Dokumentart bestimmt, welche Zeilen geschrieben werden -- ungeprüft weitergereicht wäre
|
||||
// sie ein Weg, in beliebige Vorlagen zu schreiben.
|
||||
if (!DocumentTypeCatalog::has($documentType)) {
|
||||
abort(422, 'Unbekannte Dokumentart.');
|
||||
}
|
||||
|
||||
$action = new UpdateDocumentTemplateAction(new UpdateDocumentTemplateRequest(
|
||||
documentType: $documentType,
|
||||
blocks: (array) $request->input('blocks', []),
|
||||
));
|
||||
|
||||
$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\Enumerations\ParticipantGroupType;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GlobalSettingsGetController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'groupLabels' => ParticipantGroupType::labels(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class GlobalSettingsPageController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
return new InertiaProvider('Admin/GlobalSettings', [])->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateGlobalSettings\UpdateGlobalSettingsAction;
|
||||
use App\Domains\Admin\Actions\UpdateGlobalSettings\UpdateGlobalSettingsRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GlobalSettingsUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$response = new UpdateGlobalSettingsAction(new UpdateGlobalSettingsRequest(
|
||||
globalSettings: $this->globalSettings,
|
||||
localGroupLabel: (string) $request->input('localGroup'),
|
||||
localGroupLabelPlural: (string) $request->input('localGroupPlural'),
|
||||
parentGroupLabel: (string) $request->input('parentGroup'),
|
||||
parentGroupLabelPlural: (string) $request->input('parentGroupPlural'),
|
||||
))->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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,
|
||||
'invoice_sender_name' => $tenant->invoice_sender_name,
|
||||
// Als Platzhalter im Formular: so heisst der Absender, solange nichts eingetragen ist.
|
||||
'name' => $tenant->name,
|
||||
'phone' => $tenant->phone,
|
||||
'address_1' => $tenant->address_1,
|
||||
'address_2' => $tenant->address_2,
|
||||
'address_3' => $tenant->address_3,
|
||||
'postcode' => $tenant->postcode,
|
||||
'city' => $tenant->city,
|
||||
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/contact',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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'),
|
||||
invoiceSenderName: $request->input('invoice_sender_name'),
|
||||
address1: $request->input('address_1'),
|
||||
address2: $request->input('address_2'),
|
||||
address3: $request->input('address_3'),
|
||||
phone: $request->input('phone'),
|
||||
));
|
||||
|
||||
$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,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Enumerations\TaxExemptionReason;
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ManagedTenantTaxGetController extends CommonController
|
||||
{
|
||||
public function __invoke(string $slug, Request $request): JsonResponse
|
||||
{
|
||||
$tenant = $this->adminTenants->findBySlug($slug);
|
||||
|
||||
return response()->json([
|
||||
'tax_liable' => $tenant->tax_liable,
|
||||
'vat_rate' => $tenant->vat_rate,
|
||||
'tax_exemption_reason' => $tenant->tax_exemption_reason,
|
||||
'tax_exemption_note' => $tenant->tax_exemption_note,
|
||||
'vat_pricing_mode' => $tenant->vat_pricing_mode,
|
||||
'tax_number' => $tenant->tax_number,
|
||||
'vat_id' => $tenant->vat_id,
|
||||
'invoice_prefix' => $tenant->invoice_prefix,
|
||||
// In der Mandanten-Verwaltung ist der Nummernkreis änderbar.
|
||||
'can_edit_invoice_prefix' => true,
|
||||
'exemption_reasons' => TaxExemptionReason::options(),
|
||||
'pricing_modes' => VatPricingMode::options(),
|
||||
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/tax',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
|
||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
|
||||
use App\Enumerations\TaxExemptionReason;
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ManagedTenantTaxUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(string $slug, Request $request): JsonResponse
|
||||
{
|
||||
$tenant = $this->adminTenants->findBySlug($slug);
|
||||
|
||||
$action = new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
|
||||
tenant: $tenant,
|
||||
taxLiable: $request->boolean('tax_liable'),
|
||||
vatRate: (int)$request->input('vat_rate', 0),
|
||||
exemptionReason: TaxExemptionReason::where('slug', $request->input('tax_exemption_reason'))->first(),
|
||||
exemptionNote: $request->input('tax_exemption_note'),
|
||||
vatPricingMode: VatPricingMode::tryFrom((string) $request->input('vat_pricing_mode', 'inclusive')) ?? VatPricingMode::Inclusive,
|
||||
taxNumber: $request->input('tax_number'),
|
||||
vatId: $request->input('vat_id'),
|
||||
invoicePrefix: $request->input('invoice_prefix'),
|
||||
));
|
||||
|
||||
$response = $action->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\CreateParentGroup\CreateParentGroupAction;
|
||||
use App\Domains\Admin\Actions\CreateParentGroup\CreateParentGroupRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParentGroupCreateController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$response = new CreateParentGroupAction(new CreateParentGroupRequest(
|
||||
name: (string) $request->input('name'),
|
||||
))->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\DeleteParentGroup\DeleteParentGroupAction;
|
||||
use App\Domains\Admin\Actions\DeleteParentGroup\DeleteParentGroupRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParentGroupDeleteController extends CommonController
|
||||
{
|
||||
public function __invoke(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$parentGroup = $this->adminParentGroups->findById($id);
|
||||
|
||||
if ($parentGroup === null) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Eintrag nicht gefunden.'], 404);
|
||||
}
|
||||
|
||||
$response = new DeleteParentGroupAction(new DeleteParentGroupRequest(
|
||||
parentGroup: $parentGroup,
|
||||
inUse: $this->adminParentGroups->isInUse($parentGroup),
|
||||
))->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 ParentGroupListApiController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$parentGroups = $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||
'id' => $parentGroup->id,
|
||||
'name' => $parentGroup->name,
|
||||
'isFallback' => $parentGroup->is_fallback,
|
||||
'tenantCount' => $parentGroup->tenants_count,
|
||||
'deletable' => !$parentGroup->is_fallback && !$this->adminParentGroups->isInUse($parentGroup),
|
||||
]);
|
||||
|
||||
return response()->json(['parentGroups' => $parentGroups]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
use App\Providers\InertiaProvider;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Response;
|
||||
|
||||
class ParentGroupListPageController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
return new InertiaProvider('Admin/ParentGroupList', [
|
||||
'groupLabels' => ParticipantGroupType::labels(),
|
||||
])->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\RenameParentGroup\RenameParentGroupAction;
|
||||
use App\Domains\Admin\Actions\RenameParentGroup\RenameParentGroupRequest;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ParentGroupUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$parentGroup = $this->adminParentGroups->findById($id);
|
||||
|
||||
if ($parentGroup === null) {
|
||||
return response()->json(['status' => 'error', 'message' => 'Eintrag nicht gefunden.'], 404);
|
||||
}
|
||||
|
||||
$response = new RenameParentGroupAction(new RenameParentGroupRequest(
|
||||
parentGroup: $parentGroup,
|
||||
name: (string) $request->input('name'),
|
||||
))->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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,
|
||||
'invoice_sender_name' => $this->tenant->invoice_sender_name,
|
||||
// Als Platzhalter im Formular: so heisst der Absender, solange nichts eingetragen ist.
|
||||
'name' => $this->tenant->name,
|
||||
'phone' => $this->tenant->phone,
|
||||
'address_1' => $this->tenant->address_1,
|
||||
'address_2' => $this->tenant->address_2,
|
||||
'address_3' => $this->tenant->address_3,
|
||||
'postcode' => $this->tenant->postcode,
|
||||
'city' => $this->tenant->city,
|
||||
'saveEndpoint' => '/api/v1/admin/tenant/contact',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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'),
|
||||
invoiceSenderName: $request->input('invoice_sender_name'),
|
||||
address1: $request->input('address_1'),
|
||||
address2: $request->input('address_2'),
|
||||
address3: $request->input('address_3'),
|
||||
phone: $request->input('phone'),
|
||||
));
|
||||
|
||||
$response = $action->execute();
|
||||
|
||||
return response()->json([
|
||||
'status' => $response->success ? 'success' : 'error',
|
||||
'message' => $response->message,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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'),
|
||||
parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')),
|
||||
));
|
||||
|
||||
$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,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
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,
|
||||
'parent_group_id' => $tenant->parent_group_id,
|
||||
'parentGroups' => $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||
'id' => $parentGroup->id,
|
||||
'name' => $parentGroup->name,
|
||||
]),
|
||||
'parentGroupLabel' => ParticipantGroupType::ParentGroup->label(),
|
||||
'saveEndpoint' => '/api/v1/admin/tenants/' . $slug . '/general',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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'),
|
||||
parentGroup: $this->adminParentGroups->findById((int) $request->input('parent_group_id')),
|
||||
));
|
||||
|
||||
$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,34 @@
|
||||
<?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,
|
||||
'parentGroup' => $tenant->parentGroup?->name,
|
||||
];
|
||||
});
|
||||
|
||||
// Auswahl für das Anlage-Formular (Pflichtangabe).
|
||||
$parentGroups = $this->adminParentGroups->all()->map(fn ($parentGroup) => [
|
||||
'id' => $parentGroup->id,
|
||||
'name' => $parentGroup->name,
|
||||
'isFallback' => $parentGroup->is_fallback,
|
||||
]);
|
||||
|
||||
return response()->json(['tenants' => $mapped, 'parentGroups' => $parentGroups]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Enumerations\ParticipantGroupType;
|
||||
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', [
|
||||
'groupLabels' => ParticipantGroupType::labels(),
|
||||
]);
|
||||
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,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use App\Resources\AvailablePaymentMethodResource;
|
||||
use App\Scopes\CommonController;
|
||||
use App\ValueObjects\BankStatementRuleset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantPaymentMethodsGetController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'paymentMethods' => AvailablePaymentMethod::all()
|
||||
->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)),
|
||||
'saveEndpoint' => '/api/v1/admin/tenant/payment-methods',
|
||||
// Der app-weite Standard für das Kontoauszug-Format. Das Formular zeigt ihn als
|
||||
// Vorbelegung an, damit erkennbar ist, wovon ein Override abweicht.
|
||||
'statementRulesetDefault' => BankStatementRuleset::default()->toArray(),
|
||||
'statementRulesetCharsets' => BankStatementRuleset::CHARSETS,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodAction;
|
||||
use App\Domains\Admin\Actions\UpdateAvailablePaymentMethod\UpdateAvailablePaymentMethodRequest;
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantPaymentMethodsUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$availablePaymentMethod = AvailablePaymentMethod::findOrFail($id);
|
||||
|
||||
$action = new UpdateAvailablePaymentMethodAction(new UpdateAvailablePaymentMethodRequest(
|
||||
availablePaymentMethod: $availablePaymentMethod,
|
||||
name: $request->input('name'),
|
||||
description: $request->input('description'),
|
||||
active: (bool) $request->input('active'),
|
||||
configuration: (array) $request->input('configuration', []),
|
||||
));
|
||||
|
||||
$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\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,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Enumerations\TaxExemptionReason;
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantTaxGetController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'tax_liable' => $this->tenant->tax_liable,
|
||||
'vat_rate' => $this->tenant->vat_rate,
|
||||
'tax_exemption_reason' => $this->tenant->tax_exemption_reason,
|
||||
'tax_exemption_note' => $this->tenant->tax_exemption_note,
|
||||
'vat_pricing_mode' => $this->tenant->vat_pricing_mode,
|
||||
'tax_number' => $this->tenant->tax_number,
|
||||
'vat_id' => $this->tenant->vat_id,
|
||||
'invoice_prefix' => $this->tenant->invoice_prefix,
|
||||
// Im Self-Service nicht änderbar: das Präfix bestimmt den Nummernkreis.
|
||||
'can_edit_invoice_prefix' => false,
|
||||
'exemption_reasons' => TaxExemptionReason::options(),
|
||||
'pricing_modes' => VatPricingMode::options(),
|
||||
'saveEndpoint' => '/api/v1/admin/tenant/tax',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Admin\Controllers;
|
||||
|
||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxAction;
|
||||
use App\Domains\Admin\Actions\UpdateTenantTax\UpdateTenantTaxRequest;
|
||||
use App\Enumerations\TaxExemptionReason;
|
||||
use App\Enumerations\VatPricingMode;
|
||||
use App\Scopes\CommonController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TenantTaxUpdateController extends CommonController
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$action = new UpdateTenantTaxAction(new UpdateTenantTaxRequest(
|
||||
tenant: $this->tenant,
|
||||
taxLiable: $request->boolean('tax_liable'),
|
||||
vatRate: (int)$request->input('vat_rate', 0),
|
||||
exemptionReason: TaxExemptionReason::where('slug', $request->input('tax_exemption_reason'))->first(),
|
||||
exemptionNote: $request->input('tax_exemption_note'),
|
||||
vatPricingMode: VatPricingMode::tryFrom((string) $request->input('vat_pricing_mode', 'inclusive')) ?? VatPricingMode::Inclusive,
|
||||
taxNumber: $request->input('tax_number'),
|
||||
vatId: $request->input('vat_id'),
|
||||
// invoice_prefix wird hier bewusst nicht durchgereicht -- der Nummernkreis ist nur in der
|
||||
// Mandanten-Verwaltung änderbar.
|
||||
));
|
||||
|
||||
$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' => currentUser()?->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]);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 566 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user