diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php
new file mode 100644
index 0000000..e79978a
--- /dev/null
+++ b/app/Http/Controllers/NotificationController.php
@@ -0,0 +1,47 @@
+service->markAllAsRead($request->user());
+
+ return back();
+ }
+
+ public function destroyAll(Request $request): RedirectResponse
+ {
+ $this->service->deleteAll($request->user());
+
+ return back();
+ }
+
+ public function markRead(Request $request, Notification $notification): RedirectResponse
+ {
+ abort_unless($notification->user_id === $request->user()->id, 403);
+
+ $this->service->markAsRead($notification);
+
+ return back();
+ }
+
+ public function destroy(Request $request, Notification $notification): RedirectResponse
+ {
+ abort_unless($notification->user_id === $request->user()->id, 403);
+
+ $this->service->delete($notification);
+
+ return back();
+ }
+}
diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php
index f4cc770..9118ed5 100644
--- a/app/Http/Middleware/HandleInertiaRequests.php
+++ b/app/Http/Middleware/HandleInertiaRequests.php
@@ -2,7 +2,9 @@
namespace App\Http\Middleware;
+use App\Services\NotificationService;
use Illuminate\Http\Request;
+use Inertia\Inertia;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
@@ -42,6 +44,12 @@ public function share(Request $request): array
'user' => $request->user(),
],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
+ 'unreadNotificationsCount' => fn () => $request->user()
+ ? app(NotificationService::class)->unreadCount($request->user())
+ : 0,
+ 'notifications' => Inertia::merge(fn () => $request->user()
+ ? app(NotificationService::class)->paginated($request->user())
+ : null)->append('data', 'id'),
];
}
}
diff --git a/app/Models/Notification.php b/app/Models/Notification.php
new file mode 100644
index 0000000..23f4c25
--- /dev/null
+++ b/app/Models/Notification.php
@@ -0,0 +1,26 @@
+ 'boolean',
+ ];
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index f95b5ea..9d023c5 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -49,4 +50,9 @@ public function lecturer(): HasOne
{
return $this->hasOne(Lecturer::class);
}
+
+ public function notifications(): HasMany
+ {
+ return $this->hasMany(Notification::class);
+ }
}
diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php
new file mode 100644
index 0000000..43e6016
--- /dev/null
+++ b/app/Services/NotificationService.php
@@ -0,0 +1,50 @@
+where('user_id', $user->id)
+ ->orderByDesc('created_at')
+ ->orderByDesc('id')
+ ->paginate($perPage);
+ }
+
+ public function unreadCount(User $user): int
+ {
+ return Notification::query()
+ ->where('user_id', $user->id)
+ ->where('is_read', false)
+ ->count();
+ }
+
+ public function markAsRead(Notification $notification): void
+ {
+ $notification->update(['is_read' => true]);
+ }
+
+ public function markAllAsRead(User $user): void
+ {
+ Notification::query()
+ ->where('user_id', $user->id)
+ ->where('is_read', false)
+ ->update(['is_read' => true]);
+ }
+
+ public function delete(Notification $notification): bool
+ {
+ return $notification->delete();
+ }
+
+ public function deleteAll(User $user): void
+ {
+ Notification::query()->where('user_id', $user->id)->delete();
+ }
+}
diff --git a/database/migrations/2026_08_25_000004_create_notifications_table.php b/database/migrations/2026_08_25_000004_create_notifications_table.php
new file mode 100644
index 0000000..f381484
--- /dev/null
+++ b/database/migrations/2026_08_25_000004_create_notifications_table.php
@@ -0,0 +1,25 @@
+id();
+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+ $table->string('title', 150)->nullable();
+ $table->text('content')->nullable();
+ $table->boolean('is_read')->nullable()->default(false);
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('notifications');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index e15ef5b..68da324 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -30,6 +30,7 @@ public function run(): void
TuitionPaymentSeeder::class,
AnnouncementSeeder::class,
LetterRequestSeeder::class,
+ NotificationSeeder::class,
]);
}
}
diff --git a/database/seeders/NotificationSeeder.php b/database/seeders/NotificationSeeder.php
new file mode 100644
index 0000000..0da9df3
--- /dev/null
+++ b/database/seeders/NotificationSeeder.php
@@ -0,0 +1,31 @@
+ 1,
+ 'title' => 'Tugas baru diunggah',
+ 'content' => 'Tugas "Membuat Landing Page" telah tersedia di kelas SI-5A',
+ 'is_read' => false,
+ 'created_at' => '2026-08-05 09:05:00',
+ 'updated_at' => '2026-08-05 09:05:00',
+ ],
+ [
+ 'user_id' => 2,
+ 'title' => 'Pembayaran UKT diterima sebagian',
+ 'content' => 'Pembayaran cicilan tahap 1 sebesar Rp1.500.000 telah tercatat',
+ 'is_read' => true,
+ 'created_at' => '2026-08-04 11:05:00',
+ 'updated_at' => '2026-08-04 12:00:00',
+ ],
+ ]);
+ }
+}
diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx
index ad4ac74..f663319 100644
--- a/resources/js/components/app-sidebar-header.tsx
+++ b/resources/js/components/app-sidebar-header.tsx
@@ -1,5 +1,6 @@
import { Breadcrumbs } from '@/components/breadcrumbs';
import { NavUser } from '@/components/nav-user';
+import { NotificationBell } from '@/components/notification-bell';
import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
@@ -14,7 +15,8 @@ export function AppSidebarHeader({
+ {notification.title ?? '-'} +
+ {notification.content && ( ++ {notification.content} +
+ )} ++ {format( + new Date( + notification.created_at, + ), + 'd MMM yyyy, HH:mm', + )} +
+