feat: add mobile inbox template for notifications and discussions management
This commit is contained in:
parent
ef3f1bdaa6
commit
750abb703e
1 changed files with 369 additions and 112 deletions
|
|
@ -1,45 +1,178 @@
|
|||
{% extends "mobile/mobile_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load roles %}
|
||||
{% load mention_tags %}
|
||||
|
||||
{% block title %}{% translate "Notifications" %}{% endblock title %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
.notif-card {
|
||||
border-left: 4px solid transparent;
|
||||
/* Tabs styling */
|
||||
.mobile-inbox-tabs {
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.mobile-inbox-tabs .nav-link {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.mobile-inbox-tabs .nav-link.active {
|
||||
color: var(--brand-color, #2f3e8b);
|
||||
border-bottom-color: var(--brand-color, #2f3e8b);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Notification cards */
|
||||
.notif-card {
|
||||
border-left: 4px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.notif-card.unread {
|
||||
border-left-color: var(--brand-color, #2f3e8b);
|
||||
background-color: #f8faff;
|
||||
}
|
||||
|
||||
.notif-meta {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.notif-type-pill {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
/* Discussion cards */
|
||||
.discussion-card {
|
||||
border-left: 4px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
.discussion-card:hover, .discussion-card:focus {
|
||||
background-color: #f8faff;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.discussion-card.has-unread {
|
||||
border-left-color: var(--brand-color, #2f3e8b);
|
||||
background-color: #f4f7ff;
|
||||
}
|
||||
.avatar-discussion {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Stacked avatars */
|
||||
.avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.avatar-group .avatar-circle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #ffffff;
|
||||
margin-left: -5px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.08);
|
||||
}
|
||||
.avatar-group .avatar-circle:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.avatar-group .avatar-more {
|
||||
background-color: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
{% endblock extra_head %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Top Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1 class="h5 mb-0">
|
||||
{% translate "Notifications" %}
|
||||
{% if unread_count > 0 %}
|
||||
<span class="badge rounded-pill ms-1" style="background-color: var(--brand-color, #2f3e8b);">{{ unread_count }}</span>
|
||||
{% endif %}
|
||||
<h1 class="h5 mb-0 fw-bold">
|
||||
<i class="bi bi-bell me-1 text-primary"></i>{% translate "Notifications" %}
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'notifications:settings' %}" class="btn btn-outline-secondary btn-sm" title="{% translate 'Paramètres' %}">
|
||||
<i class="bi bi-gear"></i>
|
||||
</a>
|
||||
<a href="{% url 'mobile:mobile_index' %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-house me-1"></i>{% translate "Accueil" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<ul class="nav mobile-inbox-tabs mb-3 flex-nowrap" id="mobileInboxTabs" role="tablist">
|
||||
<li class="nav-item flex-fill text-center" role="presentation">
|
||||
<button class="nav-link w-100 justify-content-center {% if active_tab != 'discussions' %}active{% endif %}"
|
||||
id="tab-notifications-btn"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#pane-notifications"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="pane-notifications"
|
||||
aria-selected="{% if active_tab != 'discussions' %}true{% else %}false{% endif %}">
|
||||
<i class="bi bi-bell me-1.5"></i>{% translate "Notifications" %}
|
||||
{% if unread_count > 0 %}
|
||||
<span class="badge rounded-pill ms-1" style="background-color: var(--brand-color, #2f3e8b); font-size: 0.72rem;">
|
||||
{{ unread_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill text-center" role="presentation">
|
||||
<button class="nav-link w-100 justify-content-center {% if active_tab == 'discussions' %}active{% endif %}"
|
||||
id="tab-discussions-btn"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#pane-discussions"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="pane-discussions"
|
||||
aria-selected="{% if active_tab == 'discussions' %}true{% else %}false{% endif %}">
|
||||
<i class="bi bi-chat-dots me-1.5"></i>{% translate "Discussions" %}
|
||||
{% if unread_discussions_count > 0 %}
|
||||
<span class="badge rounded-pill ms-1" style="background-color: var(--brand-color, #2f3e8b); font-size: 0.72rem;">
|
||||
{{ unread_discussions_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Tabs Content -->
|
||||
<div class="tab-content" id="mobileInboxTabsContent">
|
||||
|
||||
<!-- ================= Pane 1: Notifications ================= -->
|
||||
<div class="tab-pane fade {% if active_tab != 'discussions' %}show active{% endif %}"
|
||||
id="pane-notifications"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tab-notifications-btn">
|
||||
|
||||
<!-- Filters & Quick Actions -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body py-2">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<form method="get" class="row g-2 align-items-center" id="mobile-filter-form">
|
||||
<input type="hidden" name="tab" value="notifications">
|
||||
<div class="col-12">
|
||||
<select name="type" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">{% translate "Tous les types" %}</option>
|
||||
|
|
@ -54,10 +187,10 @@
|
|||
name="archived" value="1"
|
||||
{% if show_archived %}checked{% endif %}
|
||||
onchange="this.form.submit()">
|
||||
<label class="form-check-label" for="showArchived">{% translate "Archivées" %}</label>
|
||||
<label class="form-check-label small" for="showArchived">{% translate "Archivées" %}</label>
|
||||
</div>
|
||||
{% if filter_type or show_archived %}
|
||||
<a href="{% url 'mobile:notifications_mobile' %}" class="btn btn-link btn-sm text-muted p-0">
|
||||
<a href="{% url 'mobile:notifications_mobile' %}?tab=notifications" class="btn btn-link btn-sm text-muted p-0">
|
||||
{% translate "Réinitialiser" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
@ -78,14 +211,14 @@
|
|||
<div class="d-flex flex-column gap-2">
|
||||
{% for item in notifications %}
|
||||
{% with notif=item.notification %}
|
||||
<div class="card notif-card {% if not item.is_read and not item.is_archived %}unread{% endif %}"
|
||||
<div class="card notif-card shadow-sm border-0 {% if not item.is_read and not item.is_archived %}unread{% endif %}"
|
||||
id="notif-row-{{ item.recipient_id }}-{{ item.recipient_type }}">
|
||||
<div class="card-body py-2">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex justify-content-between gap-2">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
|
||||
<i class="bi {{ notif.type_icon }} text-{{ notif.type_color }}"></i>
|
||||
<strong class="small">{{ notif.title }}</strong>
|
||||
<strong class="small text-dark">{{ notif.title }}</strong>
|
||||
{% if not item.is_read and not item.is_archived %}
|
||||
<span class="badge bg-primary notif-type-pill">{% translate "Nouveau" %}</span>
|
||||
{% endif %}
|
||||
|
|
@ -105,7 +238,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-end gap-2">
|
||||
<div class="d-flex flex-column align-items-end gap-2 flex-shrink-0">
|
||||
{% if not item.is_read %}
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-success mark-read-btn"
|
||||
|
|
@ -145,6 +278,116 @@
|
|||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ================= Pane 2: Discussions ================= -->
|
||||
<div class="tab-pane fade {% if active_tab == 'discussions' %}show active{% endif %}"
|
||||
id="pane-discussions"
|
||||
role="tabpanel"
|
||||
aria-labelledby="tab-discussions-btn">
|
||||
|
||||
<!-- Action Button: Nouvelle Discussion -->
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-primary btn-sm w-100 shadow-sm" data-bs-toggle="modal" data-bs-target="#newDiscussionModal">
|
||||
<i class="bi bi-chat-plus-fill me-1.5"></i>{% translate "Nouvelle discussion" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if not discussion_items %}
|
||||
<div class="card border-0 shadow-sm rounded-3">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-chat-square fs-1 d-block mb-3 text-primary opacity-50"></i>
|
||||
<p class="mb-0">{% translate "Vous n'avez encore aucune discussion." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for item in discussion_items %}
|
||||
<a href="{% url 'notifications:discussion_detail' item.discussion.pk %}"
|
||||
class="card discussion-card border-0 shadow-sm rounded-3 {% if item.unread_count %}has-unread{% endif %}">
|
||||
<div class="card-body py-2 px-3">
|
||||
<div class="d-flex align-items-start gap-2.5">
|
||||
<!-- Icône typée -->
|
||||
{% with type_info=item.discussion.type_info %}
|
||||
<div class="avatar-discussion {{ type_info.bg_class }} {{ type_info.text_class }} border {{ type_info.border_class }}" title="{{ type_info.label }}">
|
||||
<i class="bi {{ type_info.icon }} fs-5"></i>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
<!-- Détail -->
|
||||
<div class="flex-grow-1 min-w-0">
|
||||
<div class="d-flex justify-content-between align-items-start gap-1 mb-1">
|
||||
<span class="fw-bold text-dark text-truncate small" style="max-width: 70%;">
|
||||
{{ item.discussion.title|default:item.discussion }}
|
||||
</span>
|
||||
<span class="text-muted text-nowrap" style="font-size: 0.72rem;">
|
||||
{{ item.discussion.updated_at|date:"d/m H:i" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Badges -->
|
||||
<div class="d-flex flex-wrap gap-1 mb-1.5">
|
||||
{% if item.discussion.object_label and item.discussion.object_label != item.discussion.title %}
|
||||
<span class="badge bg-light text-secondary border rounded-pill py-0.5 px-1.5" style="font-size: 0.68rem;">
|
||||
<i class="bi bi-link-45deg me-0.5"></i>{{ item.discussion.object_label }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if item.discussion.is_closed %}
|
||||
<span class="badge bg-secondary-subtle text-secondary rounded-pill py-0.5 px-1.5" style="font-size: 0.68rem;">
|
||||
<i class="bi bi-lock-fill me-0.5"></i>{% translate "Clôturée" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Aperçu dernier message -->
|
||||
{% if item.last_message %}
|
||||
<p class="mb-2 text-muted text-truncate" style="font-size: 0.82rem;">
|
||||
<strong class="text-secondary">{{ item.last_message.author.get_full_name|default:item.last_message.author.username }}</strong> :
|
||||
<span class="text-dark opacity-75">{{ item.last_message.content|clean_mentions_preview|truncatechars:90 }}</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="mb-2 text-muted fst-italic" style="font-size: 0.82rem;">{% translate "Aucun message encore." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Participants & Badge non lus -->
|
||||
<div class="d-flex align-items-center justify-content-between pt-1">
|
||||
<div class="avatar-group">
|
||||
{% for p in item.discussion.participants.all|slice:":4" %}
|
||||
<div class="avatar-circle"
|
||||
style="background-color: {{ p|avatar_bg_color }}; color: {{ p|avatar_text_color }};"
|
||||
title="{{ p.get_full_name|default:p.username }}">
|
||||
{{ p|user_initials }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if item.discussion.participants.count > 4 %}
|
||||
<div class="avatar-circle avatar-more">
|
||||
+{{ item.discussion.participants.count|add:"-4" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if item.unread_count %}
|
||||
<span class="badge rounded-pill px-2 py-0.5 fw-semibold" style="background-color: var(--brand-color, #2f3e8b); color: #fff; font-size: 0.72rem;">
|
||||
{{ item.unread_count }} {% translate "non lu(s)" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modal Nouvelle Discussion -->
|
||||
{% include "notifications/_new_discussion_modal.html" %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
|
|
@ -155,6 +398,18 @@
|
|||
const archiveBase = "{% url 'notifications:archive' 0 %}";
|
||||
const markAllUrl = "{% url 'notifications:mark_all_read' %}";
|
||||
|
||||
// Synchronize tab changes in URL query param
|
||||
const tabs = document.querySelectorAll('#mobileInboxTabs button[data-bs-toggle="tab"]');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', event => {
|
||||
const isDiscussions = event.target.id === 'tab-discussions-btn';
|
||||
const tabName = isDiscussions ? 'discussions' : 'notifications';
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('tab', tabName);
|
||||
window.history.replaceState({}, '', url);
|
||||
});
|
||||
});
|
||||
|
||||
function updateBadge(count) {
|
||||
const badges = document.querySelectorAll('#mobile-navbar-notif-badge, #navbar-notif-badge');
|
||||
badges.forEach((badge) => {
|
||||
|
|
@ -188,7 +443,8 @@
|
|||
}
|
||||
|
||||
document.querySelectorAll('.mark-read-btn').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
button.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
button.disabled = true;
|
||||
const pk = button.dataset.pk;
|
||||
const type = button.dataset.type;
|
||||
|
|
@ -207,7 +463,8 @@
|
|||
});
|
||||
|
||||
document.querySelectorAll('.archive-btn').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
button.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
button.disabled = true;
|
||||
const pk = button.dataset.pk;
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue