diff --git a/app/Http/Controllers/API/EventsController.php b/app/Http/Controllers/API/EventsController.php
new file mode 100644
index 0000000000..c9c62cf473
--- /dev/null
+++ b/app/Http/Controllers/API/EventsController.php
@@ -0,0 +1,125 @@
+filled('search')) {
+ $search = $request->input('search');
+ $query->where(function ($q) use ($search) {
+ $q->where('venue', 'like', "%{$search}%")
+ ->orWhere('location', 'like', "%{$search}%");
+ });
+ }
+
+ // Apply deleted filter
+ if ($request->filled('deleted')) {
+ $deletedFilter = $request->input('deleted');
+ if ($deletedFilter === 'only') {
+ $query->onlyTrashed();
+ } elseif ($deletedFilter === 'all') {
+ $query->withTrashed();
+ }
+ // Default shows only non-deleted
+ }
+
+ // Handle sorting
+ $sortBy = $request->input('sort_by', 'event_start_utc');
+ $sortDirection = $request->input('sort_direction', 'desc');
+
+ if (!in_array(strtolower($sortDirection), ['asc', 'desc'])) {
+ $sortDirection = 'desc';
+ }
+
+ $sortableColumns = [
+ 'event_start_utc' => 'event_start_utc',
+ 'venue' => 'venue',
+ 'location' => 'location',
+ 'created_at' => 'created_at',
+ ];
+
+ $sortColumn = $sortableColumns[$sortBy] ?? 'event_start_utc';
+ $query->orderBy($sortColumn, $sortDirection);
+
+ $perPage = min($request->input('per_page', 100), 500);
+ $events = $query->paginate($perPage);
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $events->getCollection(),
+ 'current_page' => $events->currentPage(),
+ 'last_page' => $events->lastPage(),
+ 'per_page' => $events->perPage(),
+ 'total' => $events->total(),
+ 'from' => $events->firstItem(),
+ 'to' => $events->lastItem(),
+ ]);
+ } catch (\Exception $e) {
+ Log::error('Error fetching events: ' . $e->getMessage());
+ return response()->json([
+ 'success' => false,
+ 'message' => 'Failed to fetch events',
+ ], 500);
+ }
+ }
+
+ public static function performSingleAction(int $event_id, string $action): JsonResponse
+ {
+ try {
+ $event = Party::withTrashed()->findOrFail($event_id);
+ $result = self::performAction($event, $action);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => $result['message'],
+ 'event' => $result,
+ ]);
+ } catch (\Exception $e) {
+ Log::error('Error performing event action: ' . $e->getMessage());
+
+ $statusCode = $e->getCode() === 409 ? 409 : 500;
+
+ return response()->json([
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ], $statusCode);
+ }
+ }
+
+ private static function performAction(Party $event, string $action): array
+ {
+ switch ($action) {
+ case 'restore':
+ // Check if parent group is soft-deleted
+ $group = Group::withTrashed()->find($event->group);
+ if ($group && $group->trashed()) {
+ throw new \Exception("Cannot restore event: the parent group '{$group->name}' is deleted. Restore the group first.", 409);
+ }
+
+ $event->restore();
+ break;
+
+ default:
+ throw new \Exception("Invalid action: {$action}");
+ }
+
+ return [
+ 'id' => $event->idevents,
+ 'venue' => $event->venue,
+ 'message' => "Event has been {$action}d successfully.",
+ ];
+ }
+}
diff --git a/app/Http/Controllers/API/GroupsController.php b/app/Http/Controllers/API/GroupsController.php
index 2f62ee5179..05a59131f0 100644
--- a/app/Http/Controllers/API/GroupsController.php
+++ b/app/Http/Controllers/API/GroupsController.php
@@ -29,6 +29,17 @@ public function index(Request $request): JsonResponse
});
}
+ // Apply deleted filter
+ if ($request->filled('deleted')) {
+ $deletedFilter = $request->input('deleted');
+ if ($deletedFilter === 'only') {
+ $query->onlyTrashed();
+ } elseif ($deletedFilter === 'all') {
+ $query->withTrashed();
+ }
+ // Default (no filter or 'active') shows only non-deleted
+ }
+
// Handle sorting
$sortBy = $request->input('sort_by', 'name');
$sortDirection = $request->input('sort_direction', 'asc');
@@ -85,7 +96,7 @@ public function index(Request $request): JsonResponse
public static function performSingleAction(int $group_id, string $action): JsonResponse
{
try {
- $group = Group::findOrFail($group_id);
+ $group = Group::withTrashed()->findOrFail($group_id);
$result = self::performAction($group, $action);
@@ -98,8 +109,8 @@ public static function performSingleAction(int $group_id, string $action): JsonR
Log::error('Error performing action: ' . $e->getMessage());
return response()->json([
'success' => false,
- 'message' => 'Failed to perform action'
- ], 500);
+ 'message' => $e->getMessage(),
+ ], 422);
}
}
@@ -107,7 +118,7 @@ public static function performBulkActions(Request $request, string $action): Jso
{
try {
$group_ids = $request->input('group_ids');
- $groups = Group::whereIn('idgroups', $group_ids)->get();
+ $groups = Group::withTrashed()->whereIn('idgroups', $group_ids)->get();
$failedGroups = [];
@@ -161,13 +172,27 @@ private static function performAction(Group $group, string $action): array
break;
case 'delete':
- $groupName = $group->name;
- if (!$group->canDelete()) {
- throw new \Exception("Group '{$groupName}' cannot be deleted because it has events with devices.");
- }
+ // Soft-delete all group events (preserving devices and volunteer data)
+ \App\Models\Party::where('events.group', $group->idgroups)->each(function ($event) {
+ $event->delete();
+ });
$group->delete();
break;
+ case 'restore':
+ if (!$group->trashed()) {
+ break; // Skip non-deleted groups silently (relevant for bulk actions)
+ }
+ $group->restore();
+ // Also restore soft-deleted events for this group
+ \App\Models\Party::withTrashed()
+ ->where('events.group', $group->idgroups)
+ ->whereNotNull('events.deleted_at')
+ ->each(function ($event) {
+ $event->restore();
+ });
+ break;
+
default:
throw new \Exception("Invalid action: {$action}");
}
@@ -428,6 +453,7 @@ private function transformGroup($group): array
'country_display' => $countryDisplay,
'approved' => (bool) $group->approved,
'archived_at' => $group->archived_at,
+ 'deleted_at' => $group->deleted_at,
'created_at' => $group->created_at,
'networks' => $group->networks,
'group_tags' => $group->group_tags,
diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php
index ac3b7d1f38..fe69022718 100644
--- a/app/Http/Controllers/GroupController.php
+++ b/app/Http/Controllers/GroupController.php
@@ -440,34 +440,23 @@ public function delete($id): RedirectResponse
{
$group = Group::where('idgroups', $id)->first();
- $name = $group->name;
-
- if (Auth::user()->hasRole('Administrator') && $group->canDelete()) {
- // We know we can delete the group; if it has any past events they must be empty, so delete all
- // events (including future).
- $allEvents = Party::withTrashed()->where('events.group', $id)->get();
+ if (!$group) {
+ return redirect('/group')->with('warning', 'Group not found.');
+ }
- foreach ($allEvents as $event) {
- // Delete any users - these are not cascaded in the DB.
- $users = EventsUsers::where('event', $event->idevents)->get();
+ $name = $group->name;
- foreach ($users as $user) {
- // Need to force delete to get rid of the row and avoid constraint violations.
- $user->forceDelete();
- }
+ if (Auth::user()->hasRole('Administrator')) {
+ // Soft-delete all group events (preserving devices and volunteer data).
+ Party::where('events.group', $id)->each(function ($event) {
+ $event->delete();
+ });
- $event->forceDelete();
- }
+ $group->delete();
- $r = $group->delete($id);
-
- if (! $r) {
- return redirect('/user/forbidden');
- } else {
- return redirect('/group')->with('success', __('groups.delete_succeeded', [
- 'name' => $name,
- ]));
- }
+ return redirect('/group')->with('success', __('groups.delete_succeeded', [
+ 'name' => $name,
+ ]));
} else {
return redirect('/user/forbidden');
}
diff --git a/app/Http/Controllers/PartyController.php b/app/Http/Controllers/PartyController.php
index 400482fee4..3dce9bd2bd 100644
--- a/app/Http/Controllers/PartyController.php
+++ b/app/Http/Controllers/PartyController.php
@@ -834,16 +834,6 @@ public function deleteEvent($id): RedirectResponse
return redirect()->back()->with('warning', __('events.delete_permission'));
}
- $event = Party::findOrFail($id);
-
- Audits::where('auditable_type', \App\Models\Party::class)->where('auditable_id', $id)->delete();
- Device::where('event', $id)->delete();
-
- // We have to do a loop to avoid the gotcha where bulk delete operations don't invoke observers.
- foreach (EventsUsers::where('event', $id)->get() as $delete) {
- $delete->delete();
- };
-
$event->delete();
event(new EventDeleted($event));
diff --git a/app/Models/Group.php b/app/Models/Group.php
index 71caf81ecf..b949d3d555 100644
--- a/app/Models/Group.php
+++ b/app/Models/Group.php
@@ -13,12 +13,14 @@
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Log;
use OwenIt\Auditing\Contracts\Auditable;
+use Illuminate\Database\Eloquent\SoftDeletes;
class Group extends Model implements Auditable
{
use HasFactory;
use \OwenIt\Auditing\Auditable;
+ use SoftDeletes;
protected $table = 'groups';
protected $primaryKey = 'idgroups';
@@ -129,6 +131,7 @@ public function findAll()
FROM `'.$this->table.'` AS `g`
LEFT JOIN `users_groups` AS `ug` ON `g`.`idgroups` = `ug`.`group`
LEFT JOIN `users` AS `u` ON `ug`.`user` = `u`.`id`
+ WHERE `g`.`deleted_at` IS NULL
GROUP BY `g`.`idgroups`
ORDER BY `g`.`name` ASC');
} catch (\Illuminate\Database\QueryException $e) {
@@ -158,6 +161,7 @@ public function findList()
) AS `xi`
ON `xi`.`reference` = `g`.`idgroups`
+ WHERE `g`.`deleted_at` IS NULL
GROUP BY `g`.`idgroups`
ORDER BY `g`.`name` ASC');
@@ -182,6 +186,7 @@ public function ofThisUser($id)
ON `xi`.`reference` = `g`.`idgroups`
WHERE `ug`.`user` = :id
+ AND `g`.`deleted_at` IS NULL
ORDER BY `g`.`name` ASC', ['id' => $id]);
}
diff --git a/database/migrations/2026_02_18_000000_add_soft_deletes_to_groups_table.php b/database/migrations/2026_02_18_000000_add_soft_deletes_to_groups_table.php
new file mode 100644
index 0000000000..34b07f54a0
--- /dev/null
+++ b/database/migrations/2026_02_18_000000_add_soft_deletes_to_groups_table.php
@@ -0,0 +1,22 @@
+softDeletes()->index();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('groups', function (Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+};
diff --git a/resources/js/components/StatsImpact.vue b/resources/js/components/StatsImpact.vue
index 8bfdccfbbd..c15c06d453 100644
--- a/resources/js/components/StatsImpact.vue
+++ b/resources/js/components/StatsImpact.vue
@@ -8,7 +8,7 @@
Unarchive
-
@@ -59,6 +63,10 @@ export default {
type: Boolean,
default: false,
},
+ deletedFilter: {
+ type: String,
+ default: "active",
+ },
},
methods: {
diff --git a/resources/js/components/admin/GroupsManagement.vue b/resources/js/components/admin/GroupsManagement.vue
index 2cacd69fce..8f7dc75559 100644
--- a/resources/js/components/admin/GroupsManagement.vue
+++ b/resources/js/components/admin/GroupsManagement.vue
@@ -11,7 +11,7 @@
-
+
@@ -27,6 +27,13 @@
{{ searchResultsText }}
+
+
+
@@ -39,13 +46,13 @@
-
+
+ :sort-field="sortField" :sort-direction="sortDirection" :deleted-filter="deletedFilter" @action="handleAction"
+ @select="handleGroupSelect" @select-all="handleSelectAll" @page-change="handlePageChange"
+ @page-size-change="handlePageSizeChange" @sort-change="handleSortChange" />
@@ -214,6 +222,10 @@ export default {
type: String,
default: "asc",
},
+ deletedFilter: {
+ type: String,
+ default: "active",
+ },
},
computed: {
diff --git a/resources/views/events/view.blade.php b/resources/views/events/view.blade.php
index c1d2e9ed59..82d8d9d8e6 100644
--- a/resources/views/events/view.blade.php
+++ b/resources/views/events/view.blade.php
@@ -64,7 +64,7 @@
idevents);
- $can_delete_event = App\Helpers\Fixometer::userHasDeletePartyPermission($event->idevents) && $event->canDelete();
+ $can_delete_event = App\Helpers\Fixometer::userHasDeletePartyPermission($event->idevents);
$is_admin = Auth::check() && App\Helpers\Fixometer::hasRole(Auth::user(), 'Administrator');
$is_attending = is_object($is_attending) && $is_attending->status == 1;
diff --git a/resources/views/group/view.blade.php b/resources/views/group/view.blade.php
index 6e7360d7e6..32e57b4c05 100644
--- a/resources/views/group/view.blade.php
+++ b/resources/views/group/view.blade.php
@@ -45,7 +45,7 @@
$can_edit_group = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup || $is_host_of_group;
$can_demote = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup;
$can_see_delete = App\Helpers\Fixometer::hasRole($user, 'Administrator');
- $can_perform_delete = $can_see_delete && $group->canDelete();
+ $can_perform_delete = $can_see_delete;
$can_perform_archive = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup;
$showCalendar = Auth::check() && (($group && $group->isVolunteer()) || App\Helpers\Fixometer::hasRole(Auth::user(), 'Administrator'));
diff --git a/routes/api.php b/routes/api.php
index 83f6d2de20..12ee7df2bf 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -162,5 +162,10 @@
Route::post('bulk/{action}', [App\Http\Controllers\API\GroupsController::class, 'performBulkActions']);
Route::post('{id}/{action}', [App\Http\Controllers\API\GroupsController::class, 'performSingleAction']);
});
+
+ Route::prefix('events')->group(function () {
+ Route::get('/', [App\Http\Controllers\API\EventsController::class, 'index']);
+ Route::post('{id}/{action}', [App\Http\Controllers\API\EventsController::class, 'performSingleAction']);
+ });
});
});
\ No newline at end of file
diff --git a/tests/Feature/Groups/GroupDeleteTest.php b/tests/Feature/Groups/GroupDeleteTest.php
index 40e4c302c2..5b710b7a7f 100644
--- a/tests/Feature/Groups/GroupDeleteTest.php
+++ b/tests/Feature/Groups/GroupDeleteTest.php
@@ -33,6 +33,9 @@ public function testDelete(): void
$this->assertStringContainsString(__('groups.delete_succeeded', [
'name' => $name,
]), $response->getContent());
+
+ // Verify soft-delete
+ $this->assertSoftDeleted('groups', ['idgroups' => $id]);
}
public function testCanDeleteWithEmptyEvent(): void
@@ -55,13 +58,15 @@ public function testCanDeleteWithEmptyEvent(): void
]), $response->getContent());
}
- public function testCantDeleteWithDevice(): void
+ public function testCanDeleteWithDevice(): void
{
$this->loginAsTestUser(Role::ADMINISTRATOR);
$id = $this->createGroup();
$this->assertNotNull($id);
+ $group = Group::where('idgroups', $id)->first();
+ $name = $group->name;
- // Add an event with a device - should not be able to delete.
+ // Add an event with a device - soft-delete should work regardless.
$idevents = $this->createEvent($id, 'yesterday');
$iddevices = $this->createDevice($idevents, 'misc');
@@ -69,13 +74,18 @@ public function testCantDeleteWithDevice(): void
$this->actingAs($user);
$this->followingRedirects();
$response = $this->get("/group/delete/$id");
- $this->assertStringContainsString('Sorry, but you do not have the permissions to perform that action.', $response->getContent());
+ $this->assertStringContainsString(__('groups.delete_succeeded', [
+ 'name' => $name,
+ ]), $response->getContent());
- // Delete the event - still shouldn't be deletable as a device exists.
- Party::find($idevents)->delete();
+ // Group should be soft-deleted
+ $this->assertSoftDeleted('groups', ['idgroups' => $id]);
- $response = $this->get("/group/delete/$id");
- $response->assertRedirect('/user/forbidden');
+ // Event should also be soft-deleted
+ $this->assertSoftDeleted('events', ['idevents' => $idevents]);
+
+ // Device should still exist
+ $this->assertGreaterThan(0, \App\Models\Device::where('event', $idevents)->count());
}
public function testCanDeleteWithDeletedEvent(): void
@@ -87,7 +97,7 @@ public function testCanDeleteWithDeletedEvent(): void
// Create a past event
$event = Party::factory()->moderated()->create([
'event_start_utc' => '2000-01-01T10:15:05+05:00',
- 'event_end_utc' => '2000-01-0113:45:05+05:00',
+ 'event_end_utc' => '2000-01-01 13:45:05+05:00',
'group' => $id,
]);
diff --git a/tests/Feature/Groups/GroupSoftDeleteTest.php b/tests/Feature/Groups/GroupSoftDeleteTest.php
new file mode 100644
index 0000000000..7606a067e1
--- /dev/null
+++ b/tests/Feature/Groups/GroupSoftDeleteTest.php
@@ -0,0 +1,211 @@
+loginAsTestUser(Role::ADMINISTRATOR);
+ $groupId = $this->createGroup();
+ $eventId = $this->createEvent($groupId, 'yesterday');
+ $deviceId = $this->createDevice($eventId, 'misc');
+
+ $deviceCountBefore = Device::where('event', $eventId)->count();
+ $this->assertGreaterThan(0, $deviceCountBefore);
+
+ // Delete the event via the web route
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+ $response = $this->post("/party/delete/{$eventId}");
+
+ // Event should be soft-deleted
+ $this->assertSoftDeleted('events', ['idevents' => $eventId]);
+
+ // Devices should still exist
+ $deviceCountAfter = Device::where('event', $eventId)->count();
+ $this->assertEquals($deviceCountBefore, $deviceCountAfter);
+
+ // Event should be hidden from default queries
+ $this->assertNull(Party::find($eventId));
+
+ // But accessible with withTrashed
+ $this->assertNotNull(Party::withTrashed()->find($eventId));
+ }
+
+ public function testGroupSoftDeleteCascadesToEvents(): void
+ {
+ $this->loginAsTestUser(Role::ADMINISTRATOR);
+ $groupId = $this->createGroup();
+ $eventId1 = $this->createEvent($groupId, 'yesterday');
+ $eventId2 = $this->createEvent($groupId, 'tomorrow');
+ $deviceId = $this->createDevice($eventId1, 'misc');
+
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ // Delete the group
+ $response = $this->get("/group/delete/{$groupId}");
+ $response->assertRedirect();
+
+ // Group should be soft-deleted
+ $this->assertSoftDeleted('groups', ['idgroups' => $groupId]);
+
+ // All events should be soft-deleted
+ $this->assertSoftDeleted('events', ['idevents' => $eventId1]);
+ $this->assertSoftDeleted('events', ['idevents' => $eventId2]);
+
+ // Devices should be unchanged
+ $this->assertGreaterThan(0, Device::where('event', $eventId1)->count());
+ }
+
+ public function testAdminApiGroupSoftDelete(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $group = Group::factory()->create();
+ $event = Party::factory()->create(['group' => $group->idgroups]);
+ $deviceId = $this->createDevice($event->idevents, 'misc');
+
+ // Soft-delete via admin API - should work even with devices
+ $response = $this->post("/api/v2/admin/groups/{$group->idgroups}/delete");
+ $response->assertJson(['success' => true]);
+
+ // Group should be soft-deleted
+ $this->assertSoftDeleted('groups', ['idgroups' => $group->idgroups]);
+
+ // Event should be soft-deleted
+ $this->assertSoftDeleted('events', ['idevents' => $event->idevents]);
+
+ // Devices should still exist
+ $this->assertGreaterThan(0, Device::where('event', $event->idevents)->count());
+ }
+
+ public function testAdminApiGroupRestore(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $group = Group::factory()->create();
+ $event1 = Party::factory()->create(['group' => $group->idgroups]);
+ $event2 = Party::factory()->create(['group' => $group->idgroups]);
+
+ // Soft-delete the group
+ $this->post("/api/v2/admin/groups/{$group->idgroups}/delete");
+ $this->assertSoftDeleted('groups', ['idgroups' => $group->idgroups]);
+
+ // Restore the group
+ $response = $this->post("/api/v2/admin/groups/{$group->idgroups}/restore");
+ $response->assertJson(['success' => true]);
+
+ // Group should be restored
+ $group->refresh();
+ $this->assertNull($group->deleted_at);
+
+ // Events should be restored
+ $event1->refresh();
+ $event2->refresh();
+ $this->assertNull($event1->deleted_at);
+ $this->assertNull($event2->deleted_at);
+ }
+
+ public function testAdminApiEventRestoreWithDeletedGroupReturns409(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $group = Group::factory()->create();
+ $event = Party::factory()->create(['group' => $group->idgroups]);
+
+ // Soft-delete the group (cascades to events)
+ $this->post("/api/v2/admin/groups/{$group->idgroups}/delete");
+
+ // Try to restore the event while group is deleted
+ $response = $this->post("/api/v2/admin/events/{$event->idevents}/restore");
+ $response->assertStatus(409);
+ $response->assertJson(['success' => false]);
+ }
+
+ public function testAdminApiEventRestoreAfterGroupRestore(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $group = Group::factory()->create();
+ $event = Party::factory()->create(['group' => $group->idgroups]);
+
+ // Soft-delete the group (cascades to events)
+ $this->post("/api/v2/admin/groups/{$group->idgroups}/delete");
+
+ // Restore the group first
+ $this->post("/api/v2/admin/groups/{$group->idgroups}/restore");
+
+ // Now event should already be restored (group restore cascades)
+ $event->refresh();
+ $this->assertNull($event->deleted_at);
+ }
+
+ public function testDeletedGroupsHiddenFromDefaultQueries(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $group = Group::factory()->create();
+ $groupId = $group->idgroups;
+
+ // Visible before deletion
+ $this->assertNotNull(Group::find($groupId));
+
+ // Soft-delete
+ $this->post("/api/v2/admin/groups/{$groupId}/delete");
+
+ // Hidden from default queries
+ $this->assertNull(Group::find($groupId));
+
+ // Visible with withTrashed
+ $this->assertNotNull(Group::withTrashed()->find($groupId));
+ }
+
+ public function testAdminApiDeletedFilter(): void
+ {
+ $admin = User::factory()->administrator()->create();
+ $this->actingAs($admin);
+
+ $activeGroup = Group::factory()->create(['name' => 'Active Group']);
+ $deletedGroup = Group::factory()->create(['name' => 'Deleted Group']);
+
+ // Soft-delete one group
+ $this->post("/api/v2/admin/groups/{$deletedGroup->idgroups}/delete");
+
+ // Default (active) - should only show active group
+ $response = $this->get('/api/v2/admin/groups');
+ $response->assertJson(['success' => true]);
+ $data = $response->json('data');
+ $names = collect($data)->pluck('name')->toArray();
+ $this->assertContains('Active Group', $names);
+ $this->assertNotContains('Deleted Group', $names);
+
+ // Only deleted - should only show deleted group
+ $response = $this->get('/api/v2/admin/groups?deleted=only');
+ $data = $response->json('data');
+ $names = collect($data)->pluck('name')->toArray();
+ $this->assertNotContains('Active Group', $names);
+ $this->assertContains('Deleted Group', $names);
+
+ // All - should show both
+ $response = $this->get('/api/v2/admin/groups?deleted=all');
+ $data = $response->json('data');
+ $names = collect($data)->pluck('name')->toArray();
+ $this->assertContains('Active Group', $names);
+ $this->assertContains('Deleted Group', $names);
+ }
+}