feat: Implement user messages inbox with message acknowledgment tracking and UI updates

This commit is contained in:
Ross
2026-04-27 13:09:19 +01:00
parent dc479cafd0
commit 3db2c1a768
6 changed files with 322 additions and 5 deletions
@@ -94,6 +94,7 @@
</div>
<div class="mt-2 d-flex flex-wrap gap-2 justify-content-md-end">
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:collection_history' collection.pk %}">Open History</a>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:user_messages_inbox' %}">Global Inbox</a>
</div>
{% if user_message_rows %}
@@ -1,7 +1,10 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<h2>{{collection.name}}</h2>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<h2 class="mb-0">{{collection.name}}</h2>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:user_messages_inbox' %}">Global Messages Inbox</a>
</div>
{% if userexams %}
@@ -0,0 +1,116 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<h2 class="h4 mb-1">Messages Inbox</h2>
<div class="text-muted small">All accessible conversation threads across collections.</div>
</div>
<div class="card border-secondary" style="min-width: 320px;">
<div class="card-body py-2 px-3">
<div class="small text-muted">Pending acknowledgements</div>
<div class="fw-semibold {% if total_outstanding %}text-danger{% endif %}">
{{ total_outstanding }} unacknowledged message{{ total_outstanding|pluralize }}
</div>
<div class="mt-2">
{% if show_all %}
<a class="btn btn-sm btn-outline-primary" href="?">Show outstanding first</a>
{% else %}
<a class="btn btn-sm btn-primary" href="?show=all">Load full history</a>
{% endif %}
</div>
</div>
</div>
</div>
{% if inbox_rows %}
<div class="d-flex flex-column gap-3">
{% for row in inbox_rows %}
<section class="card {% if row.outstanding_count %}border-danger-subtle{% else %}border-secondary-subtle{% endif %}">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<strong>{{ row.collection.name }}</strong>
<span class="text-muted small ms-2">{{ row.target_label }}</span>
{% if row.viewer_role == 'moderator' %}
<span class="badge bg-dark ms-1">Supervisor</span>
{% else %}
<span class="badge bg-secondary ms-1">Learner</span>
{% endif %}
</div>
<div class="d-flex flex-wrap gap-2 align-items-center">
{% if row.outstanding_count %}
<span class="badge bg-danger">{{ row.outstanding_count }} unacknowledged</span>
{% else %}
<span class="badge bg-secondary">No outstanding</span>
{% endif %}
<a class="btn btn-sm btn-outline-secondary" href="{{ row.detail_url }}">Open user message page</a>
</div>
</div>
<div class="card-body">
{% if row.outstanding_rows %}
<div class="mb-3">
<div class="small fw-semibold mb-2">Outstanding</div>
<div class="d-flex flex-column gap-2">
{% for case_row in row.outstanding_rows %}
<details open>
<summary class="small fw-semibold d-flex flex-wrap align-items-center gap-2">
<span>Case {{ case_row.casedetail.sort_order|add:1 }}</span>
{% if row.collection.show_title_post %}
<span class="text-muted">{{ case_row.casedetail.case.title|default:case_row.casedetail.case.pk|truncatechars:90 }}</span>
{% endif %}
<span class="badge bg-danger">{{ case_row.stats.outstanding_total_count }} unacknowledged</span>
</summary>
<div class="mt-2"
hx-get="{{ case_row.thread_url }}{% if case_row.cid %}?cid={{ case_row.cid }}{% endif %}"
hx-trigger="load"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted">Loading conversation...</div>
</div>
</details>
{% endfor %}
</div>
</div>
{% endif %}
{% if show_all and row.history_rows %}
<div>
<div class="small fw-semibold mb-2">History</div>
<div class="accordion" id="history-{{ row.attempt.id }}">
{% for case_row in row.history_rows %}
<div class="accordion-item">
<h2 class="accordion-header" id="history-heading-{{ row.attempt.id }}-{{ case_row.casedetail.case.id }}">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#history-collapse-{{ row.attempt.id }}-{{ case_row.casedetail.case.id }}" aria-expanded="false" aria-controls="history-collapse-{{ row.attempt.id }}-{{ case_row.casedetail.case.id }}">
Case {{ case_row.casedetail.sort_order|add:1 }}
{% if row.collection.show_title_post %}
: {{ case_row.casedetail.case.title|default:case_row.casedetail.case.pk|truncatechars:90 }}
{% endif %}
<span class="badge bg-secondary ms-2">{{ case_row.stats.message_count }} messages</span>
</button>
</h2>
<div id="history-collapse-{{ row.attempt.id }}-{{ case_row.casedetail.case.id }}" class="accordion-collapse collapse" aria-labelledby="history-heading-{{ row.attempt.id }}-{{ case_row.casedetail.case.id }}" data-bs-parent="#history-{{ row.attempt.id }}">
<div class="accordion-body pt-0">
<div hx-get="{{ case_row.thread_url }}{% if case_row.cid %}?cid={{ case_row.cid }}{% endif %}"
hx-trigger="revealed once"
hx-target="this"
hx-swap="innerHTML">
<div class="small text-muted">Open section to load conversation history...</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</section>
{% endfor %}
</div>
{% else %}
<div class="card border-secondary">
<div class="card-body text-muted small">No message threads available for your account.</div>
</div>
{% endif %}
{% endblock %}