From 71a48983034d1f992bbad5a5cb258376bbafeec3 Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Sun, 17 May 2026 17:57:16 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20**Introduce=20meetup=20activity?= =?UTF-8?q?=20management**=20-=20Added=20`is=5Factive`=20and=20`last=5Feve?= =?UTF-8?q?nt=5Fat`=20fields=20to=20meetups=20with=20migration.=20-=20Enha?= =?UTF-8?q?nced=20UI:=20Display=20`Aktiv`/`Inaktiv`=20badges=20and=20last?= =?UTF-8?q?=20event=20dates=20across=20dashboard,=20tables,=20and=20maps.?= =?UTF-8?q?=20-=20Introduced=20`/meetups:update-activity`=20command=20to?= =?UTF-8?q?=20manage=20activity=20flags=20and=20timestamps.=20-=20Validate?= =?UTF-8?q?d=20latitude/longitude=20to=20prevent=20`0,0`=20inputs=20in=20c?= =?UTF-8?q?ity=20creation=20and=20updates.=20-=20Updated=20factories=20and?= =?UTF-8?q?=20tests=20to=20include=20meetup=20activity=20states=20(`active?= =?UTF-8?q?`,=20`inactive`).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Database/UpdateMeetupActivity.php | 64 +++++++++++++ app/Models/Meetup.php | 4 + database/factories/MeetupFactory.php | 18 ++++ ...6_add_activity_fields_to_meetups_table.php | 23 +++++ .../views/components/meetup-popup.blade.php | 15 +++- .../views/livewire/cities/create.blade.php | 9 ++ .../views/livewire/cities/edit.blade.php | 9 ++ resources/views/livewire/dashboard.blade.php | 6 +- .../livewire/dashboard/activities.blade.php | 6 +- .../livewire/dashboard/top-meetups.blade.php | 10 ++- .../views/livewire/meetups/create.blade.php | 13 ++- .../views/livewire/meetups/index.blade.php | 22 ++++- .../views/livewire/meetups/map.blade.php | 28 +++++- routes/console.php | 3 + tests/Feature/Cities/CityCrudTest.php | 34 +++++++ .../Database/UpdateMeetupActivityTest.php | 90 +++++++++++++++++++ 16 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 app/Console/Commands/Database/UpdateMeetupActivity.php create mode 100644 database/migrations/2026_05_17_153816_add_activity_fields_to_meetups_table.php create mode 100644 tests/Feature/Database/UpdateMeetupActivityTest.php diff --git a/app/Console/Commands/Database/UpdateMeetupActivity.php b/app/Console/Commands/Database/UpdateMeetupActivity.php new file mode 100644 index 0000000..5723e57 --- /dev/null +++ b/app/Console/Commands/Database/UpdateMeetupActivity.php @@ -0,0 +1,64 @@ +subYear(); + + Meetup::query()->chunkById(200, function ($meetups) use ($threshold) { + foreach ($meetups as $meetup) { + $this->updateMeetup($meetup, $threshold); + } + }); + + $this->info('Meetup activity flags updated.'); + + return Command::SUCCESS; + } + + private function updateMeetup(Meetup $meetup, CarbonInterface $threshold): void + { + $lastEventAt = MeetupEvent::query() + ->where('meetup_id', $meetup->id) + ->where('start', '<=', now()) + ->max('start'); + + $lastEventAt = $lastEventAt ? Date::parse($lastEventAt) : null; + + $hasFutureEvent = MeetupEvent::query() + ->where('meetup_id', $meetup->id) + ->where('start', '>', now()) + ->exists(); + + $hasActiveRecurrence = MeetupEvent::query() + ->where('meetup_id', $meetup->id) + ->whereNotNull('recurrence_type') + ->where(function ($query) { + $query->whereNull('recurrence_end_date') + ->orWhere('recurrence_end_date', '>=', now()); + }) + ->exists(); + + $isActive = ($lastEventAt && $lastEventAt->greaterThanOrEqualTo($threshold)) + || $hasFutureEvent + || $hasActiveRecurrence; + + $meetup->forceFill([ + 'is_active' => $isActive, + 'last_event_at' => $lastEventAt, + ])->saveQuietly(); + } +} diff --git a/app/Models/Meetup.php b/app/Models/Meetup.php index 9fec58c..00dbbd2 100644 --- a/app/Models/Meetup.php +++ b/app/Models/Meetup.php @@ -41,6 +41,8 @@ class Meetup extends Model implements HasMedia 'community', 'github_data', 'visible_on_map', + 'is_active', + 'last_event_at', ]; /** @@ -53,6 +55,8 @@ class Meetup extends Model implements HasMedia 'city_id' => 'integer', 'github_data' => 'json', 'simplified_geojson' => 'array', + 'is_active' => 'boolean', + 'last_event_at' => 'datetime', ]; protected static function booted() diff --git a/database/factories/MeetupFactory.php b/database/factories/MeetupFactory.php index b07f682..ad8c9a5 100644 --- a/database/factories/MeetupFactory.php +++ b/database/factories/MeetupFactory.php @@ -38,6 +38,24 @@ class MeetupFactory extends Factory 'nostr' => NostrHelper::randomNpub(), 'nostr_status' => NostrHelper::fakeNostrEventStatus(), 'created_by' => User::factory(), + 'is_active' => false, + 'last_event_at' => null, ]; } + + public function active(): static + { + return $this->state(fn (array $attrs) => [ + 'is_active' => true, + 'last_event_at' => now()->subDays(30), + ]); + } + + public function inactive(): static + { + return $this->state(fn (array $attrs) => [ + 'is_active' => false, + 'last_event_at' => now()->subYears(2), + ]); + } } diff --git a/database/migrations/2026_05_17_153816_add_activity_fields_to_meetups_table.php b/database/migrations/2026_05_17_153816_add_activity_fields_to_meetups_table.php new file mode 100644 index 0000000..8575c89 --- /dev/null +++ b/database/migrations/2026_05_17_153816_add_activity_fields_to_meetups_table.php @@ -0,0 +1,23 @@ +boolean('is_active')->default(false)->after('visible_on_map'); + $table->timestamp('last_event_at')->nullable()->after('is_active'); + }); + } + + public function down(): void + { + Schema::table('meetups', function (Blueprint $table) { + $table->dropColumn(['is_active', 'last_event_at']); + }); + } +}; diff --git a/resources/views/components/meetup-popup.blade.php b/resources/views/components/meetup-popup.blade.php index d83eb29..6a6cd69 100644 --- a/resources/views/components/meetup-popup.blade.php +++ b/resources/views/components/meetup-popup.blade.php @@ -1,7 +1,20 @@ @props(['meetup', 'url', 'eventUrl' => null])
`,
+ iconSize: [20, 20],
+ iconAnchor: [10, 20],
+ popupAnchor: [0, -20],
+ });
+
this.markers.forEach(marker => {
L.marker([marker.city.latitude, marker.city.longitude], {
- icon: btcIcon
+ icon: marker.is_active ? btcIcon : btcIconInactive
})
.bindPopup(marker.popupHtml)
.addTo(map);
diff --git a/routes/console.php b/routes/console.php
index 50e7617..c3e7d5b 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -1,6 +1,7 @@
everyFifteenMinutes();
@@ -12,3 +13,5 @@ Schedule::command(PublishUnpublishedItems::class, [
Schedule::command(PublishUnpublishedItems::class, [
'--model' => 'Meetup',
])->dailyAt('18:00');
+
+Schedule::command(UpdateMeetupActivity::class)->dailyAt('03:30');
diff --git a/tests/Feature/Cities/CityCrudTest.php b/tests/Feature/Cities/CityCrudTest.php
index c45f245..7e49fd6 100644
--- a/tests/Feature/Cities/CityCrudTest.php
+++ b/tests/Feature/Cities/CityCrudTest.php
@@ -86,6 +86,40 @@ it('rejects city creation with non-existent country', function () {
->assertHasErrors(['country_id' => 'exists']);
});
+it('rejects city creation when latitude and longitude are both zero', function () {
+ actingAsUser();
+
+ Livewire::test('cities.create')
+ ->set('name', 'Null Island')
+ ->set('country_id', $this->country->id)
+ ->set('latitude', 0)
+ ->set('longitude', 0)
+ ->call('createCity')
+ ->assertHasErrors(['latitude']);
+
+ expect(City::query()->where('name', 'Null Island')->exists())->toBeFalse();
+});
+
+it('rejects city update when latitude and longitude are both zero', function () {
+ $city = City::factory()->create([
+ 'name' => 'Berlin Test',
+ 'country_id' => $this->country->id,
+ 'latitude' => 52.52,
+ 'longitude' => 13.405,
+ ]);
+ actingAsUser();
+
+ Livewire::test('cities.edit', ['city' => $city])
+ ->set('name', 'Berlin Test')
+ ->set('country_id', $this->country->id)
+ ->set('latitude', 0)
+ ->set('longitude', 0)
+ ->call('updateCity')
+ ->assertHasErrors(['latitude']);
+
+ expect($city->refresh()->latitude)->toEqual(52.52);
+});
+
it('updates an existing city', function () {
$city = City::factory()->create(['name' => 'Old Name', 'country_id' => $this->country->id]);
actingAsUser();
diff --git a/tests/Feature/Database/UpdateMeetupActivityTest.php b/tests/Feature/Database/UpdateMeetupActivityTest.php
new file mode 100644
index 0000000..752bfa4
--- /dev/null
+++ b/tests/Feature/Database/UpdateMeetupActivityTest.php
@@ -0,0 +1,90 @@
+create(['is_active' => false, 'last_event_at' => null]);
+ MeetupEvent::factory()->create([
+ 'meetup_id' => $meetup->id,
+ 'start' => now()->subMonths(3),
+ 'recurrence_type' => null,
+ ]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeTrue()
+ ->and($meetup->last_event_at)->not->toBeNull();
+});
+
+it('marks a meetup as inactive when the last event is older than a year and no future event exists', function () {
+ $meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]);
+ MeetupEvent::factory()->create([
+ 'meetup_id' => $meetup->id,
+ 'start' => now()->subYears(2),
+ 'recurrence_type' => null,
+ ]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeFalse()
+ ->and($meetup->last_event_at)->not->toBeNull();
+});
+
+it('keeps a meetup active when it only has a future event', function () {
+ $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]);
+ MeetupEvent::factory()->create([
+ 'meetup_id' => $meetup->id,
+ 'start' => now()->addDays(14),
+ 'recurrence_type' => null,
+ ]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeTrue()
+ ->and($meetup->last_event_at)->toBeNull();
+});
+
+it('keeps a meetup active when a recurring event has no end date', function () {
+ $meetup = Meetup::factory()->create(['is_active' => false, 'last_event_at' => null]);
+ MeetupEvent::factory()->create([
+ 'meetup_id' => $meetup->id,
+ 'start' => now()->subYears(3),
+ 'recurrence_type' => RecurrenceType::Monthly,
+ 'recurrence_end_date' => null,
+ ]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeTrue();
+});
+
+it('marks a meetup as inactive when no events exist at all', function () {
+ $meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeFalse()
+ ->and($meetup->last_event_at)->toBeNull();
+});
+
+it('marks a meetup as inactive when a recurring event has ended more than a year ago', function () {
+ $meetup = Meetup::factory()->create(['is_active' => true, 'last_event_at' => now()]);
+ MeetupEvent::factory()->create([
+ 'meetup_id' => $meetup->id,
+ 'start' => now()->subYears(3),
+ 'recurrence_type' => RecurrenceType::Monthly,
+ 'recurrence_end_date' => now()->subYears(2),
+ ]);
+
+ $this->artisan('meetups:update-activity')->assertSuccessful();
+
+ $meetup->refresh();
+ expect($meetup->is_active)->toBeFalse();
+});