TIFNJK_E41221880/resources/views/pages/notifikasi/notifikasi.blade.php

746 lines
27 KiB
PHP

@extends('pages.index')
@section('admin_content')
<style>
/* Modern UI for Notifications */
.notif-wrapper {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.notif-header-section {
background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
color: white;
padding: 30px 40px;
border-radius: 16px;
margin-bottom: 30px;
box-shadow: 0 10px 25px rgba(13, 110, 253, 0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.notif-header-section h1 {
font-weight: 700;
font-size: 32px;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.notif-header-section p {
opacity: 0.9;
font-size: 16px;
margin: 0;
}
.btn-mark-all {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.4);
padding: 10px 24px;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s;
backdrop-filter: blur(5px);
}
.btn-mark-all:hover {
background: white;
color: #0d6efd;
border-color: white;
transform: translateY(-2px);
}
.filters-container {
display: flex;
gap: 15px;
margin-bottom: 30px;
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.search-box {
flex: 1;
position: relative;
}
.search-box i {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
}
.search-box input {
width: 100%;
padding: 12px 15px 12px 45px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
transition: all 0.3s;
}
.search-box input:focus {
background: white;
border-color: #0d6efd;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
outline: none;
}
.filter-select {
min-width: 200px;
}
.filter-select select {
width: 100%;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
cursor: pointer;
}
.notif-card {
background: white;
border-radius: 16px;
padding: 24px;
margin-bottom: 16px;
display: flex;
gap: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border: 1px solid #f3f4f6;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
cursor: pointer;
}
.notif-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
border-color: #e5e7eb;
}
.notif-card.unread::before {
content: '';
position: absolute;
left: 12px;
top: 12px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #0d6efd;
box-shadow: 0 0 0 4px rgba(13, 110, 253, 0.15);
z-index: 2;
}
.notif-icon-wrap {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
background: #eef2ff;
color: #0d6efd;
}
.notif-actions {
position: absolute;
right: 20px;
top: 20px;
opacity: 0;
transition: all 0.3s ease;
}
.notif-card:hover .notif-actions {
opacity: 1;
}
.btn-delete-notif {
background: #fee2e2;
color: #ef4444;
border: none;
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.btn-delete-notif:hover {
background: #ef4444;
color: white;
transform: scale(1.1);
}
.notif-content {
flex: 1;
}
.notif-header-text {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.notif-title {
font-weight: 700;
font-size: 18px;
color: #111827;
margin: 0;
}
.notif-time {
font-size: 13px;
color: #6b7280;
background: #f3f4f6;
padding: 4px 10px;
border-radius: 20px;
font-weight: 500;
}
.notif-message {
color: #4b5563;
font-size: 15px;
line-height: 1.5;
margin-bottom: 12px;
}
.notif-meta {
display: flex;
gap: 15px;
font-size: 13px;
color: #6b7280;
}
.notif-meta span {
display: flex;
align-items: center;
gap: 6px;
background: #f9fafb;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid #e5e7eb;
}
.notif-meta i {
color: #9ca3af;
}
/* Role badges */
.role-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.4px;
line-height: 1.4;
}
.role-badge.role-admin { background:#eef2ff; color:#4338ca; }
.role-badge.role-teacher { background:#dcfce7; color:#15803d; }
.role-badge.role-student { background:#fef3c7; color:#b45309; }
.role-badge.role-system { background:#f3f4f6; color:#4b5563; }
.notif-sender {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.empty-state-icon {
font-size: 64px;
color: #e5e7eb;
margin-bottom: 20px;
}
.empty-state h3 {
color: #374151;
font-weight: 600;
margin-bottom: 8px;
}
.empty-state p {
color: #6b7280;
}
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 30px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
}
.pagination-btn {
padding: 10px 14px;
border: 1px solid #e5e7eb;
background: white;
color: #365CF5;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.pagination-btn:hover:not(:disabled) {
background: #365CF5;
color: white;
border-color: #365CF5;
}
.pagination-btn.active {
background: #365CF5;
color: white;
border-color: #365CF5;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
color: #6b7280;
font-size: 14px;
font-weight: 600;
}
</style>
<div class="notif-wrapper mt-4">
<!-- Header Section -->
<div class="notif-header-section">
<div>
<h1><i class="fas fa-bell me-2"></i> Notifikasi</h1>
<p>Kelola semua pemberitahuan Anda di sini</p>
</div>
<div>
<button class="btn btn-mark-all" id="markAllRead">
<i class="fas fa-check-double me-2"></i> Tandai Semua Dibaca
</button>
</div>
</div>
<!-- Filter & Sort Section -->
<div class="filters-container">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchNotification" placeholder="Cari notifikasi...">
</div>
<div class="filter-select">
<select id="filterNotification">
<option value="">Semua Notifikasi</option>
<option value="unread">Belum Dibaca</option>
<option value="read">Sudah Dibaca</option>
</select>
</div>
</div>
<!-- Notification List -->
<div id="notificationContainer">
@if(isset($notifications) && $notifications->count() > 0)
<div id="notificationList">
@foreach($notifications as $notification)
@php
$sender = $notification->sender;
$senderName = $sender ? $sender->display_name : 'Sistem';
$senderRole = $sender ? $sender->role : 'system';
$senderLabel = $sender ? $sender->role_label : 'Sistem';
$senderEmail = $sender->email ?? null;
$className = $notification->class?->name ?? '-';
@endphp
<div class="notif-card unread" onclick="openNotificationModal(this, 'notifDetailModal-{{ $notification->id }}')"
data-id="{{ $notification->id }}"
data-title="{{ $notification->title }}"
data-message="{{ $notification->body }}"
data-time="{{ $notification->created_at?->format('d M Y H:i') }}"
data-sender="{{ $senderName }}"
data-class="{{ $className }}">
<div class="notif-actions">
<form action="{{ route('notifikasi.destroy', $notification->id) }}" method="POST" style="margin: 0;" onsubmit="return confirm('Apakah Anda yakin ingin menghapus notifikasi ini?');">
@csrf
@method('DELETE')
<button type="submit" class="btn-delete-notif" title="Hapus Notifikasi" onclick="event.stopPropagation();">
<i class="fas fa-trash-alt"></i>
</button>
</form>
</div>
<div class="notif-icon-wrap {{ $notification->type ?? '' }}">
<i class="fas fa-envelope"></i>
</div>
<div class="notif-content">
<div class="notif-header-text">
<h3 class="notif-title">{{ $notification->title }}</h3>
<span class="notif-time">{{ $notification->created_at?->format('d M Y H:i') }}</span>
</div>
<p class="notif-message">{{ Str::limit($notification->body, 100) }}</p>
<div class="notif-meta">
<span class="notif-sender">
<i class="fas fa-user"></i>
<strong>{{ $senderName }}</strong>
<span class="role-badge role-{{ $senderRole }}">
@switch($senderRole)
@case('admin') <i class="fas fa-user-shield"></i> @break
@case('teacher') <i class="fas fa-chalkboard-teacher"></i> @break
@case('student') <i class="fas fa-user-graduate"></i> @break
@default <i class="fas fa-robot"></i>
@endswitch
{{ $senderLabel }}
</span>
</span>
@if($notification->class)
<span>
<i class="fas fa-chalkboard"></i>
{{ $className }}
</span>
@endif
</div>
</div>
</div>
<div class="modal fade" id="notifDetailModal-{{ $notification->id }}" tabindex="-1" aria-labelledby="notifDetailModalLabel-{{ $notification->id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: none; overflow: hidden;">
<div class="modal-header" style="background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%); color: white; border: none; padding: 20px 24px;">
<h5 class="modal-title" id="notifDetailModalLabel-{{ $notification->id }}" style="font-weight: 600;">
<i class="fas fa-info-circle me-2"></i> Detail Notifikasi
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="padding: 24px;">
<div class="d-flex align-items-center mb-3">
<div class="notif-icon-wrap me-3" style="width: 40px; height: 40px; font-size: 16px;">
<i class="fas fa-envelope"></i>
</div>
<div>
<h4 style="margin: 0; font-size: 18px; font-weight: 700; color: #111827;">{{ $notification->title }}</h4>
<small style="color: #6b7280;">{{ $notification->created_at?->format('d M Y H:i') }}</small>
</div>
</div>
<hr>
<div style="background: #f9fafb; padding: 16px; border-radius: 8px; margin-bottom: 20px;">
<p style="margin: 0; color: #4b5563; font-size: 15px; line-height: 1.6;">{{ $notification->body }}</p>
</div>
<div class="d-flex gap-3">
<div style="flex: 1; background: white; border: 1px solid #e5e7eb; padding: 12px; border-radius: 8px;">
<span style="display: block; font-size: 12px; color: #6b7280; margin-bottom: 4px;">Pengirim</span>
<div style="font-weight: 600; font-size: 14px; color: #111827; display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<i class="fas fa-user text-primary me-1"></i>
<span>{{ $senderName }}</span>
<span class="role-badge role-{{ $senderRole }}">
@switch($senderRole)
@case('admin') <i class="fas fa-user-shield"></i> @break
@case('teacher') <i class="fas fa-chalkboard-teacher"></i> @break
@case('student') <i class="fas fa-user-graduate"></i> @break
@default <i class="fas fa-robot"></i>
@endswitch
{{ $senderLabel }}
</span>
</div>
@if($senderEmail)
<div style="margin-top:6px; font-size:12px; color:#6b7280;">
<i class="fas fa-envelope me-1"></i>{{ $senderEmail }}
</div>
@endif
</div>
<div style="flex: 1; background: white; border: 1px solid #e5e7eb; padding: 12px; border-radius: 8px;">
<span style="display: block; font-size: 12px; color: #6b7280; margin-bottom: 4px;">Kelas</span>
<div style="font-weight: 600; font-size: 14px; color: #111827;">
<i class="fas fa-chalkboard text-primary me-1"></i> {{ $className }}
</div>
</div>
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid #f3f4f6; padding: 16px 24px;">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" style="border-radius: 8px; padding: 8px 20px;">Tutup</button>
</div>
</div>
</div>
</div>
@endforeach
</div>
@else
<!-- Empty State -->
<div class="empty-state">
<div class="empty-state-icon">
<i class="fas fa-inbox"></i>
</div>
<h3>Tidak ada notifikasi</h3>
<p>Belum ada notifikasi baru untuk Anda saat ini.</p>
</div>
@endif
<!-- Pagination Container -->
<div id="paginationContainer" class="pagination-container" style="display: none;">
<button class="pagination-btn" id="prevBtn" onclick="previousPage()">&laquo; Sebelumnya</button>
<span class="pagination-info" id="pageInfo"></span>
<button class="pagination-btn" id="nextBtn" onclick="nextPage()">Berikutnya &raquo;</button>
</div>
</div>
</div>
<script>
// kode untuk localStorage untuk menyimpan status baca notifikasi
const READ_NOTIFICATION_STORAGE_KEY = 'sas_read_notifications';
const ITEMS_PER_PAGE = 10;
let currentPage = 1;
let filteredCards = [];
function getReadNotificationIds() {
try {
const savedValue = localStorage.getItem(READ_NOTIFICATION_STORAGE_KEY);
const parsedValue = savedValue ? JSON.parse(savedValue) : [];
return Array.isArray(parsedValue) ? parsedValue : [];
} catch (error) {
return [];
}
}
function saveReadNotificationId(notificationId) {
if (!notificationId) {
return;
}
const readIds = new Set(getReadNotificationIds());
readIds.add(String(notificationId));
localStorage.setItem(READ_NOTIFICATION_STORAGE_KEY, JSON.stringify(Array.from(readIds)));
}
function displayPage(page) {
const startIndex = (page - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const totalPages = Math.ceil(filteredCards.length / ITEMS_PER_PAGE);
// Sembunyikan SEMUA card dulu (bukan hanya yg di filteredCards),
// supaya kartu yg tidak lolos filter benar-benar hilang.
const allCards = document.querySelectorAll('#notificationList .notif-card');
allCards.forEach(card => {
card.style.display = 'none';
});
// Tampilkan kartu untuk halaman saat ini
for (let i = startIndex; i < endIndex && i < filteredCards.length; i++) {
filteredCards[i].style.display = '';
}
// Update pagination info
const paginationContainer = document.getElementById('paginationContainer');
const pageInfo = document.getElementById('pageInfo');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
if (filteredCards.length === 0 || totalPages <= 1) {
paginationContainer.style.display = 'none';
} else {
paginationContainer.style.display = 'flex';
pageInfo.textContent = `Halaman ${page} dari ${totalPages}`;
prevBtn.disabled = page === 1;
nextBtn.disabled = page === totalPages;
}
currentPage = page;
}
function previousPage() {
if (currentPage > 1) {
displayPage(currentPage - 1);
}
}
function nextPage() {
const totalPages = Math.ceil(filteredCards.length / ITEMS_PER_PAGE);
if (currentPage < totalPages) {
displayPage(currentPage + 1);
}
}
// Tandai semua notifikasi sebagai sudah dibaca
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('searchNotification');
const filterSelect = document.getElementById('filterNotification');
const notificationList = document.getElementById('notificationList');
const notificationContainer = document.getElementById('notificationContainer');
const markAllReadBtn = document.getElementById('markAllRead');
if (!searchInput || !notificationList) {
return;
}
const cards = Array.from(notificationList.querySelectorAll('.notif-card'));
const readIds = new Set(getReadNotificationIds());
cards.forEach(function(card) {
const cardId = card.getAttribute('data-id');
if (cardId && readIds.has(String(cardId))) {
card.classList.remove('unread');
}
});
// Event listener untuk tombol Tandai Semua Dibaca
if (markAllReadBtn) {
markAllReadBtn.addEventListener('click', function(e) {
e.preventDefault();
cards.forEach(function(card) {
const cardId = card.getAttribute('data-id');
card.classList.remove('unread');
saveReadNotificationId(cardId);
});
if (typeof window.applyNotificationFilters === 'function') {
window.applyNotificationFilters();
}
});
}
const searchEmptyState = document.createElement('div');
searchEmptyState.className = 'empty-state search-empty-state';
searchEmptyState.style.display = 'none';
searchEmptyState.innerHTML = `
<div class="empty-state-icon">
<i class="fas fa-search"></i>
</div>
<h3>Notifikasi tidak ditemukan</h3>
<p>Coba gunakan kata kunci lain.</p>
`;
notificationContainer.appendChild(searchEmptyState);
// Pesan empty state kontekstual berdasarkan filter aktif.
const EMPTY_STATES = {
search: {
icon: 'fa-search',
title: 'Notifikasi tidak ditemukan',
desc: 'Coba gunakan kata kunci lain atau ubah filter.'
},
unread: {
icon: 'fa-check-circle',
title: 'Tidak ada notifikasi belum dibaca',
desc: 'Semua notifikasi sudah Anda baca. Bagus!'
},
read: {
icon: 'fa-inbox',
title: 'Belum ada notifikasi yang dibaca',
desc: 'Buka notifikasi untuk menandainya sebagai dibaca.'
},
all: {
icon: 'fa-bell-slash',
title: 'Tidak ada notifikasi',
desc: 'Belum ada pemberitahuan untuk Anda saat ini.'
}
};
function renderEmptyState(state) {
const cfg = EMPTY_STATES[state] || EMPTY_STATES.search;
searchEmptyState.innerHTML = `
<div class="empty-state-icon">
<i class="fas ${cfg.icon}"></i>
</div>
<h3>${cfg.title}</h3>
<p>${cfg.desc}</p>
`;
}
function getCardSearchText(card) {
const title = card.getAttribute('data-title') || '';
const message = card.getAttribute('data-message') || '';
const sender = card.getAttribute('data-sender') || '';
const className = card.getAttribute('data-class') || '';
const time = card.getAttribute('data-time') || '';
return `${title} ${message} ${sender} ${className} ${time}`.toLowerCase();
}
function applyNotificationFilters() {
const searchKeyword = searchInput.value.trim().toLowerCase();
const statusFilter = filterSelect ? filterSelect.value : '';
filteredCards = [];
cards.forEach(function(card) {
const matchesSearch = !searchKeyword || getCardSearchText(card).includes(searchKeyword);
const isUnread = card.classList.contains('unread');
const matchesStatus = !statusFilter
|| (statusFilter === 'unread' && isUnread)
|| (statusFilter === 'read' && !isUnread);
const shouldShow = matchesSearch && matchesStatus;
if (shouldShow) {
filteredCards.push(card);
}
});
if (filteredCards.length === 0) {
// Tentukan state empty yg paling tepat
let state = 'all';
if (searchKeyword) {
state = 'search';
} else if (statusFilter === 'unread') {
state = 'unread';
} else if (statusFilter === 'read') {
state = 'read';
}
renderEmptyState(state);
searchEmptyState.style.display = '';
} else {
searchEmptyState.style.display = 'none';
}
// Reset to page 1 when filter changes
currentPage = 1;
displayPage(1);
}
searchInput.addEventListener('input', applyNotificationFilters);
if (filterSelect) {
filterSelect.addEventListener('change', applyNotificationFilters);
}
window.applyNotificationFilters = applyNotificationFilters;
applyNotificationFilters();
});
// Fungsi untuk menampilkan detail notifikasi di modal
function openNotificationModal(card, modalId) {
// Tandai sebagai sudah dibaca secara visual jika perlu
card.classList.remove('unread');
saveReadNotificationId(card.getAttribute('data-id'));
if (typeof window.applyNotificationFilters === 'function') {
window.applyNotificationFilters();
}
// Tampilkan modal
var notifModal = new bootstrap.Modal(document.getElementById(modalId));
notifModal.show();
}
</script>
@endsection