2026-03-28 14:03:56 +03:00

321 lines
12 KiB
HTML

{% extends 'layouts/base.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Notifications" %} - PX360{% endblock %}
{% block content %}
<!-- Header -->
<header class="mb-6">
<div class="flex justify-between items-start">
<div>
<h1 class="text-2xl font-bold text-navy">
<i data-lucide="bell" class="w-6 h-6 inline-block mr-2"></i>
{% trans "Notifications" %}
</h1>
<p class="text-sm text-slate mt-1">{% trans "View and manage your notifications" %}</p>
</div>
<div class="flex gap-3">
{% if unread_count > 0 %}
<button onclick="markAllAsRead()"
class="inline-flex items-center gap-2 px-4 py-2 bg-navy text-white rounded-xl text-sm font-semibold hover:bg-blue transition">
<i data-lucide="check-double" class="w-4 h-4"></i>
{% trans "Mark all as read" %}
</button>
{% endif %}
{% if notifications %}
<button onclick="dismissAll()"
class="inline-flex items-center gap-2 px-4 py-2 border border-slate-200 text-slate rounded-xl text-sm font-semibold hover:bg-light transition">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Dismiss all" %}
</button>
{% endif %}
</div>
</div>
</header>
<!-- Filter Tabs -->
<div class="mb-6">
<div class="flex gap-2">
<a href="?filter=all"
class="px-4 py-2 rounded-xl text-sm font-semibold transition {% if filter == 'all' %}bg-navy text-white{% else %}bg-white text-slate hover:bg-light{% endif %}">
{% trans "All" %}
</a>
<a href="?filter=unread"
class="px-4 py-2 rounded-xl text-sm font-semibold transition {% if filter == 'unread' %}bg-navy text-white{% else %}bg-white text-slate hover:bg-light{% endif %}">
{% trans "Unread" %}
{% if unread_count > 0 %}
<span class="ml-2 bg-red text-white text-xs px-2 py-0.5 rounded-full">{{ unread_count }}</span>
{% endif %}
</a>
<a href="?filter=read"
class="px-4 py-2 rounded-xl text-sm font-semibold transition {% if filter == 'read' %}bg-navy text-white{% else %}bg-white text-slate hover:bg-light{% endif %}">
{% trans "Read" %}
</a>
</div>
</div>
<!-- Notifications List -->
{% if notifications %}
<div class="space-y-3">
{% for notification in notifications %}
<div id="notification-{{ notification.id }}"
class="bg-white rounded-2xl shadow-sm border-2 {% if not notification.is_read %}border-blue-200{% else %}border-slate-100{% endif %} p-5 hover:shadow-md transition cursor-pointer"
onclick="openNotification('{{ notification.id }}', '{{ notification.action_url|default:'' }}')">
<div class="flex items-start gap-4">
<!-- Icon -->
<div class="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 {% if not notification.is_read %}bg-blue-100{% else %}bg-slate-100{% endif %}">
<i data-lucide="{{ notification.get_icon|default:'bell' }}"
class="w-6 h-6 {% if not notification.is_read %}text-blue-600{% else %}text-slate-500{% endif %}"></i>
</div>
<!-- Content -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="font-bold text-navy {% if not notification.is_read %}text-lg{% else %}text-base{% endif %}">
{{ notification.get_title }}
</h3>
<p class="text-slate mt-1 line-clamp-2">{{ notification.get_message }}</p>
<div class="flex items-center gap-3 mt-2 text-xs text-slate">
<span>{{ notification.created_at|timesince }} {% trans "ago" %}</span>
{% if not notification.is_read %}
<span class="bg-blue text-white px-2 py-0.5 rounded-full">{% trans "New" %}</span>
{% endif %}
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 flex-shrink-0">
{% if not notification.is_read %}
<button onclick="event.stopPropagation(); markAsRead('{{ notification.id }}')"
class="p-2 text-blue hover:bg-blue-50 rounded-lg transition"
title="{% trans 'Mark as read' %}">
<i data-lucide="check" class="w-5 h-5"></i>
</button>
{% endif %}
<button onclick="event.stopPropagation(); dismissNotification('{{ notification.id }}')"
class="p-2 text-slate hover:bg-slate-100 rounded-lg transition"
title="{% trans 'Dismiss' %}">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Pagination -->
{% if notifications.has_other_pages %}
<div class="mt-6 flex justify-center gap-2">
{% if notifications.has_previous %}
<a href="?filter={{ filter }}&page={{ notifications.previous_page_number }}"
class="px-4 py-2 border border-slate-200 rounded-xl text-slate hover:bg-light transition">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</a>
{% endif %}
<span class="px-4 py-2 bg-navy text-white rounded-xl font-semibold">
{{ notifications.number }} / {{ notifications.paginator.num_pages }}
</span>
{% if notifications.has_next %}
<a href="?filter={{ filter }}&page={{ notifications.next_page_number }}"
class="px-4 py-2 border border-slate-200 rounded-xl text-slate hover:bg-light transition">
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</a>
{% endif %}
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="text-center py-16">
<div class="w-24 h-24 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-6">
<i data-lucide="bell-off" class="w-12 h-12 text-slate-400"></i>
</div>
<h3 class="text-xl font-bold text-navy mb-2">
{% if filter == 'unread' %}
{% trans "No unread notifications" %}
{% elif filter == 'read' %}
{% trans "No read notifications" %}
{% else %}
{% trans "No notifications" %}
{% endif %}
</h3>
<p class="text-slate">
{% if filter == 'unread' %}
{% trans "You're all caught up!" %}
{% else %}
{% trans "You don't have any notifications yet." %}
{% endif %}
</p>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Mark single notification as read
async function markAsRead(notificationId) {
try {
const response = await fetch(`/notifications/api/mark-read/${notificationId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
if (response.ok) {
const card = document.getElementById(`notification-${notificationId}`);
if (card) {
// Remove blue border, add gray border
card.classList.remove('border-blue-200');
card.classList.add('border-slate-100');
// Update icon background
const iconContainer = card.querySelector('.w-12');
iconContainer.classList.remove('bg-blue-100');
iconContainer.classList.add('bg-slate-100');
// Update icon color
const icon = card.querySelector('.w-6');
icon.classList.remove('text-blue-600');
icon.classList.add('text-slate-500');
// Remove "New" badge
const badge = card.querySelector('.bg-blue.text-white');
if (badge) badge.remove();
// Update title size
const title = card.querySelector('h3');
title.classList.remove('text-lg');
title.classList.add('text-base');
// Remove mark as read button
const readBtn = card.querySelector('[title="{% trans 'Mark as read' %}"]');
if (readBtn) readBtn.remove();
}
updateUnreadCount();
}
} catch (error) {
console.error('Error marking notification as read:', error);
}
}
// Mark all as read
async function markAllAsRead() {
try {
const response = await fetch('/notifications/api/mark-all-read/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
if (response.ok) {
window.location.reload();
}
} catch (error) {
console.error('Error marking all as read:', error);
}
}
// Dismiss single notification
async function dismissNotification(notificationId) {
try {
const response = await fetch(`/notifications/api/dismiss/${notificationId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
if (response.ok) {
const card = document.getElementById(`notification-${notificationId}`);
if (card) {
card.style.opacity = '0';
card.style.transform = 'translateX(20px)';
setTimeout(() => card.remove(), 300);
}
updateUnreadCount();
}
} catch (error) {
console.error('Error dismissing notification:', error);
}
}
// Dismiss all
async function dismissAll() {
if (!confirm('{% trans "Are you sure you want to dismiss all notifications?" %}')) {
return;
}
try {
const response = await fetch('/notifications/api/dismiss-all/', {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
if (response.ok) {
window.location.reload();
}
} catch (error) {
console.error('Error dismissing all:', error);
}
}
// Open notification - mark as read and navigate
async function openNotification(notificationId, actionUrl) {
// Mark as read first
await markAsRead(notificationId);
// Navigate if URL exists
if (actionUrl) {
window.location.href = actionUrl;
}
}
// Update unread count badge
async function updateUnreadCount() {
try {
const response = await fetch('/notifications/api/unread-count/');
const data = await response.json();
// Update topbar badge
const badge = document.getElementById('notification-count-badge');
if (badge) {
if (data.count > 0) {
badge.textContent = data.count;
badge.classList.remove('hidden');
} else {
badge.classList.add('hidden');
}
}
} catch (error) {
console.error('Error updating count:', error);
}
}
// Get CSRF cookie
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
</script>
{% endblock %}