488 lines
23 KiB
HTML
488 lines
23 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
{% load social_filters %}
|
|
|
|
{% block title %}{% trans "Analytics Dashboard" %} - {% trans "Social Media Monitoring" %} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Page Header -->
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-3">
|
|
<div>
|
|
<h2 class="mb-1 fw-bold">
|
|
<i class="bi bi-graph-up-arrow text-primary me-2"></i>
|
|
{% trans "Analytics Dashboard" %}
|
|
</h2>
|
|
<p class="text-muted mb-0 small">{% trans "Social media insights and trends" %}</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<a href="{% url 'social:social_comment_list' %}" class="btn btn-outline-primary">
|
|
<i class="bi bi-grid-fill me-1"></i> {% trans "Dashboard" %}
|
|
</a>
|
|
<form class="d-inline-flex" method="get">
|
|
<div class="input-group shadow-sm">
|
|
<span class="input-group-text bg-light border-end-0">
|
|
<i class="bi bi-calendar-range text-muted"></i>
|
|
</span>
|
|
<input type="date"
|
|
name="start_date"
|
|
class="form-control border-start-0 border-end-0"
|
|
value="{{ start_date|default:'' }}"
|
|
placeholder="{% trans 'Date from' %}">
|
|
<span class="input-group-text bg-light border-end-0 border-start-0">-</span>
|
|
<input type="date"
|
|
name="end_date"
|
|
class="form-control border-start-0"
|
|
value="{{ end_date|default:'' }}"
|
|
placeholder="{% trans 'Date to' %}">
|
|
<button type="submit" class="btn btn-primary px-4">
|
|
<i class="bi bi-funnel-fill me-1"></i> {% trans "Filter" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overview Cards -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- Total Comments Card -->
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Total Comments" %}</h6>
|
|
<h2 class="mb-0 fw-bold text-primary">{{ total_comments }}</h2>
|
|
</div>
|
|
<div class="bg-primary bg-opacity-10 rounded-3 p-3">
|
|
<i class="bi bi-chat-dots text-primary fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="badge bg-success-subtle text-success">
|
|
<i class="bi bi-check-circle me-1"></i>{{ analyzed_comments }}
|
|
</span>
|
|
<span class="text-muted small">{% trans "analyzed" %}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Positive Comments Card -->
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Positive" %}</h6>
|
|
<h2 class="mb-0 fw-bold text-success">{{ sentiment_distribution|get_sentiment_count:'positive' }}</h2>
|
|
</div>
|
|
<div class="bg-success bg-opacity-10 rounded-3 p-3">
|
|
<i class="bi bi-emoji-smile text-success fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="progress mb-2" style="height: 6px;">
|
|
<div class="progress-bar bg-success"
|
|
role="progressbar"
|
|
style="width: {% widthratio sentiment_distribution|get_sentiment_count:'positive' total_comments 100 %}%"></div>
|
|
</div>
|
|
<small class="text-muted">{% widthratio sentiment_distribution|get_sentiment_count:'positive' total_comments 100 %}% {% trans "of total" %}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Negative Comments Card -->
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Negative" %}</h6>
|
|
<h2 class="mb-0 fw-bold text-danger">{{ sentiment_distribution|get_sentiment_count:'negative' }}</h2>
|
|
</div>
|
|
<div class="bg-danger bg-opacity-10 rounded-3 p-3">
|
|
<i class="bi bi-emoji-frown text-danger fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="progress mb-2" style="height: 6px;">
|
|
<div class="progress-bar bg-danger"
|
|
role="progressbar"
|
|
style="width: {% widthratio sentiment_distribution|get_sentiment_count:'negative' total_comments 100 %}%"></div>
|
|
</div>
|
|
<small class="text-muted">{% widthratio sentiment_distribution|get_sentiment_count:'negative' total_comments 100 %}% {% trans "of total" %}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avg Engagement Card -->
|
|
<div class="col-lg-3 col-md-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
<div>
|
|
<h6 class="text-muted text-uppercase small fw-bold mb-2">{% trans "Avg Engagement" %}</h6>
|
|
<h2 class="mb-0 fw-bold text-info">{{ engagement_metrics.avg_likes|add:engagement_metrics.avg_replies|floatformat:1 }}</h2>
|
|
</div>
|
|
<div class="bg-info bg-opacity-10 rounded-3 p-3">
|
|
<i class="bi bi-heart-pulse text-info fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<small class="text-muted"><i class="bi bi-lightning me-1"></i>{% trans "likes + replies" %}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- Sentiment Distribution -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-pie-chart-fill text-primary"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Sentiment Distribution" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<canvas id="sentimentChart" height="220"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Platform Distribution -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-info bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-bar-chart-fill text-info"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Platform Distribution" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<canvas id="platformChart" height="220"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Daily Trends -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-12">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-success bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-graph-up-arrow text-success"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Daily Trends" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<canvas id="trendsChart" height="120"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keywords & Topics -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- Top Keywords -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-warning bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-key-fill text-warning"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Top Keywords" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
{% if top_keywords %}
|
|
<div class="row g-2">
|
|
{% for keyword in top_keywords|slice:":12" %}
|
|
<div class="col-md-6 col-sm-12 mb-2">
|
|
<div class="card border-0 shadow-sm p-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span class="fw-semibold">{{ keyword.keyword }}</span>
|
|
<span class="badge bg-primary rounded-pill">{{ keyword.count }}</span>
|
|
</div>
|
|
<div class="progress mb-0" style="height: 8px;">
|
|
<div class="progress-bar bg-warning"
|
|
role="progressbar"
|
|
style="width: {% widthratio keyword.count top_keywords.0.count 100 %}%;
|
|
background: linear-gradient(90deg, #ffc107 0%, #ffca2c 100%);"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-key text-muted fs-1"></i>
|
|
<p class="text-muted mt-2">{% trans "No keywords found" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Topics -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-collection-fill text-primary"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Top Topics" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
{% if top_topics %}
|
|
<div class="list-group list-group-flush">
|
|
{% for topic in top_topics|slice:":10" %}
|
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0 py-3 border-0">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary bg-opacity-10 rounded-circle p-2 me-3">
|
|
<i class="bi bi-hash text-primary small"></i>
|
|
</div>
|
|
<span class="fw-medium">{{ topic.topic }}</span>
|
|
</div>
|
|
<span class="badge bg-info rounded-pill px-3 py-2">{{ topic.count }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-collection text-muted fs-1"></i>
|
|
<p class="text-muted mt-2">{% trans "No topics found" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Platform Breakdown -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-12">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-secondary bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-grid-fill text-secondary"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Platform Breakdown" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-striped border-0">
|
|
<thead>
|
|
<tr class="table-light">
|
|
<th class="fw-bold text-muted small text-uppercase ps-3">{% trans "Platform" %}</th>
|
|
<th class="fw-bold text-muted small text-uppercase">{% trans "Comments" %}</th>
|
|
<th class="fw-bold text-muted small text-uppercase">{% trans "Avg Sentiment" %}</th>
|
|
<th class="fw-bold text-muted small text-uppercase">{% trans "Total Likes" %}</th>
|
|
<th class="fw-bold text-muted small text-uppercase">{% trans "Total Replies" %}</th>
|
|
<th class="fw-bold text-muted small text-uppercase pe-3">{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for platform in platform_distribution %}
|
|
<tr class="align-middle">
|
|
<td class="ps-3">
|
|
<a href="{% url 'social:social_platform' platform.platform %}" class="text-decoration-none fw-semibold text-primary">
|
|
{{ platform.platform_display }}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-light text-dark">{{ platform.count }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="{% if platform.avg_sentiment > 0.5 %}badge bg-success-subtle text-success{% elif platform.avg_sentiment < 0.5 %}badge bg-danger-subtle text-danger{% else %}badge bg-secondary-subtle text-secondary{% endif %} px-3 py-2 rounded-pill">
|
|
{{ platform.avg_sentiment|floatformat:2 }}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="d-inline-flex align-items-center gap-1">
|
|
<i class="bi bi-hand-thumbs-up text-muted small"></i>
|
|
<span>{{ platform.total_likes }}</span>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="d-inline-flex align-items-center gap-1">
|
|
<i class="bi bi-chat text-muted small"></i>
|
|
<span>{{ platform.total_replies }}</span>
|
|
</span>
|
|
</td>
|
|
<td class="pe-3">
|
|
<a href="{% url 'social:social_platform' platform.platform %}" class="btn btn-sm btn-primary rounded-pill px-3">
|
|
<i class="bi bi-arrow-right me-1"></i>{% trans "View" %}
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="6" class="text-center py-5">
|
|
<i class="bi bi-grid text-muted fs-1 mb-2"></i>
|
|
<p class="text-muted mb-0">{% trans "No data available" %}</p>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Entities -->
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-0 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-purple bg-opacity-10 rounded-2 p-2 me-3">
|
|
<i class="bi bi-tags-fill text-purple"></i>
|
|
</div>
|
|
<h6 class="mb-0 fw-bold">{% trans "Top Entities" %}</h6>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
{% if top_entities %}
|
|
<div class="row g-3">
|
|
{% for entity in top_entities|slice:":20" %}
|
|
<div class="col-lg-2 col-md-3 col-sm-4 col-6 mb-3">
|
|
<div class="card border-0 shadow-sm h-100 hover-lift">
|
|
<div class="card-body text-center p-3">
|
|
<div class="bg-purple bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-2" style="width: 60px; height: 60px;">
|
|
<i class="bi bi-tag text-purple fs-5"></i>
|
|
</div>
|
|
<h6 class="mb-2 fw-semibold text-truncate" style="max-width: 120px; margin: 0 auto;">{{ entity.entity }}</h6>
|
|
<span class="badge bg-purple text-white rounded-pill px-3 py-2">{{ entity.count }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-tags text-muted fs-1 mb-2"></i>
|
|
<p class="text-muted mb-0">{% trans "No entities found" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// Sentiment Distribution Chart
|
|
const sentimentCtx = document.getElementById('sentimentChart').getContext('2d');
|
|
new Chart(sentimentCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Positive', 'Neutral', 'Negative'],
|
|
datasets: [{
|
|
data: [
|
|
{% for item in sentiment_distribution %}{% if item.sentiment == 'positive' %}{{ item.count }}{% endif %}{% endfor %},
|
|
{% for item in sentiment_distribution %}{% if item.sentiment == 'neutral' %}{{ item.count }}{% endif %}{% endfor %},
|
|
{% for item in sentiment_distribution %}{% if item.sentiment == 'negative' %}{{ item.count }}{% endif %}{% endfor %}
|
|
],
|
|
backgroundColor: ['#198754', '#6c757d', '#dc3545']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Platform Distribution Chart
|
|
const platformCtx = document.getElementById('platformChart').getContext('2d');
|
|
new Chart(platformCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [{% for item in platform_distribution %}'{{ item.platform_display }}',{% endfor %}],
|
|
datasets: [{
|
|
label: 'Comments',
|
|
data: [{% for item in platform_distribution %}{{ item.count }},{% endfor %}],
|
|
backgroundColor: '#0d6efd'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Daily Trends Chart
|
|
const trendsCtx = document.getElementById('trendsChart').getContext('2d');
|
|
new Chart(trendsCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [{% for item in daily_trends %}'{{ item.day }}',{% endfor %}],
|
|
datasets: [
|
|
{
|
|
label: 'Total',
|
|
data: [{% for item in daily_trends %}{{ item.count }},{% endfor %}],
|
|
borderColor: '#0d6efd',
|
|
tension: 0.1
|
|
},
|
|
{
|
|
label: 'Positive',
|
|
data: [{% for item in daily_trends %}{{ item.positive }},{% endfor %}],
|
|
borderColor: '#198754',
|
|
tension: 0.1
|
|
},
|
|
{
|
|
label: 'Negative',
|
|
data: [{% for item in daily_trends %}{{ item.negative }},{% endfor %}],
|
|
borderColor: '#dc3545',
|
|
tension: 0.1
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
{% endblock %}
|