Support oft parent groups

This commit is contained in:
2026-09-29 14:35:33 +02:00
parent c4ca101594
commit a5e4e55df5
86 changed files with 1936 additions and 80 deletions
+31
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use App\Casts\AmountCast;
use App\Enumerations\EatingHabit;
use App\Enumerations\ParticipantGroupType;
use App\Enumerations\ParticipationFeeType;
use App\RelationModels\EventParticipationFee;
use App\RelationModels\EventPaymentMethods;
@@ -13,6 +14,7 @@ use DateTime;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
/**
* @property string $tenant
@@ -50,6 +52,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property boolean $short_registration
* @property boolean $swimming_permission_required
* @property string|null $invoice_key
* @property string $participant_group_type
*/
class Event extends InstancedModel
{
@@ -101,6 +104,7 @@ class Event extends InstancedModel
'swimming_permission_required',
'invoice_key',
'participant_group_type',
];
protected $casts = [
@@ -186,6 +190,33 @@ class Event extends InstancedModel
return $this->belongsToMany(Tenant::class, 'event_local_groups', 'event_id', 'local_group_id');
}
public function participantGroupType() : ParticipantGroupType {
return ParticipantGroupType::fromStored($this->participant_group_type);
}
public function asksForParentGroup() : bool {
return $this->participantGroupType() === ParticipantGroupType::ParentGroup;
}
/**
* Was bei der Anmeldung zur Auswahl steht: die teilnehmenden Local Groups oder -- fragt die Veranstaltung nach der
* Parent Group -- deren Parent Groups, jede nur einmal (alphabetisch, „Ohne Zuordnung" zuletzt).
* Einzige Quelle für Anzeige und Prüfung der Auswahl.
*
* @return Collection<int, Tenant>|Collection<int, ParentGroup>
*/
public function participantGroupOptions() : Collection {
$localGroups = $this->localGroups()->with('parentGroup')->get();
if (!$this->asksForParentGroup()) {
return $localGroups;
}
return ParentGroup::sortForDisplay(
$localGroups->pluck('parentGroup')->filter()->unique('id')
);
}
public function eatingHabits() : BelongsToMany{
return $this->belongsToMany(EatingHabit::class, 'event_allowed_eating_habits', 'event_id', 'eating_habit_id');
}
+17
View File
@@ -38,6 +38,7 @@ class EventParticipant extends InstancedModel
'fee_type',
'sibling_reduction',
'local_group',
'parent_group_id',
'birthday',
'address_1',
@@ -143,6 +144,22 @@ class EventParticipant extends InstancedModel
return $this->belongsTo(Tenant::class, 'local_group', 'slug');
}
/** Parent Group -- nur bei Veranstaltungen gesetzt, die danach statt nach der Local Group fragen. */
public function parentGroup()
{
return $this->belongsTo(ParentGroup::class, 'parent_group_id');
}
/** Anzeigename der bei der Anmeldung gewählten Gruppe, je nachdem, wonach die Veranstaltung fragt. */
public function participantGroupName(Event $event) : string
{
if ($event->asksForParentGroup()) {
return $this->parentGroup?->name ?? 'Keine Angabe';
}
return $this->localGroup?->name ?? 'Nicht im LV';
}
public function participationType()
{
return $this->belongsTo(ParticipationType::class, 'participation_type', 'slug');
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Scopes\CommonModel;
/**
* App-weite Einstellung als Schlüssel/Wert-Paar (z. B. die Bezeichnungen „Stamm"/„Bund").
* Nicht mandantengebunden; gelesen wird über {@see \App\Repositories\GlobalSettingRepository}.
*
* @property string $key
* @property string $value
*/
class GlobalSetting extends CommonModel
{
public const string LOCAL_GROUP_LABEL = 'local_group_label';
public const string LOCAL_GROUP_LABEL_PLURAL = 'local_group_label_plural';
public const string PARENT_GROUP_LABEL = 'parent_group_label';
public const string PARENT_GROUP_LABEL_PLURAL = 'parent_group_label_plural';
protected $table = 'global_settings';
protected $primaryKey = 'key';
protected $keyType = 'string';
public $incrementing = false;
protected $fillable = [
'key',
'value',
];
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use App\Scopes\CommonModel;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
/**
* Übergeordnete Gruppe (Parent Group, im UI z. B. „Bund"). Mehrere Mandanten (Local Groups) gehören zur selben
* Parent Group. Nicht mandantengebunden -- die Liste gilt app-weit.
*
* @property int $id
* @property string $name
* @property boolean $is_fallback
*/
class ParentGroup extends CommonModel
{
protected $table = 'parent_groups';
protected $fillable = [
'name',
'is_fallback',
];
protected $casts = [
'is_fallback' => 'boolean',
];
/** Die Gruppe „Ohne Zuordnung" -- Vorgabe für Mandanten, denen keine Gruppe zugewiesen wurde. */
public static function fallback() : self {
return self::where('is_fallback', true)->orderBy('id')->firstOrFail();
}
/** Anzeigereihenfolge: alphabetisch, „Ohne Zuordnung" (Fallback) immer zuletzt. */
public function scopeOrdered(Builder $query) : Builder {
return $query->orderBy('is_fallback')->orderBy('name');
}
/**
* Dieselbe Reihenfolge wie {@see scopeOrdered()} für bereits geladene Gruppen.
*
* @param Collection<int, self> $parentGroups
* @return Collection<int, self>
*/
public static function sortForDisplay(Collection $parentGroups) : Collection {
return $parentGroups
->sort(static fn (self $a, self $b): int =>
[$a->is_fallback, mb_strtolower($a->name)] <=> [$b->is_fallback, mb_strtolower($b->name)])
->values();
}
public function tenants() : HasMany {
return $this->hasMany(Tenant::class, 'parent_group_id');
}
public function eventParticipants() : HasMany {
return $this->hasMany(EventParticipant::class, 'parent_group_id');
}
}
+9
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use App\Scopes\CommonModel;
use App\Support\Text;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property string $slug
@@ -35,6 +36,7 @@ use App\Support\Text;
* @property string|null $tax_exemption_reason
* @property string|null $tax_exemption_note
* @property string $vat_pricing_mode
* @property int $parent_group_id
*/
class Tenant extends CommonModel
{
@@ -46,9 +48,15 @@ class Tenant extends CommonModel
{
static::creating(static function (self $tenant): void {
$tenant->invoice_prefix ??= strtoupper((string) $tenant->slug);
// Die Parent Group ist Pflicht; ohne Angabe landet der Mandant bei „Ohne Zuordnung".
$tenant->parent_group_id ??= ParentGroup::fallback()->id;
});
}
public function parentGroup() : BelongsTo {
return $this->belongsTo(ParentGroup::class, 'parent_group_id');
}
public static function getTempDirectory() : string {
return currentTenant()->slug . '/temp-data/';
}
@@ -95,6 +103,7 @@ class Tenant extends CommonModel
'tax_number',
'vat_id',
'invoice_prefix',
'parent_group_id',
];
protected $casts = [