318 lines
15 KiB
Plaintext
318 lines
15 KiB
Plaintext
<x-app-layout>
|
|
@push('styles')
|
|
<style>
|
|
.ck-editor__editable {
|
|
min-height: 200px;
|
|
}
|
|
</style>
|
|
@endpush
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="row align-items-center mb-4">
|
|
<div class="col">
|
|
<h4 class="fw-bold mb-1">{{ __('Notification Center') }}</h4>
|
|
<p class="text-secondary small mb-0">{{ __('Manage system notifications and activity feed.') }}</p>
|
|
</div>
|
|
@hasanyrole('Developer|Administrator')
|
|
<div class="col-auto">
|
|
<div class="btn-group me-2">
|
|
<button type="button" class="btn btn-outline-dark btn-sm rounded-pill px-3" id="page-mark-all-read">
|
|
<i class="bi bi-check-all me-1"></i>{{ __('Mark all read') }}
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm rounded-pill px-3 ms-2" id="page-clear-read">
|
|
<i class="bi bi-trash3 me-1"></i>{{ __('Clear read') }}
|
|
</button>
|
|
</div>
|
|
<button type="button" class="btn btn-dark btn-sm rounded-pill px-3" data-bs-toggle="modal" data-bs-target="#sendNotificationModal">
|
|
<i class="bi bi-plus-lg me-1"></i>{{ __('Send') }}
|
|
</button>
|
|
</div>
|
|
@else
|
|
<div class="col-auto">
|
|
<button type="button" class="btn btn-outline-dark btn-sm rounded-pill px-3" id="page-mark-all-read">
|
|
<i class="bi bi-check-all me-1"></i>{{ __('Mark all read') }}
|
|
</button>
|
|
</div>
|
|
@endhasanyrole
|
|
</div>
|
|
|
|
<div class="row justify-content-center">
|
|
<div class="col-12 col-xl-10 col-lg-11">
|
|
{{-- Standardized Notification Feed --}}
|
|
<div id="notification-feed">
|
|
<div class="card adminuiux-card border-0 shadow-sm rounded-3">
|
|
<div class="card-body text-center py-5">
|
|
<div class="spinner-border text-primary spinner-border-sm" role="status"></div>
|
|
<p class="text-secondary small mt-2">{{ __('Loading...') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Pagination --}}
|
|
<div id="feed-pagination" class="d-flex justify-content-center mt-5"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- MODAL --}}
|
|
<div class="modal fade" id="sendNotificationModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content rounded-3 border-0 shadow-lg">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{{ __('Send Notification') }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<form method="POST" action="{{ rout('notification-center.store') }}" id="manualNotificationForm" class="ajax-form" data-reset="true">
|
|
@csrf
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">{{ __('Title') }} <span class="text-danger">*</span></label>
|
|
<input type="text" name="title" class="form-control" placeholder="{{ __('Notification Title') }}" required maxlength="100">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold">{{ __('Message') }} <span class="text-danger">*</span></label>
|
|
<textarea id="notificationMessage" name="message" class="form-control" rows="4"></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label fw-semibold">{{ __('Recipient') }} <span class="text-danger">*</span></label>
|
|
<select name="recipient" class="form-select">
|
|
<option value="all">{{ __('All Users (Public)') }}</option>
|
|
@foreach($roles as $roleName)
|
|
<option value="{{ $roleName }}" {{ $roleName === 'Developer' ? 'selected' : '' }}>{{ ucfirst($roleName) }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label fw-semibold">{{ __('Type') }} <span class="text-danger">*</span></label>
|
|
<select name="type" class="form-select">
|
|
<option value="info">{{ __('Information') }}</option>
|
|
<option value="warning">{{ __('Warning') }}</option>
|
|
<option value="system">{{ __('System Alert') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-dark rounded-pill px-4" data-bs-dismiss="modal">
|
|
{{ __('Close') }}
|
|
</button>
|
|
<button type="submit" class="btn btn-dark rounded-pill px-4">
|
|
<i class="bi bi-send me-1"></i> {{ __('Send Notification') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.ckeditor.com/ckeditor5/41.1.0/classic/ckeditor.js" crossorigin="anonymous"></script>
|
|
<script>
|
|
$(document).ready(function() {
|
|
let notificationEditor;
|
|
let currentPage = 1;
|
|
|
|
const editorEl = document.querySelector('#notificationMessage');
|
|
if (editorEl && typeof ClassicEditor !== 'undefined') {
|
|
ClassicEditor
|
|
.create(editorEl, {
|
|
ckfinder: { uploadUrl: "{{ route('editor.upload') }}?_token={{ csrf_token() }}" }
|
|
})
|
|
.then(newEditor => {
|
|
notificationEditor = newEditor;
|
|
editorEl.ckeditorInstance = newEditor;
|
|
})
|
|
.catch(error => console.error('CKEditor Error:', error));
|
|
}
|
|
|
|
// Load feed — this MUST always run
|
|
loadFeed(1);
|
|
|
|
function loadFeed(page = 1) {
|
|
currentPage = page;
|
|
const $container = $('#notification-feed');
|
|
|
|
$.ajax({
|
|
url: "{{ route('notification-center.index') }}",
|
|
data: {
|
|
start: (page - 1) * 10,
|
|
length: 10,
|
|
draw: 1
|
|
},
|
|
success: function(response) {
|
|
console.log('Notification Response:', response);
|
|
if (!response.data || response.data.length === 0) {
|
|
$container.html(`
|
|
<div class="text-center py-5 opacity-50">
|
|
<i class="bi bi-inbox h1 display-1"></i>
|
|
<p class="small mt-2">{{ __('Inbox is empty') }}</p>
|
|
</div>
|
|
`);
|
|
} else {
|
|
let html = '';
|
|
response.data.forEach(n => {
|
|
try {
|
|
html += window.renderNotificationCard(n);
|
|
} catch (e) {
|
|
console.error('Render Error:', e, n);
|
|
}
|
|
});
|
|
$container.html(html);
|
|
}
|
|
renderPagination(response.recordsTotal, page);
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX Error:', status, error, xhr.responseText);
|
|
let message = 'Failed to load notifications.';
|
|
try {
|
|
if (xhr.responseJSON && xhr.responseJSON.message) {
|
|
message = xhr.responseJSON.message;
|
|
}
|
|
} catch(e) {}
|
|
|
|
$container.html(`
|
|
<div class="card adminuiux-card border-0 shadow-sm rounded-3">
|
|
<div class="card-body text-center py-5 text-danger">
|
|
<i class="bi bi-exclamation-circle h3"></i>
|
|
<p class="small mt-2">${message}</p>
|
|
<p class="text-muted smallest">Status: ${xhr.status}</p>
|
|
<button class="btn btn-sm btn-outline-danger mt-2" onclick="location.reload()">Refresh Page</button>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
});
|
|
}
|
|
|
|
window.reloadFeed = () => loadFeed(currentPage);
|
|
|
|
function renderPagination(total, current) {
|
|
const pages = Math.ceil(total / 10);
|
|
if (pages <= 1) { $('#feed-pagination').html(''); return; }
|
|
|
|
let html = '<ul class="pagination pagination-sm m-0">';
|
|
|
|
// Previous button
|
|
html += `<li class="page-item ${current === 1 ? 'disabled' : ''}"><a class="page-link rounded-circle mx-1 border-0 shadow-sm" href="#" data-page="${current - 1}">«</a></li>`;
|
|
|
|
// Page numbers with sliding window (current page +/- 2)
|
|
const delta = 2;
|
|
const left = current - delta;
|
|
const right = current + delta;
|
|
const range = [];
|
|
|
|
for (let i = 1; i <= pages; i++) {
|
|
if (i === 1 || i === pages || (i >= left && i <= right)) {
|
|
range.push(i);
|
|
}
|
|
}
|
|
|
|
let last = 0;
|
|
for (let i of range) {
|
|
if (last) {
|
|
if (i - last === 2) {
|
|
html += `<li class="page-item"><a class="page-link rounded-circle mx-1 border-0 shadow-sm" href="#" data-page="${last + 1}">${last + 1}</a></li>`;
|
|
} else if (i - last !== 1) {
|
|
html += `<li class="page-item disabled"><span class="page-link rounded-circle mx-1 border-0 shadow-sm">...</span></li>`;
|
|
}
|
|
}
|
|
html += `<li class="page-item ${i === current ? 'active' : ''}"><a class="page-link rounded-circle mx-1 border-0 shadow-sm" href="#" data-page="${i}">${i}</a></li>`;
|
|
last = i;
|
|
}
|
|
|
|
// Next button
|
|
html += `<li class="page-item ${current === pages ? 'disabled' : ''}"><a class="page-link rounded-circle mx-1 border-0 shadow-sm" href="#" data-page="${current + 1}">»</a></li>`;
|
|
|
|
html += '</ul>';
|
|
$('#feed-pagination').html(html);
|
|
}
|
|
|
|
$(document).on('click', '.page-link', function(e) {
|
|
e.preventDefault();
|
|
loadFeed($(this).data('page'));
|
|
});
|
|
|
|
$(document).on('click', '.btn-delete', function() {
|
|
const url = $(this).data('url');
|
|
StandardSwal.fire({
|
|
title: 'Delete this notification?',
|
|
text: 'This notification will be permanently removed from your history.',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
customClass: {
|
|
confirmButton: 'btn-pill-danger',
|
|
cancelButton: 'btn-pill-cancel'
|
|
},
|
|
confirmButtonText: 'Yes, Delete',
|
|
cancelButtonText: "Cancel",
|
|
}).then(result => {
|
|
if (result.isConfirmed) {
|
|
$.ajax({
|
|
url: url,
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
|
|
success: (res) => {
|
|
window.reloadNotificationUI();
|
|
window.showNotificationToast('success', res.message || 'Notification deleted');
|
|
},
|
|
error: (xhr) => window.showNotificationToast('error', 'Delete failed')
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
$('#page-mark-all-read').on('click', function() {
|
|
$.ajax({
|
|
url: "{{ route('notification-center.read-all') }}",
|
|
method: 'PATCH',
|
|
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
|
|
success: (res) => {
|
|
window.reloadNotificationUI();
|
|
window.showNotificationToast('success', res.message || 'All marked as read');
|
|
},
|
|
error: () => window.showNotificationToast('error', 'Process failed')
|
|
});
|
|
});
|
|
|
|
$('#page-clear-read').on('click', function() {
|
|
StandardSwal.fire({
|
|
title: "Clear all read notifications?",
|
|
text: "All read updates will be permanently purged from your feed.",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
customClass: {
|
|
confirmButton: 'btn-pill-danger',
|
|
cancelButton: 'btn-pill-cancel'
|
|
},
|
|
confirmButtonText: "Yes, Clear",
|
|
cancelButtonText: "Cancel",
|
|
}).then(result => {
|
|
if (result.isConfirmed) {
|
|
$.ajax({
|
|
url: "{{ route('notification-center.clear-read') }}",
|
|
method: 'DELETE',
|
|
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
|
|
success: (res) => {
|
|
window.reloadNotificationUI();
|
|
window.showNotificationToast('success', res.message || 'Read notifications cleared');
|
|
},
|
|
error: () => window.showNotificationToast('error', 'Clear failed')
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Listen for standard AJAX success to reload feed
|
|
$('#manualNotificationForm').on('ajaxForm:success', function() {
|
|
window.reloadNotificationUI();
|
|
if (notificationEditor) notificationEditor.setData('');
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|
|
</x-app-layout>
|