feat: delete subscribers from page subscriber list
Adds DELETE route, form request authorization, admin UI with confirm, Dutch strings, and feature tests. Made-with: Cursor
This commit is contained in:
@@ -5,9 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\DestroySubscriberRequest;
|
||||
use App\Http\Requests\Admin\IndexSubscriberRequest;
|
||||
use App\Http\Requests\Admin\QueueMailwizzSyncRequest;
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Models\Subscriber;
|
||||
use App\Services\DispatchUnsyncedMailwizzSyncJobsService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\View\View;
|
||||
@@ -32,6 +34,15 @@ class SubscriberController extends Controller
|
||||
return view('admin.subscribers.index', compact('page', 'subscribers', 'unsyncedMailwizzCount'));
|
||||
}
|
||||
|
||||
public function destroy(DestroySubscriberRequest $request, PreregistrationPage $page, Subscriber $subscriber): RedirectResponse
|
||||
{
|
||||
$subscriber->delete();
|
||||
|
||||
return redirect()
|
||||
->route('admin.pages.subscribers.index', $page)
|
||||
->with('status', __('Subscriber removed.'));
|
||||
}
|
||||
|
||||
public function queueMailwizzSync(
|
||||
QueueMailwizzSyncRequest $request,
|
||||
PreregistrationPage $page,
|
||||
|
||||
36
app/Http/Requests/Admin/DestroySubscriberRequest.php
Normal file
36
app/Http/Requests/Admin/DestroySubscriberRequest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Models\Subscriber;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DestroySubscriberRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$page = $this->route('page');
|
||||
$subscriber = $this->route('subscriber');
|
||||
if (! $page instanceof PreregistrationPage || ! $subscriber instanceof Subscriber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subscriber->preregistration_page_id !== $page->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->user()?->can('update', $page) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, ValidationRule|string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,9 @@
|
||||
"Thank you for registering!": "Bedankt voor je registratie!",
|
||||
"You are already registered for this event.": "Je bent al geregistreerd voor dit evenement.",
|
||||
"Please enter a valid email address.": "Voer een geldig e-mailadres in.",
|
||||
"Please enter a valid phone number (8–15 digits).": "Voer een geldig telefoonnummer in (8 tot 15 cijfers)."
|
||||
"Please enter a valid phone number (8–15 digits).": "Voer een geldig telefoonnummer in (8 tot 15 cijfers).",
|
||||
"Subscriber removed.": "Abonnee verwijderd.",
|
||||
"Delete this subscriber? This cannot be undone.": "Deze abonnee verwijderen? Dit kan niet ongedaan worden gemaakt.",
|
||||
"Remove": "Verwijderen",
|
||||
"Actions": "Acties"
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
@endif
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Registered at') }}</th>
|
||||
<th class="px-4 py-3 font-semibold text-slate-700">{{ __('Mailwizz') }}</th>
|
||||
<th class="w-px whitespace-nowrap px-4 py-3 font-semibold text-slate-700">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
@@ -77,10 +78,29 @@
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-4 py-3 text-right">
|
||||
@can('update', $page)
|
||||
<form
|
||||
method="post"
|
||||
action="{{ route('admin.pages.subscribers.destroy', [$page, $subscriber]) }}"
|
||||
class="inline"
|
||||
onsubmit="return confirm(@js(__('Delete this subscriber? This cannot be undone.')));"
|
||||
>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg border border-red-200 bg-white px-2.5 py-1 text-xs font-semibold text-red-700 hover:bg-red-50"
|
||||
>
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 6 : 5 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
<td colspan="{{ $page->isPhoneFieldEnabledForSubscribers() ? 7 : 6 }}" class="px-4 py-12 text-center text-slate-500">
|
||||
{{ __('No subscribers match your criteria.') }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -33,6 +33,7 @@ Route::middleware(['auth', 'verified'])->prefix('admin')->name('admin.')->group(
|
||||
|
||||
// Subscribers (nested under pages) — export before index so the path is unambiguous
|
||||
Route::get('pages/{page}/subscribers/export', [SubscriberController::class, 'export'])->name('pages.subscribers.export');
|
||||
Route::delete('pages/{page}/subscribers/{subscriber}', [SubscriberController::class, 'destroy'])->name('pages.subscribers.destroy');
|
||||
Route::post('pages/{page}/subscribers/queue-mailwizz-sync', [SubscriberController::class, 'queueMailwizzSync'])->name('pages.subscribers.queue-mailwizz-sync');
|
||||
Route::get('pages/{page}/subscribers', [SubscriberController::class, 'index'])->name('pages.subscribers.index');
|
||||
|
||||
|
||||
131
tests/Feature/DestroySubscriberTest.php
Normal file
131
tests/Feature/DestroySubscriberTest.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\PreregistrationPage;
|
||||
use App\Models\Subscriber;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DestroySubscriberTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_page_owner_can_delete_subscriber_on_that_page(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'user']);
|
||||
$page = PreregistrationPage::query()->create([
|
||||
'slug' => (string) Str::uuid(),
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Fest',
|
||||
'heading' => 'Fest',
|
||||
'intro_text' => null,
|
||||
'thank_you_message' => null,
|
||||
'expired_message' => null,
|
||||
'ticketshop_url' => null,
|
||||
'start_date' => now()->subDay(),
|
||||
'end_date' => now()->addMonth(),
|
||||
'phone_enabled' => false,
|
||||
'background_image' => null,
|
||||
'logo_image' => null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$subscriber = Subscriber::query()->create([
|
||||
'preregistration_page_id' => $page->id,
|
||||
'first_name' => 'Ada',
|
||||
'last_name' => 'Lovelace',
|
||||
'email' => 'ada@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||
|
||||
$response->assertRedirect(route('admin.pages.subscribers.index', $page));
|
||||
$response->assertSessionHas('status');
|
||||
$this->assertDatabaseMissing('subscribers', ['id' => $subscriber->id]);
|
||||
}
|
||||
|
||||
public function test_other_user_cannot_delete_subscriber(): void
|
||||
{
|
||||
$owner = User::factory()->create(['role' => 'user']);
|
||||
$intruder = User::factory()->create(['role' => 'user']);
|
||||
$page = PreregistrationPage::query()->create([
|
||||
'slug' => (string) Str::uuid(),
|
||||
'user_id' => $owner->id,
|
||||
'title' => 'Fest',
|
||||
'heading' => 'Fest',
|
||||
'intro_text' => null,
|
||||
'thank_you_message' => null,
|
||||
'expired_message' => null,
|
||||
'ticketshop_url' => null,
|
||||
'start_date' => now()->subDay(),
|
||||
'end_date' => now()->addMonth(),
|
||||
'phone_enabled' => false,
|
||||
'background_image' => null,
|
||||
'logo_image' => null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$subscriber = Subscriber::query()->create([
|
||||
'preregistration_page_id' => $page->id,
|
||||
'first_name' => 'A',
|
||||
'last_name' => 'B',
|
||||
'email' => 'x@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($intruder)->delete(route('admin.pages.subscribers.destroy', [$page, $subscriber]));
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
||||
}
|
||||
|
||||
public function test_cannot_delete_subscriber_using_wrong_page_in_url(): void
|
||||
{
|
||||
$user = User::factory()->create(['role' => 'user']);
|
||||
$pageA = PreregistrationPage::query()->create([
|
||||
'slug' => (string) Str::uuid(),
|
||||
'user_id' => $user->id,
|
||||
'title' => 'A',
|
||||
'heading' => 'A',
|
||||
'intro_text' => null,
|
||||
'thank_you_message' => null,
|
||||
'expired_message' => null,
|
||||
'ticketshop_url' => null,
|
||||
'start_date' => now()->subDay(),
|
||||
'end_date' => now()->addMonth(),
|
||||
'phone_enabled' => false,
|
||||
'background_image' => null,
|
||||
'logo_image' => null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$pageB = PreregistrationPage::query()->create([
|
||||
'slug' => (string) Str::uuid(),
|
||||
'user_id' => $user->id,
|
||||
'title' => 'B',
|
||||
'heading' => 'B',
|
||||
'intro_text' => null,
|
||||
'thank_you_message' => null,
|
||||
'expired_message' => null,
|
||||
'ticketshop_url' => null,
|
||||
'start_date' => now()->subDay(),
|
||||
'end_date' => now()->addMonth(),
|
||||
'phone_enabled' => false,
|
||||
'background_image' => null,
|
||||
'logo_image' => null,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$subscriber = Subscriber::query()->create([
|
||||
'preregistration_page_id' => $pageB->id,
|
||||
'first_name' => 'A',
|
||||
'last_name' => 'B',
|
||||
'email' => 'y@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->delete(route('admin.pages.subscribers.destroy', [$pageA, $subscriber]));
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas('subscribers', ['id' => $subscriber->id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user