Files
B42/at_django_boilerplate/user_activity/templates/active_sessions.html
2026-01-07 12:09:20 +05:30

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 %}