rustpbx 0.4.2

A SIP PBX implementation in Rust
Documentation
{% extends "console/layout.html" %}
{% block title %}{{ "notifications_page.title" | t }} ยท {{site_name|default('RustPBX')}}{% endblock %}
{% block content %}
<div class="p-6" x-data="notificationsPage()" x-init="init()">
    <div class="mx-auto max-w-4xl space-y-6">
        <header class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
            <div class="space-y-1">
                <p class="text-xs font-semibold uppercase tracking-wide text-sky-600">{{
                    "notifications_page.system_notifications" | t }}</p>
                <h1 class="text-2xl font-semibold text-slate-900">{{ "notifications_page.title" | t }}</h1>
                <p class="text-sm text-slate-500">{{ "notifications_page.system_notifications" | t }}</p>
            </div>
            <button type="button" @click="markAllRead()"
                class="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-600 shadow-sm hover:bg-slate-50 transition">
                <svg class="h-4 w-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                    stroke-width="1.8">
                    <path stroke-linecap="round" stroke-linejoin="round"
                        d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
                {{ "notifications_page.mark_all_read" | t }}
            </button>
        </header>

        {% if notifications %}
        <div class="space-y-3">
            {% for n in notifications %}
            <div id="notification-{{ n.id }}"
                class="flex items-start gap-4 rounded-xl bg-white p-5 shadow-sm ring-1 transition {{ 'ring-sky-200 bg-sky-50/40' if not n.read else 'ring-black/5' }}">
                <!-- Icon -->
                <div
                    class="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-full
                    {{ 'bg-sky-100 text-sky-600' if n.kind == 'update' else ('bg-amber-100 text-amber-600' if n.kind == 'warning' else 'bg-slate-100 text-slate-500') }}">
                    {% if n.kind == 'update' %}
                    <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round"
                            d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
                    </svg>
                    {% elif n.kind == 'warning' %}
                    <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round"
                            d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
                    </svg>
                    {% else %}
                    <svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <path stroke-linecap="round" stroke-linejoin="round"
                            d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
                    </svg>
                    {% endif %}
                </div>
                <!-- Content -->
                <div class="min-w-0 flex-1">
                    <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                            <p class="text-sm font-semibold text-slate-900">{{ n.title }}</p>
                            {% if not n.read %}
                            <span class="inline-block h-2 w-2 rounded-full bg-sky-500" title="{{ "
                                notifications_page.unread" | t }}"></span>
                            {% endif %}
                        </div>
                        <time class="shrink-0 text-xs text-slate-400">{{ n.created_at }}</time>
                    </div>
                    {% if n.body %}
                    <p class="mt-1 text-sm text-slate-600">{{ n.body }}</p>
                    {% endif %}
                    {% if not n.read %}
                    <button type="button" @click="markRead({{ n.id }})" data-mark-read
                        class="mt-2 text-xs font-semibold text-sky-600 hover:text-sky-500 transition">
                        {{ "notifications_page.mark_as_read" | t }}
                    </button>
                    {% endif %}
                </div>
            </div>
            {% endfor %}
        </div>
        {% else %}
        <div
            class="flex flex-col items-center justify-center rounded-xl bg-white py-20 text-center shadow-sm ring-1 ring-black/5">
            <svg class="h-12 w-12 text-slate-300" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                stroke-width="1">
                <path stroke-linecap="round" stroke-linejoin="round"
                    d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
            </svg>
            <p class="mt-4 text-sm font-semibold text-slate-700">{{ "notifications_page.no_notifications" | t }}</p>
            <p class="mt-1 text-xs text-slate-400">{{ "notifications_page.all_caught_up" | t }}</p>
        </div>
        {% endif %}
    </div>
</div>

{% block scripts %}
<script>
    function notificationsPage() {
        return {
            init() { },
            async markRead(id) {
                const res = await fetch(`{{ api_prefix | safe }}/notifications/${id}/read`, { method: 'POST' });
                if (res.ok) {
                    const el = document.getElementById(`notification-${id}`);
                    if (el) {
                        el.classList.remove('ring-sky-200', 'bg-sky-50/40');
                        el.classList.add('ring-black/5');
                        el.querySelectorAll('[title="Unread"]').forEach(e => e.remove());
                        el.querySelectorAll('[data-mark-read]').forEach(b => b.remove());
                    }
                    window.dispatchEvent(new CustomEvent('notifications:refresh'));
                }
            },
            async markAllRead() {
                const res = await fetch(`{{ api_prefix | safe }}/notifications/read-all`, { method: 'POST' });
                if (res.ok) {
                    window.location.reload();
                }
            }
        };
    }
</script>
{% endblock %}
{% endblock %}