diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 23f4c25..68535e5 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -23,4 +23,9 @@ public function user(): BelongsTo { return $this->belongsTo(User::class); } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } } diff --git a/app/Services/Admin/Manage/AnnouncementService.php b/app/Services/Admin/Manage/AnnouncementService.php index 2233fa1..5fb6c49 100644 --- a/app/Services/Admin/Manage/AnnouncementService.php +++ b/app/Services/Admin/Manage/AnnouncementService.php @@ -3,10 +3,16 @@ namespace App\Services\Admin\Manage; use App\Models\Announcement; +use App\Models\Student; +use App\Services\NotificationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; class AnnouncementService { + public function __construct( + private readonly NotificationService $notificationService, + ) {} + public function paginated(int $perPage = 25, string $search = '', ?int $departmentId = null): LengthAwarePaginator { return Announcement::query() @@ -20,13 +26,27 @@ public function paginated(int $perPage = 25, string $search = '', ?int $departme public function create(array $data): Announcement { - return Announcement::create([ + $announcement = Announcement::create([ 'title' => $data['title'], 'content' => $data['content'], 'department_id' => $data['department_id'] ?? null, 'enrollment_year' => $data['enrollment_year'] ?? null, 'created_by' => auth()->id(), ]); + + $recipientUserIds = Student::query() + ->when($announcement->department_id, fn ($q, $departmentId) => $q->where('department_id', $departmentId)) + ->when($announcement->enrollment_year, fn ($q, $year) => $q->where('enrollment_year', $year)) + ->pluck('user_id'); + + $this->notificationService->sendToUsers( + $recipientUserIds, + $announcement->title, + $announcement->content, + $announcement->created_by, + ); + + return $announcement; } public function update(Announcement $announcement, array $data): Announcement diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index 43e6016..6d3e5e3 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -5,13 +5,40 @@ use App\Models\Notification; use App\Models\User; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; class NotificationService { + /** + * @param Collection|array $userIds + */ + public function sendToUsers(Collection|array $userIds, string $title, ?string $content = null, ?int $createdBy = null): void + { + $userIds = collect($userIds)->filter()->unique()->values(); + + if ($userIds->isEmpty()) { + return; + } + + $createdBy ??= auth()->id(); + $now = now(); + + Notification::insert($userIds->map(fn (int $userId) => [ + 'user_id' => $userId, + 'created_by' => $createdBy, + 'title' => $title, + 'content' => $content, + 'is_read' => false, + 'created_at' => $now, + 'updated_at' => $now, + ])->all()); + } + public function paginated(User $user, int $perPage = 15): LengthAwarePaginator { return Notification::query() ->where('user_id', $user->id) + ->with('creator.profile') ->orderByDesc('created_at') ->orderByDesc('id') ->paginate($perPage); diff --git a/database/migrations/2026_08_25_000004_create_notifications_table.php b/database/migrations/2026_08_25_000004_create_notifications_table.php index f381484..ff02838 100644 --- a/database/migrations/2026_08_25_000004_create_notifications_table.php +++ b/database/migrations/2026_08_25_000004_create_notifications_table.php @@ -11,6 +11,7 @@ public function up(): void Schema::create('notifications', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); $table->string('title', 150)->nullable(); $table->text('content')->nullable(); $table->boolean('is_read')->nullable()->default(false); diff --git a/database/migrations/2026_08_30_150000_add_created_by_to_notifications_table.php b/database/migrations/2026_08_30_150000_add_created_by_to_notifications_table.php new file mode 100644 index 0000000..9ff53ea --- /dev/null +++ b/database/migrations/2026_08_30_150000_add_created_by_to_notifications_table.php @@ -0,0 +1,22 @@ +foreignId('created_by')->nullable()->after('user_id')->constrained('users')->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('notifications', function (Blueprint $table) { + $table->dropConstrainedForeignId('created_by'); + }); + } +}; diff --git a/resources/js/components/notification-bell.tsx b/resources/js/components/notification-bell.tsx index f21f88b..62c1a26 100644 --- a/resources/js/components/notification-bell.tsx +++ b/resources/js/components/notification-bell.tsx @@ -6,6 +6,13 @@ import { useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { Button } from '@/components/ui/button'; import { Combobox, ComboboxContent } from '@/components/ui/combobox'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; import { destroy, markAllRead, clearAll, read } from '@/routes/notifications'; import type { Notification, NotificationPage } from '@/types/notification'; @@ -23,6 +30,7 @@ export function NotificationBell() { const [open, setOpen] = useState(false); const [clearAllOpen, setClearAllOpen] = useState(false); + const [detail, setDetail] = useState(null); const hasMore = notifications.current_page < notifications.last_page; const hasNotifications = notifications.data.length > 0; @@ -47,6 +55,14 @@ export function NotificationBell() { router.delete(destroy(notification.id).url, { preserveScroll: true }); } + function handleOpenDetail(notification: Notification) { + setDetail(notification); + + if (!notification.is_read) { + handleMarkRead(notification); + } + } + return ( <> @@ -70,9 +86,9 @@ export function NotificationBell() { -
+
Notifikasi
-
+
{hasNotifications ? (
{notifications.data.map((notification) => ( @@ -108,7 +124,13 @@ export function NotificationBell() { 'bg-primary/5', )} > -
+
-
+
{!notification.is_read && (