diff --git a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue index 08254f2..c0dd4f9 100644 --- a/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue +++ b/app/Domains/Dashboard/Views/Partials/Widgets/MyParticipations.vue @@ -33,7 +33,7 @@ function navigateTo(url) {
- 📍 {{ props.event.postalCode }} {{ props.event.location }} + 📍 {{ props.event.fullAddress }}
- 📍 {{ props.event.postalCode }} {{ props.event.location }} + 📍 {{ props.event.fullAddress }}
diff --git a/app/Models/Event.php b/app/Models/Event.php index 3fe8098..68e6a4b 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property int $cost_unit_id * @property string $name * @property string $location + * @property string|null $street + * @property string|null $house_number * @property string $postal_code * @property string $email * @property DateTime $start_date @@ -59,6 +61,8 @@ class Event extends InstancedModel 'name', 'identifier', 'location', + 'street', + 'house_number', 'postal_code', 'email', 'start_date', @@ -131,6 +135,18 @@ class Event extends InstancedModel 'swimming_permission_required' => 'boolean', ]; + /** + * Anschrift des Veranstaltungsorts in einer Zeile. Straße und Hausnummer sind optional -- fehlen sie, + * bleibt es bei der bisherigen Ausgabe "PLZ Ort". + */ + public function getFullAddress(): string + { + $city = trim($this->postal_code . ' ' . $this->location); + $street = trim($this->street . ' ' . $this->house_number); + + return '' === $street ? $city : $street . ', ' . $city; + } + public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class, 'tenant', 'slug'); diff --git a/app/Resources/EventParticipantResource.php b/app/Resources/EventParticipantResource.php index 9a679bb..83c6b24 100644 --- a/app/Resources/EventParticipantResource.php +++ b/app/Resources/EventParticipantResource.php @@ -109,6 +109,7 @@ class EventParticipantResource extends JsonResource default => 'Nicht geprüft', }, 'eventName' => $this->resource->event()->first()->name, + 'eventAddress' => $event->getFullAddress(), 'arrivalDateReadable' => $this->resource->arrival_date->format('d.m.Y'), 'departureDateReadable' => $this->resource->departure_date->format('d.m.Y'), 'participationOptions' => $this->resource->selectedOptions()->get()->map( diff --git a/app/Resources/EventResource.php b/app/Resources/EventResource.php index ac06764..c4858ff 100644 --- a/app/Resources/EventResource.php +++ b/app/Resources/EventResource.php @@ -30,7 +30,12 @@ class EventResource extends JsonResource{ 'url' => 'https://' . currentTenant()->url . '/event/' . $this->event->identifier . '/signup', 'urlShort' => 'https://' . currentTenant()->url . '/event/' . $this->event->identifier, 'location' => $this->event->location, + 'street' => $this->event->street, + 'houseNumber' => $this->event->house_number, 'postalCode' => $this->event->postal_code, + // Fertig formatierte Anschrift für alle Anzeigen -- die Einzelfelder brauchen nur die + // Bearbeitungsformulare. + 'fullAddress' => $this->event->getFullAddress(), 'email' => $this->event->email, 'accountOwner' => $this->event->account_owner, 'accountIban' => $this->event->account_iban, diff --git a/database/migrations/2026_09_16_140010_add_street_and_house_number_to_events.php b/database/migrations/2026_09_16_140010_add_street_and_house_number_to_events.php new file mode 100644 index 0000000..59cc5dc --- /dev/null +++ b/database/migrations/2026_09_16_140010_add_street_and_house_number_to_events.php @@ -0,0 +1,26 @@ +string('street')->nullable()->after('location'); + $table->string('house_number')->nullable()->after('street'); + }); + } + + public function down(): void + { + Schema::table('events', function (Blueprint $table) { + $table->dropColumn(['street', 'house_number']); + }); + } +}; diff --git a/tests/Feature/EventAddressTest.php b/tests/Feature/EventAddressTest.php new file mode 100644 index 0000000..1d401a6 --- /dev/null +++ b/tests/Feature/EventAddressTest.php @@ -0,0 +1,177 @@ +tenant = Tenant::create([ + 'slug' => 'tv', + 'name' => 'Test', + 'email' => 't@example.com', + 'email_finance' => 'finance@example.com', + 'url' => 'test.local', + 'account_name' => 'Test e.V.', + 'account_iban' => 'DE00', + 'account_bic' => 'XY', + 'city' => 'Stadt', + 'postcode' => '00000', + 'is_active_local_group' => true, + 'has_active_instance' => true, + 'tax_liable' => false, + 'vat_rate' => 0, + 'vat_pricing_mode' => 'inclusive', + ]); + + app()->instance('tenant', $this->tenant); + + DB::table('cost_unit_types')->insert(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']); + DB::table('participation_types')->insert(['slug' => 'participant', 'name' => 'Teilnehmer']); + DB::table('participation_fee_types')->insert(['slug' => 'fixed', 'name' => 'Fix']); + EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_VEGAN, 'name' => 'Vegan']); + EatingHabit::create(['slug' => EatingHabit::EATING_HABIT_VEGETARIAN, 'name' => 'Vegetarisch']); + } + + private function createEvent(?string $street = null, ?string $houseNumber = null): Event + { + $response = new CreateEventCommand(new CreateEventRequest( + name: 'Sommerlager', + location: 'Leipzig', + postalCode: '04103', + email: 'e@example.com', + begin: new DateTime('2026-09-01'), + end: new DateTime('2026-09-05'), + earlyBirdEnd: new DateTime('2026-08-20'), + registrationFinalEnd: new DateTime('2026-08-25'), + earlyBirdEndAmountIncrease: 0, + participationFeeType: ParticipationFeeType::where('slug', 'fixed')->first(), + accountOwner: 'Owner', + accountIban: 'DE00', + payPerDay: false, + street: $street, + houseNumber: $houseNumber, + ))->execute(); + + $this->assertTrue($response->success); + + return $response->event->fresh(); + } + + private function updateEvent(Event $event, ?string $street, ?string $houseNumber): Event + { + $response = new UpdateEventCommand(new UpdateEventRequest( + event: $event, + eventName: $event->name, + eventLocation: $event->location, + postalCode: $event->postal_code, + email: $event->email, + earlyBirdEnd: new DateTime('2026-08-20'), + registrationFinalEnd: new DateTime('2026-08-25'), + alcoholicsAge: 16, + sendWeeklyReports: true, + registrationAllowed: true, + flatSupport: Amount::fromString('0,00'), + supportPerPerson: Amount::fromString('0,00'), + contributingLocalGroups: [], + eatingHabits: [], + street: $street, + houseNumber: $houseNumber, + ))->execute(); + + $this->assertTrue($response->success); + + return $event->fresh(); + } + + public function test_anlage_speichert_strasse_und_hausnummer(): void + { + $event = $this->createEvent('Musterweg', '12'); + + $this->assertSame('Musterweg', $event->street); + $this->assertSame('12', $event->house_number); + } + + public function test_anlage_ohne_angabe_laesst_beide_felder_leer(): void + { + $event = $this->createEvent(); + + $this->assertNull($event->street); + $this->assertNull($event->house_number); + } + + public function test_leere_eingaben_werden_bei_der_anlage_zu_null(): void + { + $event = $this->createEvent('', ' '); + + $this->assertNull($event->street); + $this->assertNull($event->house_number); + } + + public function test_einstellungen_ergaenzen_und_leeren_die_anschrift(): void + { + $event = $this->updateEvent($this->createEvent(), 'Musterweg', '12'); + + $this->assertSame('Musterweg', $event->street); + $this->assertSame('12', $event->house_number); + + $event = $this->updateEvent($event, '', ''); + + $this->assertNull($event->street); + $this->assertNull($event->house_number); + } + + public function test_anschrift_enthaelt_strasse_nur_wenn_sie_gefuellt_ist(): void + { + $this->assertSame('Musterweg 12, 04103 Leipzig', $this->createEvent('Musterweg', '12')->getFullAddress()); + $this->assertSame('04103 Leipzig', $this->createEvent()->getFullAddress()); + } + + public function test_resource_liefert_einzelfelder_und_fertige_anschrift(): void + { + $event = $this->createEvent('Musterweg', '12'); + + // Die Resource gibt auch die Kostenstelle aus; im Betrieb legt der Controller sie direkt nach + // der Veranstaltung an. + $event->cost_unit_id = CostUnit::create([ + 'tenant' => $this->tenant->slug, + 'name' => 'Sommerlager', + 'type' => CostUnitType::COST_UNIT_TYPE_EVENT, + 'distance_allowance' => 0.25, + 'mail_on_new' => false, + 'allow_new' => true, + 'archived' => false, + ])->id; + $event->save(); + + $resource = new EventResource($event)->toArray(new Request()); + + $this->assertSame('Musterweg', $resource['street']); + $this->assertSame('12', $resource['houseNumber']); + $this->assertSame('Musterweg 12, 04103 Leipzig', $resource['fullAddress']); + } +}