diff --git a/app/Http/Controllers/Admin/EmailController.php b/app/Http/Controllers/Admin/EmailController.php index 497c4c0..85a213d 100644 --- a/app/Http/Controllers/Admin/EmailController.php +++ b/app/Http/Controllers/Admin/EmailController.php @@ -19,22 +19,28 @@ class EmailController extends Controller */ public function create(): Response { - $recipientCount = User::where('receive_emails', true) + $allSubscribersCount = User::where('receive_emails', true) ->whereNotNull('email') ->count(); + $studentsCount = User::where('receive_emails', true) + ->whereNotNull('email') + ->doesntHave('roles') + ->count(); + return Inertia::render('admin/EmailSend', [ - 'recipientCount' => $recipientCount, + 'recipientCount' => $allSubscribersCount, + 'studentsCount' => $studentsCount, ]); } /** - * Dispatch emails to either a single user or all subscribed users in bulk. + * Dispatch emails to either a single user or all/student subscribed users in bulk. */ public function store(Request $request): RedirectResponse { $validated = $request->validate([ - 'recipient_type' => ['required', 'in:all,single'], + 'recipient_type' => ['required', 'in:all,students,single'], 'recipient_email' => ['required_if:recipient_type,single', 'nullable', 'email', 'max:255'], 'subject' => ['required', 'string', 'max:255'], 'body' => ['required', 'string'], @@ -70,16 +76,20 @@ public function store(Request $request): RedirectResponse ->with('success', "Email successfully queued for {$recipientEmail}."); } - // Bulk broadcast to all subscribed users + // Bulk broadcast query $usersQuery = User::where('receive_emails', true) ->whereNotNull('email'); + if ($validated['recipient_type'] === 'students') { + $usersQuery->doesntHave('roles'); + } + $totalRecipients = $usersQuery->count(); if ($totalRecipients === 0) { return redirect() ->route('admin.emails.create') - ->with('error', 'No subscribed recipients found.'); + ->with('error', 'No subscribed recipients found for the selected target.'); } $usersQuery->chunkById(100, function ($users) use ($subject, $body, $imageUrl) { @@ -95,8 +105,10 @@ public function store(Request $request): RedirectResponse } }); + $targetLabel = $validated['recipient_type'] === 'students' ? 'students (non-staff)' : 'all subscribed'; + return redirect() ->route('admin.emails.create') - ->with('success', "Email broadcast successfully queued for {$totalRecipients} recipients."); + ->with('success', "Email broadcast successfully queued for {$totalRecipients} {$targetLabel} recipients."); } } diff --git a/resources/js/pages/admin/EmailSend.vue b/resources/js/pages/admin/EmailSend.vue index 3ae94d1..e161e89 100644 --- a/resources/js/pages/admin/EmailSend.vue +++ b/resources/js/pages/admin/EmailSend.vue @@ -6,6 +6,7 @@ import { Send, Users, User, + GraduationCap, AlertCircle, AtSign, Eye, @@ -22,6 +23,10 @@ const props = defineProps({ type: Number, default: 0, }, + studentsCount: { + type: Number, + default: 0, + }, }); const page = usePage(); @@ -34,7 +39,7 @@ const imagePreview = ref(null); const fileInput = ref(null); const form = useForm({ - recipient_type: 'all' as 'all' | 'single', + recipient_type: 'all' as 'all' | 'students' | 'single', recipient_email: '', subject: '', body: '', @@ -168,7 +173,7 @@ const submitForm = () => { Recipient Target -
+
+ + + + @@ -235,7 +274,7 @@ const submitForm = () => {

- Send to an individual email address + Specific email address

@@ -716,14 +755,18 @@ const submitForm = () => { {{ form.recipient_type === 'single' ? 'Confirm Single Email' - : 'Confirm Email Broadcast' + : form.recipient_type === 'students' + ? 'Confirm Students Broadcast' + : 'Confirm Email Broadcast' }}

{{ form.recipient_type === 'single' ? `Direct email to ${form.recipient_email}` - : `Queue broadcast to ${props.recipientCount} subscribed users` + : form.recipient_type === 'students' + ? `Queue broadcast to ${props.studentsCount} students (non-staff)` + : `Queue broadcast to ${props.recipientCount} subscribed users` }}

@@ -737,6 +780,12 @@ const submitForm = () => { {{ form.recipient_email }}? + + Are you sure you want to send this broadcast? The emails + will be dispatched to + {{ props.studentsCount }} students + (public non-staff users) who have enabled email updates. + Are you sure you want to send this broadcast? The emails will be dispatched to all diff --git a/tests/Feature/AdminEmailTest.php b/tests/Feature/AdminEmailTest.php index 92f050e..9d441e8 100644 --- a/tests/Feature/AdminEmailTest.php +++ b/tests/Feature/AdminEmailTest.php @@ -141,3 +141,47 @@ ! empty($mail->imageUrl); }); }); + +test('admin can queue broadcast emails specifically to students non-staff users', function () { + Mail::fake(); + + $admin = User::factory()->create(['email' => 'admin@example.com']); + $admin->assignRole('admin'); + + $editor = User::factory()->create(['email' => 'editor@example.com', 'receive_emails' => true]); + $editorRole = Role::findOrCreate('editor', 'web'); + $editor->assignRole($editorRole); + + $student1 = User::factory()->create(['email' => 'student1@example.com', 'receive_emails' => true]); + $student2 = User::factory()->create(['email' => 'student2@example.com', 'receive_emails' => true]); + $unsubStudent = User::factory()->create(['email' => 'unsub_student@example.com', 'receive_emails' => false]); + + $response = $this->actingAs($admin)->post(route('admin.emails.store'), [ + 'recipient_type' => 'students', + 'subject' => 'Student Community Update', + 'body' => '

Special notice for all students.

', + ]); + + $response->assertRedirect(route('admin.emails.create')); + $response->assertSessionHas('success'); + + // Students with receive_emails=true should receive the email + Mail::assertQueued(BulkAnnouncementMail::class, function ($mail) use ($student1) { + return $mail->hasTo($student1->email) && + $mail->mailSubject === 'Student Community Update'; + }); + + Mail::assertQueued(BulkAnnouncementMail::class, function ($mail) use ($student2) { + return $mail->hasTo($student2->email); + }); + + // Staff/role-assigned users should NOT receive it + Mail::assertNotQueued(BulkAnnouncementMail::class, function ($mail) use ($editor) { + return $mail->hasTo($editor->email); + }); + + // Unsubscribed students should NOT receive it + Mail::assertNotQueued(BulkAnnouncementMail::class, function ($mail) use ($unsubStudent) { + return $mail->hasTo($unsubStudent->email); + }); +});