feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
<x-app-layout>
|
||||
@push('styles')
|
||||
<style>
|
||||
.ck-editor__editable {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<div class="container-fluid pb-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 d-flex flex-wrap gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-outline-dark btn-sm rounded-pill px-3 d-inline-flex align-items-center gap-1"
|
||||
data-bs-toggle="modal" data-bs-target="#notifDocsModal">
|
||||
<i class="bi bi-book"></i>{{ __('Documentation') }}
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<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 d-flex gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-outline-dark btn-sm rounded-pill px-3 d-inline-flex align-items-center gap-1"
|
||||
data-bs-toggle="modal" data-bs-target="#notifDocsModal">
|
||||
<i class="bi bi-book"></i>{{ __('Documentation') }}
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{{-- ====================================================================
|
||||
DOCUMENTATION (MODAL)
|
||||
==================================================================== --}}
|
||||
<div class="modal fade" id="notifDocsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-fullscreen-lg-down modal-dialog-scrollable modal-dialog-centered">
|
||||
<div class="modal-content rounded-4 border-0 shadow-lg overflow-hidden">
|
||||
<div class="modal-header p-0 border-0" style="background:#0f172a;">
|
||||
<div class="d-flex w-100 align-items-center justify-content-between p-4 p-lg-5 text-white">
|
||||
<div>
|
||||
<div class="extra-small opacity-50 fw-bold mb-2" style="letter-spacing:1.5px;">REFERENCE GUIDE</div>
|
||||
<h3 class="fw-black mb-2" style="letter-spacing:-1px;">{{ __('Notification Center Guide') }}</h3>
|
||||
<p class="mb-0 opacity-75" style="font-size:.9rem;line-height:1.7;max-width:720px;">
|
||||
{{ __('How notifications flow through this system — from sending to bell icon to acknowledged. Who sees what, when, and why.') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<i class="bi bi-bell-fill display-3 opacity-25 d-none d-md-inline"></i>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div class="p-4 p-lg-5">
|
||||
|
||||
{{-- LIFECYCLE DIAGRAM --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-arrow-right-circle text-primary me-1"></i>{{ __('Notification lifecycle') }}
|
||||
</h6>
|
||||
<div class="p-4 rounded-4 mb-5" style="background:#f8fafc;border:1px solid #e2e8f0;">
|
||||
<div class="row text-center align-items-center g-3">
|
||||
<div class="col-md-2">
|
||||
<div class="p-3 rounded-4 bg-white shadow-sm h-100">
|
||||
<i class="bi bi-send-fill text-primary fs-3"></i>
|
||||
<div class="fw-bold small mt-2">{{ __('Sent') }}</div>
|
||||
<div class="extra-small text-muted">Admin clicks Send / system triggers</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 d-none d-md-block"><i class="bi bi-arrow-right fs-4 text-muted"></i></div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-3 rounded-4 bg-white shadow-sm h-100">
|
||||
<i class="bi bi-database-fill text-info fs-3"></i>
|
||||
<div class="fw-bold small mt-2">{{ __('Persisted') }}</div>
|
||||
<div class="extra-small text-muted">Row written to <code>notifications</code> table</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 d-none d-md-block"><i class="bi bi-arrow-right fs-4 text-muted"></i></div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-3 rounded-4 bg-white shadow-sm h-100">
|
||||
<i class="bi bi-broadcast text-warning fs-3"></i>
|
||||
<div class="fw-bold small mt-2">{{ __('Broadcast') }}</div>
|
||||
<div class="extra-small text-muted">Real-time via Reverb WebSocket</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 d-none d-md-block"><i class="bi bi-arrow-right fs-4 text-muted"></i></div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-3 rounded-4 bg-white shadow-sm h-100">
|
||||
<i class="bi bi-bell-fill text-success fs-3"></i>
|
||||
<div class="fw-bold small mt-2">{{ __('Delivered') }}</div>
|
||||
<div class="extra-small text-muted">Bell icon + feed updates</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small text-muted text-center mb-0 mt-4" style="line-height:1.7;">
|
||||
{{ __('Notifications persist in the database even after they are read — only "Clear read" deletes them. The feed paginates 15 per page.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- THREE TYPES --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-tags-fill text-info me-1"></i>{{ __('Notification types · pick the right one') }}
|
||||
</h6>
|
||||
<div class="row g-3 mb-5">
|
||||
<div class="col-md-4">
|
||||
<div class="p-4 rounded-4 h-100" style="background:#eff6ff;border:1px solid #bfdbfe;">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi bi-info-circle-fill text-primary fs-3"></i>
|
||||
<div class="fw-bold text-primary fs-6">{{ __('INFORMATION') }}</div>
|
||||
</div>
|
||||
<div class="small text-muted mb-3" style="line-height:1.7;">
|
||||
{{ __('Neutral updates. No action required from the recipient.') }}
|
||||
</div>
|
||||
<div class="extra-small text-muted">
|
||||
<span class="fw-bold text-dark">{{ __('Use for:') }}</span>
|
||||
<ul class="mb-0 ps-3 mt-1" style="line-height:1.8;">
|
||||
<li>{{ __('New feature announcement') }}</li>
|
||||
<li>{{ __('Scheduled maintenance reminder') }}</li>
|
||||
<li>{{ __('Newsletter / company update') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-4 rounded-4 h-100" style="background:#fffbeb;border:1px solid #fde68a;">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning fs-3"></i>
|
||||
<div class="fw-bold fs-6" style="color:#a16207;">{{ __('WARNING') }}</div>
|
||||
</div>
|
||||
<div class="small text-muted mb-3" style="line-height:1.7;">
|
||||
{{ __('Recipient should pay attention. Soft urgency.') }}
|
||||
</div>
|
||||
<div class="extra-small text-muted">
|
||||
<span class="fw-bold text-dark">{{ __('Use for:') }}</span>
|
||||
<ul class="mb-0 ps-3 mt-1" style="line-height:1.8;">
|
||||
<li>{{ __('Quota nearing limit') }}</li>
|
||||
<li>{{ __('Pending approval required') }}</li>
|
||||
<li>{{ __('Deprecated feature retiring soon') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-4 rounded-4 h-100" style="background:#fef2f2;border:1px solid #fecaca;">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi bi-megaphone-fill text-danger fs-3"></i>
|
||||
<div class="fw-bold text-danger fs-6">{{ __('SYSTEM ALERT') }}</div>
|
||||
</div>
|
||||
<div class="small text-muted mb-3" style="line-height:1.7;">
|
||||
{{ __('Immediate attention. Affects the platform itself.') }}
|
||||
</div>
|
||||
<div class="extra-small text-muted">
|
||||
<span class="fw-bold text-dark">{{ __('Use for:') }}</span>
|
||||
<ul class="mb-0 ps-3 mt-1" style="line-height:1.8;">
|
||||
<li>{{ __('Emergency maintenance starting NOW') }}</li>
|
||||
<li>{{ __('Security incident notification') }}</li>
|
||||
<li>{{ __('Service outage / degradation') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- RECIPIENT TARGETING --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-people-fill text-success me-1"></i>{{ __('Recipient targeting') }}
|
||||
</h6>
|
||||
<div class="row g-3 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="p-4 rounded-4 h-100" style="background:#f0fdf4;border:1px solid #bbf7d0;">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi bi-globe text-success fs-4"></i>
|
||||
<div class="fw-bold text-success">{{ __('All Users (Public)') }}</div>
|
||||
</div>
|
||||
<div class="small text-muted" style="line-height:1.7;">
|
||||
{{ __('Broadcast to every active account in the system, regardless of role. Use for company-wide announcements only — over-using this trains users to ignore the bell icon.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-4 rounded-4 h-100" style="background:#eff6ff;border:1px solid #bfdbfe;">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi bi-funnel-fill text-primary fs-4"></i>
|
||||
<div class="fw-bold text-primary">{{ __('By Role') }}</div>
|
||||
</div>
|
||||
<div class="small text-muted" style="line-height:1.7;">
|
||||
{{ __('Pick a specific role (e.g. Developer, Manager, Staff). Only users holding that role will see the notification. Precise — recommended default.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- USER ACTIONS --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-hand-index-fill text-warning me-1"></i>{{ __('Reader actions') }}
|
||||
</h6>
|
||||
<div class="row g-3 mb-5">
|
||||
@php
|
||||
$actions = [
|
||||
['icon'=>'bi-check2','color'=>'#22c55e','title'=>'Mark as read','desc'=>'Click a single notification card. Removes its unread highlight; stays in the feed.','boldWord'=>null],
|
||||
['icon'=>'bi-check-all','color'=>'#3b82f6','title'=>'Mark all read','desc'=>'Bulk action. Marks every unread notification at once. Useful after vacation / morning catch-up.','boldWord'=>null],
|
||||
['icon'=>'bi-trash3-fill','color'=>'#ef4444','title'=>'Clear read','desc1'=>'Permanently deletes ','boldWord'=>'already-read','desc2'=>' notifications. Unread ones are preserved. Cannot be undone.'],
|
||||
['icon'=>'bi-x-lg','color'=>'#94a3b8','title'=>'Delete one','desc'=>'Dismiss a single notification (read or unread). Removes it from your feed only — other recipients still see theirs.','boldWord'=>null],
|
||||
];
|
||||
@endphp
|
||||
@foreach($actions as $a)
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="p-3 rounded-4 h-100" style="background:#fff;border:1px solid #e2e8f0;">
|
||||
<div class="rounded-3 d-inline-flex align-items-center justify-content-center mb-2"
|
||||
style="background:{{ $a['color'] }}1a;color:{{ $a['color'] }};width:40px;height:40px;">
|
||||
<i class="{{ $a['icon'] }} fs-5"></i>
|
||||
</div>
|
||||
<div class="fw-bold text-dark mb-1" style="font-size:.88rem;">{{ $a['title'] }}</div>
|
||||
<div class="small text-muted" style="line-height:1.6;">
|
||||
@if(isset($a['boldWord']))
|
||||
{{ $a['desc1'] }}<span class="fw-bold">{{ $a['boldWord'] }}</span>{{ $a['desc2'] }}
|
||||
@else
|
||||
{{ $a['desc'] }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- WRITING GUIDE --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-pencil-square text-info me-1"></i>{{ __('Writing a good notification') }}
|
||||
</h6>
|
||||
<div class="row g-3 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 rounded-4 h-100" style="background:#f0fdf4;border:1px solid #bbf7d0;">
|
||||
<div class="fw-bold text-success mb-2"><i class="bi bi-check-circle me-1"></i>{{ __('DO') }}</div>
|
||||
<ul class="small text-muted mb-0" style="line-height:1.8;padding-left:1.2rem;">
|
||||
<li>{{ __('Lead with the most important info — recipients scan, not read') }}</li>
|
||||
<li>{{ __('Keep title under 60 characters (it gets truncated on mobile)') }}</li>
|
||||
<li>{{ __('Use plain language, avoid internal jargon') }}</li>
|
||||
<li>{{ __('Include an action verb if user needs to do something ("Click here", "Approve", "Review")') }}</li>
|
||||
<li>{{ __('Match the type to the urgency') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 rounded-4 h-100" style="background:#fef2f2;border:1px solid #fecaca;">
|
||||
<div class="fw-bold text-danger mb-2"><i class="bi bi-x-circle me-1"></i>{{ __("DON'T") }}</div>
|
||||
<ul class="small text-muted mb-0" style="line-height:1.8;padding-left:1.2rem;">
|
||||
<li>{{ __('Use SYSTEM ALERT for routine info — it loses meaning') }}</li>
|
||||
<li>{{ __('Send the same message twice — there is no undo') }}</li>
|
||||
<li>{{ __('Include sensitive data (passwords, tokens) — notifications are stored plaintext') }}</li>
|
||||
<li>{{ __('Mass-send "All Users" for things only a few care about') }}</li>
|
||||
<li>{{ __('Forget to test on the recipient role first (small group) before broadcasting') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ARCHITECTURE --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-gear-fill text-secondary me-1"></i>{{ __('How it works under the hood') }}
|
||||
</h6>
|
||||
<div class="p-4 rounded-4 mb-5" style="background:#0f172a;color:#e2e8f0;">
|
||||
<pre class="mb-0" style="font-family:'Fira Code',monospace;font-size:.78rem;line-height:2;white-space:pre-wrap;color:#cbd5e1;">
|
||||
<span style="color:#22c55e;">DELIVERY PIPELINE:</span>
|
||||
|
||||
<span style="color:#fbbf24;">[1]</span> Admin submits form → <span style="color:#22c55e;">NotificationCenterController::store()</span>
|
||||
<span style="color:#fbbf24;">[2]</span> Recipients resolved:
|
||||
<span style="color:#94a3b8;">• "all" → User::where('active', true)->get()
|
||||
• role-X → User::role('X')->get()</span>
|
||||
<span style="color:#fbbf24;">[3]</span> Laravel <span style="color:#22c55e;">SystemManagementNotification</span> dispatched per user
|
||||
<span style="color:#94a3b8;">Channels: ['database', 'broadcast']</span>
|
||||
<span style="color:#fbbf24;">[4]</span> <span style="color:#22c55e;">database</span> channel → row in <code style="color:#f59e0b;">notifications</code> table
|
||||
<span style="color:#fbbf24;">[5]</span> <span style="color:#22c55e;">broadcast</span> channel → Reverb WebSocket → bell icon updates live
|
||||
<span style="color:#fbbf24;">[6]</span> Recipient pulls feed → <span style="color:#22c55e;">GET /notification-center/api/recent</span>
|
||||
|
||||
<span style="color:#94a3b8;"># Feature flag: <code style="color:#f59e0b;">feature_notification_center</code> in Global Settings
|
||||
# When OFF, the menu hides for non-admins but admins still see it.</span></pre>
|
||||
</div>
|
||||
|
||||
{{-- PERMISSIONS --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-person-badge text-secondary me-1"></i>{{ __('Who can do what') }}
|
||||
</h6>
|
||||
<div class="table-responsive mb-5">
|
||||
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
|
||||
<thead style="background:#e2e8f0;">
|
||||
<tr>
|
||||
<th class="ps-3">{{ __('CAPABILITY') }}</th>
|
||||
<th>{{ __('REQUIRED ROLE / PERMISSION') }}</th>
|
||||
<th>{{ __('NOTES') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Read own feed') }}</td>
|
||||
<td class="small"><code>view notification center</code></td>
|
||||
<td class="small text-muted">{{ __('Required to even open this page') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Mark / delete own notifications') }}</td>
|
||||
<td class="small"><code>view notification center</code></td>
|
||||
<td class="small text-muted">{{ __('Implicit — users can always manage their own feed') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Send to a role') }}</td>
|
||||
<td class="small">{{ __('Role:') }} <span class="badge bg-dark text-white">Developer</span> {{ __('or') }} <span class="badge bg-dark text-white">Administrator</span></td>
|
||||
<td class="small text-muted">{{ __('Anyone outside these roles will not see the Send button') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Toggle the whole feature off') }}</td>
|
||||
<td class="small"><code>manage global settings</code></td>
|
||||
<td class="small text-muted">{{ __('Global Settings → Notifications →') }} <code>feature_notification_center</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- TROUBLESHOOTING --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-wrench-adjustable-circle text-secondary me-1"></i>{{ __('Troubleshooting') }}
|
||||
</h6>
|
||||
<div class="table-responsive mb-5">
|
||||
<table class="table table-hover align-middle mb-0" style="background:#f8fafc;border-radius:12px;overflow:hidden;">
|
||||
<thead style="background:#e2e8f0;">
|
||||
<tr>
|
||||
<th class="ps-3">{{ __('SYMPTOM') }}</th>
|
||||
<th>{{ __('LIKELY CAUSE') }}</th>
|
||||
<th>{{ __('FIX') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Sent but recipient sees nothing') }}</td>
|
||||
<td class="small text-muted">{{ __('Recipient has no matching role, or is inactive') }}</td>
|
||||
<td class="small text-muted">{{ __('Verify the user is active + holds the targeted role') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Bell icon does not update live') }}</td>
|
||||
<td class="small text-muted">{{ __('Reverb WebSocket disconnected') }}</td>
|
||||
<td class="small text-muted">{{ __('Check Monitoring Center → Reverb status. Restart if IDLE during traffic.') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('"Clear read" did nothing') }}</td>
|
||||
<td class="small text-muted">{{ __('Nothing was read yet — only read items get purged') }}</td>
|
||||
<td class="small text-muted">{{ __('Mark as read first, then Clear read') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Menu hidden in sidebar') }}</td>
|
||||
<td class="small text-muted">{{ __('Feature flag') }} <code>feature_notification_center</code> {{ __('is OFF') }}</td>
|
||||
<td class="small text-muted">{{ __('Global Settings → Notifications → enable the toggle') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Send button missing') }}</td>
|
||||
<td class="small text-muted">{{ __('Account lacks Developer / Administrator role') }}</td>
|
||||
<td class="small text-muted">{{ __('Ask an admin to grant the role, or use CLI to seed') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold text-dark">{{ __('Feed empty even after sending') }}</td>
|
||||
<td class="small text-muted">{{ __('The notifications table was truncated') }}</td>
|
||||
<td class="small text-muted">{{ __('Send a fresh one — old data is gone, new ones will appear') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- BEST PRACTICES --}}
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="letter-spacing:.5px;">
|
||||
<i class="bi bi-stars text-success me-1"></i>{{ __('Best practices') }}
|
||||
</h6>
|
||||
<div class="row g-2 mb-4">
|
||||
@php
|
||||
$tips = [
|
||||
['icon'=>'bi-target','title'=>'Be specific with audience','desc'=>'Target a role, not "All Users", whenever the message only matters to a subset. Saves everyone scroll time.'],
|
||||
['icon'=>'bi-clock-history','title'=>'Mind the timing','desc'=>'Avoid sending non-urgent notifications outside business hours — push fatigue eats engagement.'],
|
||||
['icon'=>'bi-funnel','title'=>'One topic per notification','desc'=>'If you have three things to say, send three notifications. Long mixed messages get skimmed.'],
|
||||
['icon'=>'bi-chat-square-quote','title'=>'Test on yourself first','desc'=>'Send to "Developer" role first, check how it looks on mobile + desktop, then broadcast.'],
|
||||
['icon'=>'bi-shield-check','title'=>'Never include secrets','desc'=>'Database rows persist until cleared. Passwords, tokens, payment info → never via notification.'],
|
||||
['icon'=>'bi-bell-slash','title'=>'Respect the bell','desc'=>'Each unnecessary notification is one step closer to users disabling the bell entirely. Less is more.'],
|
||||
];
|
||||
@endphp
|
||||
@foreach($tips as $tip)
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-start gap-2 p-3 rounded-3" style="background:#f0fdf4;border:1px solid #bbf7d0;">
|
||||
<i class="{{ $tip['icon'] }} text-success fs-4"></i>
|
||||
<div>
|
||||
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">{{ $tip['title'] }}</div>
|
||||
<div class="small text-muted" style="line-height:1.6;">{{ $tip['desc'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- FOOTER NOTE --}}
|
||||
<div class="d-flex align-items-center gap-3 p-3 rounded-4" style="background:#fef3c7;border:1px solid #fde68a;">
|
||||
<i class="bi bi-info-circle-fill text-warning fs-3"></i>
|
||||
<div>
|
||||
<div class="fw-bold text-dark mb-1" style="font-size:.85rem;">{{ __('Implementation note') }}</div>
|
||||
<div class="small text-muted">
|
||||
Notifications use Laravel's built-in <code>Illuminate\Notifications</code> with two channels:
|
||||
<code>database</code> (persistent storage) and <code>broadcast</code> (real-time via Reverb WebSocket).
|
||||
The notification class is <code>App\Notifications\SystemManagementNotification</code>.
|
||||
Per-user rows live in the <code>notifications</code> table with Laravel-standard UUIDs.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</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="{{ route('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