participatory-planner / app /templates /admin /submissions.html
thadillo
Phase 4: UI updates for sentence-level categorization
634e667
{% extends "admin/base.html" %}
{% block title %}Submissions - Admin Dashboard{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<h2>
All Submissions ({{ submissions|length }})
{% if flagged_count > 0 %}
<span class="badge bg-danger">{{ flagged_count }} flagged</span>
{% endif %}
</h2>
<div class="d-flex gap-2 align-items-center">
{% if analyzed %}
<select class="form-select" onchange="filterCategory(this.value)">
<option value="all" {% if category_filter == 'all' %}selected{% endif %}>All Categories</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if category_filter == cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
{% endif %}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="flaggedOnly"
{% if flagged_only %}checked{% endif %} onchange="toggleFlagged(this.checked)">
<label class="form-check-label" for="flaggedOnly">
<i class="bi bi-flag-fill text-danger"></i> Flagged Only
</label>
</div>
</div>
</div>
{% if category_filter != 'all' or flagged_only %}
<div class="alert alert-info">
Showing {{ submissions|length }} submission{{ 's' if submissions|length != 1 else '' }}
{% if category_filter != 'all' %} in category: {{ category_filter }}{% endif %}
{% if flagged_only %} (flagged only){% endif %}
</div>
{% endif %}
<div class="row g-3">
{% if submissions %}
{% for sub in submissions %}
<div class="col-12">
<div class="card shadow-sm {% if sub.flagged_as_offensive %}border-danger{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="d-flex gap-2 flex-wrap align-items-center">
<span class="badge bg-secondary text-capitalize">{{ sub.contributor_type }}</span>
<select class="form-select form-select-sm" style="width: auto;"
onchange="updateCategory({{ sub.id }}, this.value, this)">
<option value="">Not categorized</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if sub.category == cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
{% if sub.flagged_as_offensive %}
<span class="badge bg-danger">
<i class="bi bi-exclamation-triangle-fill"></i> Flagged as Offensive
</span>
{% endif %}
</div>
<small class="text-muted">{{ sub.timestamp.strftime('%Y-%m-%d %H:%M') if sub.timestamp else '' }}</small>
</div>
<p class="mb-3">{{ sub.message }}</p>
{% if sub.sentence_analysis_done and sub.sentences %}
<!-- NEW: Category Distribution -->
<div class="mb-2">
<strong>Category Distribution:</strong>
{% for category, percentage in sub.get_category_distribution().items() %}
<span class="badge bg-secondary">{{ category }}: {{ percentage }}%</span>
{% endfor %}
</div>
<!-- NEW: Collapsible Sentence Breakdown -->
<button class="btn btn-sm btn-outline-primary mb-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#sentences-{{ sub.id }}">
<i class="bi bi-list-nested"></i> View Sentences ({{ sub.sentences|length }})
</button>
<div class="collapse" id="sentences-{{ sub.id }}">
<div class="border-start border-primary ps-3 mt-2">
{% for sentence in sub.sentences %}
<div class="mb-2 p-2 bg-light rounded">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<small class="text-muted">Sentence {{ sentence.sentence_index + 1 }}:</small>
<p class="mb-1">{{ sentence.text }}</p>
</div>
<div class="ms-2">
<select class="form-select form-select-sm"
style="width: 150px;"
onchange="updateSentenceCategory({{ sentence.id }}, this.value, this)">
<option value="">Uncategorized</option>
{% for cat in categories %}
<option value="{{ cat }}"
{% if sentence.category == cat %}selected{% endif %}>
{{ cat }}
</option>
{% endfor %}
</select>
</div>
</div>
{% if sentence.confidence %}
<small class="text-muted">Confidence: {{ "%.0f"|format(sentence.confidence * 100) }}%</small>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if sub.latitude and sub.longitude %}
<p class="text-muted small mb-3">
<i class="bi bi-geo-alt-fill"></i>
{{ sub.latitude|round(6) }}, {{ sub.longitude|round(6) }}
</p>
{% endif %}
<div class="d-flex gap-2 pt-3 border-top">
<button class="btn btn-sm btn-{{ 'success' if sub.flagged_as_offensive else 'warning' }}"
onclick="toggleFlag({{ sub.id }})">
<i class="bi bi-flag-fill"></i>
{{ 'Unflag' if sub.flagged_as_offensive else 'Flag as Offensive' }}
</button>
<button class="btn btn-sm btn-danger" onclick="deleteSubmission({{ sub.id }})">
<i class="bi bi-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="text-center py-5 text-muted">
<i class="bi bi-chat-square-text" style="font-size: 4rem;"></i>
<p class="mt-3">
{% if flagged_only %}
No flagged submissions
{% elif category_filter == 'all' %}
No submissions yet
{% else %}
No submissions in category: {{ category_filter }}
{% endif %}
</p>
</div>
</div>
{% endif %}
</div>
<script>
function filterCategory(category) {
const url = new URL(window.location);
url.searchParams.set('category', category);
window.location = url;
}
function toggleFlagged(checked) {
const url = new URL(window.location);
url.searchParams.set('flagged', checked ? 'true' : 'false');
window.location = url;
}
function updateCategory(submissionId, category, selectElement) {
fetch(`{{ url_for("admin.update_category", submission_id=0) }}`.replace('/0', `/${submissionId}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ category: category || null })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success feedback
const originalBg = selectElement.style.backgroundColor;
selectElement.style.backgroundColor = '#d1fae5';
setTimeout(() => {
selectElement.style.backgroundColor = originalBg;
}, 500);
} else {
alert('Failed to update category: ' + (data.error || 'Unknown error'));
location.reload();
}
})
.catch(err => {
console.error('Category update error:', err);
alert('Error updating category: ' + err.message);
location.reload();
});
}
function updateSentenceCategory(sentenceId, category, selectElement) {
fetch(`{{ url_for("admin.update_sentence_category", sentence_id=0) }}`.replace('/0', `/${sentenceId}`), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ category: category || null })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Show success feedback
const originalBg = selectElement.style.backgroundColor;
selectElement.style.backgroundColor = '#d1fae5';
setTimeout(() => {
selectElement.style.backgroundColor = originalBg;
}, 500);
} else {
alert('Failed to update sentence category: ' + (data.error || 'Unknown error'));
location.reload();
}
})
.catch(err => {
console.error('Sentence category update error:', err);
alert('Error updating sentence category: ' + err.message);
location.reload();
});
}
function toggleFlag(submissionId) {
fetch(`{{ url_for("admin.toggle_flag", submission_id=0) }}`.replace('/0', `/${submissionId}`), {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
}
function deleteSubmission(submissionId) {
if (confirm('Are you sure you want to permanently delete this submission?')) {
fetch(`{{ url_for("admin.delete_submission", submission_id=0) }}`.replace('/0', `/${submissionId}`), {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
});
}
}
</script>
{% endblock %}