535 lines
22 KiB
HTML
Executable File
535 lines
22 KiB
HTML
Executable File
{% extends 'base.html' %}
|
|
|
|
{% block content %}
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
|
|
|
<div class="container py-5">
|
|
<div class="card shadow-lg rounded-3 overflow-hidden">
|
|
<!-- Header -->
|
|
<div class="card-header py-4" style="background: linear-gradient(135deg, #4f46e5, #7c3aed);">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h3 class="mb-0 fw-bold text-white">
|
|
<i class="fas fa-chart-pie me-2"></i>Website Analytics Dashboard
|
|
</h3>
|
|
<div class="text-white small opacity-75">
|
|
<div><i class="fas fa-network-wired me-2"></i>IP: {{ current_ip|default:"Unknown" }}</div>
|
|
<div><i class="fas fa-map-marker-alt me-2"></i>Location: {{ current_location|default:"Unknown" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="card-body p-5">
|
|
<!-- FY Selector & Total Visits -->
|
|
<div class="d-flex justify-content-between align-items-center mb-5">
|
|
<h4 class="fw-bold text-gray-800">
|
|
<i class="fas fa-chart-bar me-2 text-primary"></i>Public Page Analytics
|
|
</h4>
|
|
|
|
<!-- Date Filter -->
|
|
<div class="d-flex flex-wrap gap-3 align-items-end mb-3">
|
|
<!-- Quick Select -->
|
|
<div class="flex-grow-1" style="min-width:150px;">
|
|
<label class="text-gray-600 fw-medium small">Quick Select</label>
|
|
<select id="quickSelect" class="form-select form-select-sm" onchange="applyQuickSelect()">
|
|
<option value="">Custom</option>
|
|
<option value="today">Today</option>
|
|
<option value="last7">Last 7 Days</option>
|
|
<option value="last14">Last 14 Days</option>
|
|
<option value="lastMonth">Last Month</option>
|
|
<option value="lastQuarter">Last Quarter</option>
|
|
<option value="ytd">Year to Date</option>
|
|
<option value="last365">Last 365 Days</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- From Date -->
|
|
<div>
|
|
<label class="text-gray-600 fw-medium small">From</label>
|
|
<input type="date" id="fromDate" class="form-control form-control-sm">
|
|
</div>
|
|
|
|
<!-- To Date -->
|
|
<div>
|
|
<label class="text-gray-600 fw-medium small">To</label>
|
|
<input type="date" id="toDate" class="form-control form-control-sm">
|
|
</div>
|
|
|
|
<!-- Apply Button -->
|
|
<div class="align-self-end">
|
|
<button class="btn btn-sm btn-primary mb-1" onclick="applyDateFilter()">
|
|
Apply
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function applyQuickSelect() {
|
|
const select = document.getElementById('quickSelect');
|
|
const fromInput = document.getElementById('fromDate');
|
|
const toInput = document.getElementById('toDate');
|
|
|
|
const today = new Date();
|
|
let fromDate, toDate;
|
|
|
|
switch(select.value) {
|
|
case 'today':
|
|
fromDate = today; toDate = today; break;
|
|
case 'last7':
|
|
fromDate = new Date(today); fromDate.setDate(today.getDate() - 6); toDate = today; break;
|
|
case 'last14':
|
|
fromDate = new Date(today); fromDate.setDate(today.getDate() - 13); toDate = today; break;
|
|
case 'lastMonth':
|
|
fromDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
toDate = new Date(today.getFullYear(), today.getMonth(), 0); break;
|
|
case 'lastQuarter':
|
|
const currentMonth = today.getMonth();
|
|
const currentQuarter = Math.floor(currentMonth / 3);
|
|
fromDate = new Date(today.getFullYear(), (currentQuarter - 1) * 3, 1);
|
|
toDate = new Date(today.getFullYear(), currentQuarter * 3, 0); break;
|
|
case 'ytd':
|
|
fromDate = new Date(today.getFullYear(), 0, 1); toDate = today; break;
|
|
case 'last365':
|
|
fromDate = new Date(today); fromDate.setDate(today.getDate() - 364); toDate = today; break;
|
|
default:
|
|
fromInput.value = ''; toInput.value = ''; return;
|
|
}
|
|
|
|
const format = d => d.toISOString().split('T')[0];
|
|
fromInput.value = format(fromDate);
|
|
toInput.value = format(toDate);
|
|
}
|
|
|
|
function applyDateFilter() {
|
|
const from = document.getElementById('fromDate').value;
|
|
const to = document.getElementById('toDate').value;
|
|
|
|
const url = new URL(window.location);
|
|
if (from) url.searchParams.set('from', from);
|
|
if (to) url.searchParams.set('to', to);
|
|
|
|
window.location.href = url.toString();
|
|
}
|
|
</script>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="text-center text-gray-600 mb-5 fs-5">
|
|
<strong>{{ total_visits|default:0 }}</strong> total page visits in FY {{ selected_fy }}
|
|
</div>
|
|
|
|
<!-- Top 10 Most Visited Pages -->
|
|
<h5 class="fw-bold text-gray-800 mb-4">
|
|
<i class="fas fa-globe me-2 text-primary"></i>Top 10 Most Visited Pages
|
|
</h5>
|
|
{% if top_parts %}
|
|
<div class="row mb-5">
|
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
|
<div class="table-responsive shadow-sm rounded">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light text-uppercase small text-gray-600">
|
|
<tr>
|
|
<th class="ps-4">Page Path</th>
|
|
<th class="text-end pe-4">Visits</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for part in top_parts %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<code class="small bg-gray-100 px-2 py-1 rounded">{{ part.name|default:"/" }}</code>
|
|
</td>
|
|
<td class="text-end pe-4 fw-bold">{{ part.visits }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="chart-container shadow-sm rounded">
|
|
<canvas id="topPartsChart" height="350"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4 text-gray-500">
|
|
<i class="fas fa-globe fa-3x opacity-25 mb-3"></i>
|
|
<p>No page visit data available for this period.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<hr class="my-5 border-gray-300">
|
|
|
|
<!-- Visits by Location -->
|
|
<h5 class="fw-bold text-gray-800 mb-4">
|
|
<i class="fas fa-map-marker-alt me-2 text-primary"></i>Visits by Location (Top 10)
|
|
</h5>
|
|
{% if location_analytics.data %}
|
|
<div class="row mb-5">
|
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
|
<div class="table-responsive shadow-sm rounded">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light text-uppercase small text-gray-600">
|
|
<tr>
|
|
<th class="ps-4">Location</th>
|
|
<th class="text-end">Visits</th>
|
|
<th class="text-end pe-4">Percentage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for label, count, percentage in location_analytics.data %}
|
|
<tr>
|
|
<td class="ps-4">{{ label|default:"Unknown" }}</td>
|
|
<td class="text-end">{{ count }}</td>
|
|
<td class="text-end pe-4">{{ percentage }}%</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="chart-container shadow-sm rounded">
|
|
<canvas id="locationChart" height="350"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-4 text-gray-500">
|
|
<i class="fas fa-map-marker-alt fa-3x opacity-25 mb-3"></i>
|
|
<p>No location data available.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<hr class="my-5 border-gray-300">
|
|
|
|
<!-- Top Paths by Location -->
|
|
<h5 class="fw-bold text-gray-800 mb-4">
|
|
<i class="fas fa-link me-2 text-primary"></i>Top Pages by Location
|
|
</h5>
|
|
{% if paths_by_location.locations %}
|
|
{% for loc in paths_by_location.locations %}
|
|
<div class="mb-5">
|
|
<h6 class="text-gray-700 fw-semibold mb-3">{{ loc.location|default:"Unknown" }}</h6>
|
|
<div class="row">
|
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
|
<div class="table-responsive shadow-sm rounded">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light text-uppercase small text-gray-600">
|
|
<tr>
|
|
<th class="ps-4">Page Path</th>
|
|
<th class="text-end">Visits</th>
|
|
<th class="text-end pe-4">Percentage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for label, count, percentage in loc.data %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<code class="small bg-gray-100 px-2 py-1 rounded">{{ label|truncatechars:50 }}</code>
|
|
</td>
|
|
<td class="text-end">{{ count }}</td>
|
|
<td class="text-end pe-4">{{ percentage }}%</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="chart-container shadow-sm rounded">
|
|
<canvas id="pathChart_{{ forloop.counter }}" height="350"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-4 text-gray-500">
|
|
<i class="fas fa-link fa-3x opacity-25 mb-3"></i>
|
|
<p>No path data available by location.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<hr class="my-5 border-gray-300">
|
|
|
|
<!-- Devices by Location -->
|
|
<h5 class="fw-bold text-gray-800 mb-4">
|
|
<i class="fas fa-mobile-alt me-2 text-primary"></i>Device Types by Location
|
|
</h5>
|
|
{% if devices_by_location.locations %}
|
|
{% for loc in devices_by_location.locations %}
|
|
<div class="mb-5">
|
|
<h6 class="text-gray-700 fw-semibold mb-3">{{ loc.location|default:"Unknown" }}</h6>
|
|
<div class="row">
|
|
<div class="col-lg-6 mb-4 mb-lg-0">
|
|
<div class="table-responsive shadow-sm rounded">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light text-uppercase small text-gray-600">
|
|
<tr>
|
|
<th class="ps-4">Device</th>
|
|
<th class="text-end">Visits</th>
|
|
<th class="text-end pe-4">Percentage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for label, count, percentage in loc.data %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<i class="fas {% if label == 'Mobile' %}fa-mobile-alt{% elif label == 'Desktop' %}fa-laptop{% elif label == 'Tablet' %}fa-tablet-alt{% else %}fa-question-circle{% endif %} me-2 text-gray-600"></i>
|
|
{{ label }}
|
|
</td>
|
|
<td class="text-end">{{ count }}</td>
|
|
<td class="text-end pe-4">{{ percentage }}%</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="chart-container shadow-sm rounded">
|
|
<canvas id="deviceChart_{{ forloop.counter }}" height="350"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="text-center py-4 text-gray-500">
|
|
<i class="fas fa-mobile-alt fa-3x opacity-25 mb-3"></i>
|
|
<p>No device data available.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="card-footer bg-gray-50 py-4">
|
|
<div class="text-end">
|
|
<button class="btn btn-sm btn-gradient-primary" onclick="window.location.reload()">
|
|
<i class="fas fa-sync-alt me-1"></i> Refresh Dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Styles -->
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; }
|
|
.card { border: none; border-radius: 1.5rem; background: #ffffff; }
|
|
.chart-container {
|
|
position: relative;
|
|
height: 350px;
|
|
background: #fff;
|
|
border-radius: 1rem;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
}
|
|
code { font-family: 'Courier New', monospace; background: #f3f4f6; border-radius: 6px; }
|
|
.btn-gradient-primary {
|
|
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 0.75rem;
|
|
padding: 0.75rem 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
.btn-gradient-primary:hover {
|
|
background: linear-gradient(135deg, #4338ca, #6d28d9);
|
|
transform: translateY(-2px);
|
|
}
|
|
.text-gray-600 { color: #4b5563; }
|
|
.text-gray-700 { color: #374151; }
|
|
.text-gray-800 { color: #1f2937; }
|
|
.bg-gray-50 { background-color: #f9fafb; }
|
|
.bg-gray-100 { background-color: #f3f4f6; }
|
|
</style>
|
|
|
|
<!-- Charts -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const colors = {
|
|
background: ['#4f46e5', '#7c3aed', '#ec4899', '#f43f5e', '#fbbf24', '#2dd4bf', '#22c55e', '#3b82f6', '#a855f7', '#eab308'],
|
|
border: ['#4338ca', '#6d28d9', '#db2777', '#dc2626', '#f59e0b', '#14b8a6', '#16a34a', '#2563eb', '#9333ea', '#ca8a04']
|
|
};
|
|
|
|
Chart.register(ChartDataLabels);
|
|
|
|
// Top Pages Chart
|
|
{% if top_parts %}
|
|
new Chart(document.getElementById('topPartsChart'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: [{% for part in top_parts %}'{{ part.name|escapejs }}'{% if not forloop.last %}, {% endif %}{% endfor %}],
|
|
datasets: [{
|
|
data: [{% for part in top_parts %}{{ part.visits }}{% if not forloop.last %}, {% endif %}{% endfor %}],
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.border,
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'right', labels: { font: { family: 'Inter', size: 13 } } },
|
|
title: { display: true, text: 'Top 10 Most Visited Pages', font: { size: 16, family: 'Inter', weight: '600' }, color: '#1f2937' },
|
|
datalabels: {
|
|
color: '#fff',
|
|
font: { weight: 'bold', size: 12 },
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return percentage > 5 ? percentage + '%' : '';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
{% endif %}
|
|
|
|
// Location Chart
|
|
{% if location_analytics.data %}
|
|
new Chart(document.getElementById('locationChart'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: {{ location_analytics.labels|safe }},
|
|
datasets: [{
|
|
data: {{ location_analytics.counts|safe }},
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.border,
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'right' },
|
|
title: { display: true, text: 'Visits by Location', font: { size: 16, family: 'Inter', weight: '600' }, color: '#1f2937' },
|
|
datalabels: {
|
|
color: '#fff',
|
|
font: { weight: 'bold' },
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return percentage > 5 ? percentage + '%' : '';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
{% endif %}
|
|
|
|
// Paths by Location Charts
|
|
{% for loc in paths_by_location.locations %}
|
|
new Chart(document.getElementById('pathChart_{{ forloop.counter }}'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: {{ loc.labels|safe }},
|
|
datasets: [{
|
|
data: {{ loc.counts|safe }},
|
|
backgroundColor: colors.background,
|
|
borderColor: colors.border,
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'right' },
|
|
title: { display: true, text: 'Top Pages in {{ loc.location|escapejs }}', font: { size: 15, family: 'Inter' } },
|
|
datalabels: {
|
|
color: '#fff',
|
|
font: { weight: 'bold' },
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return percentage > 8 ? percentage + '%' : '';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
{% endfor %}
|
|
|
|
// Devices by Location Charts
|
|
{% for loc in devices_by_location.locations %}
|
|
new Chart(document.getElementById('deviceChart_{{ forloop.counter }}'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: {{ loc.labels|safe }},
|
|
datasets: [{
|
|
data: {{ loc.counts|safe }},
|
|
backgroundColor: colors.background.slice(0, {{ loc.labels|length }}),
|
|
borderColor: colors.border.slice(0, {{ loc.labels|length }}),
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'right' },
|
|
title: { display: true, text: 'Devices in {{ loc.location|escapejs }}', font: { size: 15, family: 'Inter' } },
|
|
datalabels: {
|
|
color: '#fff',
|
|
font: { weight: 'bold' },
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return percentage > 10 ? percentage + '%' : '';
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: [ChartDataLabels]
|
|
});
|
|
{% endfor %}
|
|
|
|
// FY Update Function
|
|
window.updateFY = function(fy) {
|
|
const url = new URL(window.location);
|
|
url.searchParams.set('fy', fy);
|
|
window.location.href = url.toString();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
|
|
<script>
|
|
function applyDateFilter() {
|
|
const from = document.getElementById('fromDate').value;
|
|
const to = document.getElementById('toDate').value;
|
|
|
|
console.log("From date:", from);
|
|
console.log("To date:", to);
|
|
|
|
const url = new URL(window.location);
|
|
if (from) url.searchParams.set('from', from);
|
|
if (to) url.searchParams.set('to', to);
|
|
|
|
console.log("Redirecting to:", url.toString());
|
|
|
|
|
|
window.location.href = url.toString();
|
|
}
|
|
</script>
|
|
|
|
{% endblock %} |