Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
188 lines
14 KiB
HTML
188 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Notifications{% endblock %}
|
|
{% block breadcrumb %}
|
|
<nav class="mb-4" aria-label="Breadcrumb">
|
|
<ol class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
|
<li><a href="{% url 'dashboard' %}" class="hover:text-gray-700 dark:hover:text-gray-300">Dashboard</a></li>
|
|
<li class="text-gray-300 dark:text-gray-600">/</li>
|
|
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Notifications</li>
|
|
</ol>
|
|
</nav>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="max-w-6xl mx-auto" x-data="notifList()">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h1 class="text-2xl font-bold">Notifications</h1>
|
|
<a href="{% url 'notifications-mark-all-read' %}" class="dk-btn dk-btn-primary text-sm">Mark all as read</a>
|
|
</div>
|
|
|
|
<div class="flex justify-center mb-6">
|
|
<nav role="tablist" class="inline-flex items-center gap-1 rounded-xl bg-gray-100 dark:bg-gray-950 p-1">
|
|
<a href="?filter=all{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" role="tab" class="px-4 py-2 text-sm font-medium rounded-lg whitespace-nowrap transition-colors flex items-center gap-2 {% if filter == 'all' %}bg-white dark:bg-gray-950 shadow-sm text-gray-900 dark:text-gray-100{% else %}text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300{% endif %}">
|
|
<span>All</span>
|
|
<span class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 text-[11px] font-bold rounded-full {% if filter == 'all' %}bg-gray-900/10 dark:bg-gray-100/10 text-gray-600 dark:text-gray-300{% else %}bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-400{% endif %}">{{ all_count }}</span>
|
|
</a>
|
|
<a href="?filter=unread{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" role="tab" class="px-4 py-2 text-sm font-medium rounded-lg whitespace-nowrap transition-colors flex items-center gap-2 {% if filter == 'unread' %}bg-white dark:bg-gray-950 shadow-sm text-gray-900 dark:text-gray-100{% else %}text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300{% endif %}">
|
|
<span>Unread</span>
|
|
<span class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 text-[11px] font-bold rounded-full {% if filter == 'unread' %}bg-gray-900/10 dark:bg-gray-100/10 text-gray-600 dark:text-gray-300{% else %}bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-400{% endif %}">{{ unread_count }}</span>
|
|
</a>
|
|
<a href="?filter=read{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" role="tab" class="px-4 py-2 text-sm font-medium rounded-lg whitespace-nowrap transition-colors flex items-center gap-2 {% if filter == 'read' %}bg-white dark:bg-gray-950 shadow-sm text-gray-900 dark:text-gray-100{% else %}text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300{% endif %}">
|
|
<span>Read</span>
|
|
<span class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 text-[11px] font-bold rounded-full {% if filter == 'read' %}bg-gray-900/10 dark:bg-gray-100/10 text-gray-600 dark:text-gray-300{% else %}bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-400{% endif %}">{{ read_count }}</span>
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<form method="GET" class="flex-1 flex items-center gap-3">
|
|
<input type="hidden" name="filter" value="{{ filter }}">
|
|
<div class="relative flex-1 max-w-md">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"/></svg>
|
|
<input type="text" name="search" value="{{ search_query }}" placeholder="Search notifications..." class="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<select name="type" class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg py-2 px-3 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<option value="">All types</option>
|
|
<option value="comment_added" {% if type_filter == 'comment_added' %}selected{% endif %}>Comments</option>
|
|
<option value="status_changed" {% if type_filter == 'status_changed' %}selected{% endif %}>Status changes</option>
|
|
<option value="project_assigned" {% if type_filter == 'project_assigned' %}selected{% endif %}>Assignments</option>
|
|
<option value="project_removed" {% if type_filter == 'project_removed' %}selected{% endif %}>Removals</option>
|
|
</select>
|
|
<select name="per_page" onchange="this.form.submit()" class="text-sm border border-gray-300 dark:border-gray-600 rounded-lg py-2 px-3 bg-white dark:bg-gray-950 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<option value="10" {% if per_page == 10 %}selected{% endif %}>10 per page</option>
|
|
<option value="20" {% if per_page == 20 %}selected{% endif %}>20 per page</option>
|
|
<option value="50" {% if per_page == 50 %}selected{% endif %}>50 per page</option>
|
|
</select>
|
|
<button type="submit" class="dk-btn dk-btn-primary text-sm">Apply</button>
|
|
{% if search_query or type_filter %}
|
|
<a href="?filter={{ filter }}" class="text-sm text-gray-500 dark:text-gray-400 hover:underline">Clear</a>
|
|
{% endif %}
|
|
</form>
|
|
</div>
|
|
|
|
{% if notifications %}
|
|
<form method="POST" action="{% url 'notifications-mark-bulk-read' %}" id="bulk-form">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
|
|
|
<div x-show="selectedIds.length > 0" x-cloak class="mb-4 flex items-center gap-3 px-4 py-2.5 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
<span class="text-sm font-medium text-blue-700 dark:text-blue-300" x-text="selectedIds.length + ' selected'"></span>
|
|
<button type="submit" class="dk-btn dk-btn-primary text-xs">Mark selected as read</button>
|
|
<button type="button" @click="selectedIds = []" class="text-xs text-blue-600 dark:text-blue-400 hover:underline ml-auto">Clear selection</button>
|
|
</div>
|
|
|
|
<div class="dk-card overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800">
|
|
<th class="px-4 py-3 w-10">
|
|
<input type="checkbox" @click="toggleAll($event.target.checked)" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
</th>
|
|
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Ticket</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date</th>
|
|
<th class="text-right px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-36">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100 dark:divide-gray-600">
|
|
{% for notif in notifications %}
|
|
<tr class="{% if not notif.read_at %}bg-blue-50/40 dark:bg-blue-900/10{% else %}hover:bg-gray-50 dark:hover:bg-gray-800/30{% endif %} transition-colors" :class="{ 'bg-blue-100/60 dark:bg-blue-900/20': selectedIds.includes({{ notif.id }}) }">
|
|
<td class="px-4 py-3">
|
|
<input type="checkbox" name="notification_ids" value="{{ notif.id }}" x-model="selectedIds" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
</td>
|
|
|
|
|
|
<td class="px-4 py-3">
|
|
<div class="max-w-md">
|
|
<p class="text-sm {% if not notif.read_at %}font-semibold{% else %}font-medium{% endif %} text-gray-900 dark:text-gray-100 truncate">{{ notif.title|striptags }}</p>
|
|
{% if notif.message %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">{{ notif.message|striptags }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
{% if notif.data.ticket_id %}
|
|
<a href="{% url 'ticket-detail' notif.data.ticket_id %}" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-900/50 transition-colors">
|
|
Ticket #{{ notif.data.ticket_id }}
|
|
</a>
|
|
{% else %}
|
|
<span class="text-xs text-gray-400 dark:text-gray-500">N/A</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
|
|
{% if notif.type == 'comment_added' %}bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300
|
|
{% elif notif.type == 'status_changed' %}bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-300
|
|
{% elif notif.type == 'project_assigned' %}bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-300
|
|
{% elif notif.type == 'project_removed' %}bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-300
|
|
{% else %}bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300{% endif %}">
|
|
{{ notif.type|title|cut:"_" }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">{{ notif.created_at|timesince }} ago</td>
|
|
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
<div class="flex items-center justify-end gap-2">
|
|
{% if not notif.read_at %}
|
|
<a href="{% url 'notification-mark-read' notif.id %}?next={{ request.get_full_path|urlencode }}" class="text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline">Mark read</a>
|
|
{% endif %}
|
|
{% if notif.data.ticket_id %}
|
|
<a href="{% url 'ticket-detail' notif.data.ticket_id %}" class="text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:underline">View</a>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{% if page_obj.has_other_pages %}
|
|
<div class="flex items-center justify-center gap-3 mt-6">
|
|
{% if page_obj.has_previous %}
|
|
<a href="?page={{ page_obj.previous_page_number }}&filter={{ filter }}{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" class="dk-btn dk-btn-ghost text-sm">Previous</a>
|
|
{% endif %}
|
|
<div class="flex items-center gap-1">
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if num == page_obj.number %}
|
|
<span class="w-8 h-8 flex items-center justify-center text-sm font-medium rounded-lg bg-blue-600 text-white">{{ num }}</span>
|
|
{% elif num > page_obj.number|add:"-3" and num < page_obj.number|add:"3" %}
|
|
<a href="?page={{ num }}&filter={{ filter }}{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" class="w-8 h-8 flex items-center justify-center text-sm font-medium rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">{{ num }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% if page_obj.has_next %}
|
|
<a href="?page={{ page_obj.next_page_number }}&filter={{ filter }}{% if search_query %}&search={{ search_query }}{% endif %}{% if type_filter %}&type={{ type_filter }}{% endif %}{% if per_page %}&per_page={{ per_page }}{% endif %}" class="dk-btn dk-btn-ghost text-sm">Next</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="dk-card py-16 text-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" viewBox="0 0 24 24" 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>
|
|
<h3 class="text-lg font-medium text-gray-500 dark:text-gray-400">No notifications found</h3>
|
|
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">
|
|
{% if search_query or type_filter %}
|
|
Try adjusting your search or filter.
|
|
{% else %}
|
|
You'll see notifications here when activity happens on your tickets.
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
function notifList() {
|
|
return {
|
|
selectedIds: [],
|
|
toggleAll(checked) {
|
|
this.selectedIds = checked ? [{% for n in notifications %}{{ n.id }}{% if not forloop.last %},{% endif %}{% endfor %}] : [];
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |