feat: add resources and view components
This commit is contained in:
+317
@@ -0,0 +1,317 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user