Enhances user experience with dark‑mode detection, responsive layout, and streamlined role selection workflow
109 lines
7.4 KiB
HTML
109 lines
7.4 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
{% block title %}Dashboard{% endblock %}
|
|
{% block breadcrumb %}
|
|
<nav class="mb-4" aria-label="Breadcrumb">
|
|
<ol class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
|
|
<li class="text-gray-900 dark:text-white font-medium" aria-current="page">Dashboard</li>
|
|
</ol>
|
|
</nav>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<!-- Welcome Banner -->
|
|
<div class="mb-6 rounded-xl p-6 shadow-sm" style="background: linear-gradient(135deg, var(--color-primary, #3b82f6) 0%, var(--color-primary-hover, #2563eb) 100%); color: #ffffff;">
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-xl sm:text-2xl font-bold"><span id="greeting-text">Welcome back</span>, {{ request.user.get_full_name|default:request.user.username }}!</h1>
|
|
<p id="dashboard-quote" class="text-blue-100 text-sm mt-1.5 italic font-medium">Loading quote...</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-2xl font-bold">{% now "d" %}</p>
|
|
<p class="text-blue-100 text-xs uppercase tracking-wider">{% now "M Y" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
fetch('{% static "quotes.json" %}')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data && data.quotes && data.quotes.length > 0) {
|
|
const quotesList = data.quotes;
|
|
const randomQuote = quotesList[Math.floor(Math.random() * quotesList.length)];
|
|
document.getElementById('dashboard-quote').textContent = `"${randomQuote.quote}" — ${randomQuote.author}`;
|
|
} else {
|
|
throw new Error('Invalid structure');
|
|
}
|
|
})
|
|
.catch(() => {
|
|
const fallbackQuotes = [
|
|
"The only way to do great work is to love what you do. — Steve Jobs",
|
|
"Quality is not an act, it is a habit. — Aristotle",
|
|
"Action is the foundational key to all success. — Pablo Picasso",
|
|
"Strive not to be a success, but rather to be of value. — Albert Einstein",
|
|
"The secret of getting ahead is getting started. — Mark Twain"
|
|
];
|
|
const randomQuote = fallbackQuotes[Math.floor(Math.random() * fallbackQuotes.length)];
|
|
document.getElementById('dashboard-quote').textContent = randomQuote;
|
|
});
|
|
</script>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 mb-6">
|
|
{% for stat in stats %}
|
|
<div class="group relative rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 p-5 transition-all duration-200 hover:shadow-md hover:-translate-y-0.5">
|
|
{% if stat.super %}
|
|
<div class="absolute top-4 right-4">
|
|
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-900/30 px-2 py-0.5 text-[10px] font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wider">Admin</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg
|
|
{% if stat.color == 'primary' %}bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400
|
|
{% elif stat.color == 'success' %}bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400
|
|
{% elif stat.color == 'info' %}bg-cyan-100 dark:bg-cyan-900/50 text-cyan-600 dark:text-cyan-400
|
|
{% elif stat.color == 'danger' %}bg-red-100 dark:bg-red-900/50 text-red-600 dark:text-red-400
|
|
{% elif stat.color == 'warning' %}bg-amber-100 dark:bg-amber-900/50 text-amber-600 dark:text-amber-400
|
|
{% else %}bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400{% endif %}">
|
|
<i class="fas {{ stat.icon }} text-lg"></i>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-600 dark:text-gray-400">{{ stat.label }}</span>
|
|
</div>
|
|
<p class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">{{ stat.value }}</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ stat.desc }}</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Charts Row 1 -->
|
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 mb-6">
|
|
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10" hx-get="/dashboard/charts/tickets-per-project/" hx-trigger="load, every[60s]" hx-swap="outerHTML"><div class="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-600"><div><h3 class="text-base font-semibold text-gray-900 dark:text-white">Tickets per Project</h3><p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Distribution across projects</p></div></div><div class="p-6"><div class="h-[280px] animate-pulse bg-gray-100 dark:bg-gray-800 rounded"></div></div></div>
|
|
<div class="xl:col-span-2 rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10" hx-get="/dashboard/charts/monthly-trend/" hx-trigger="load, every[60s]" hx-swap="outerHTML"><div class="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-600"><div><h3 class="text-base font-semibold text-gray-900 dark:text-white">Monthly Trend</h3><p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Ticket creation over time</p></div></div><div class="p-6"><div class="h-[280px] animate-pulse bg-gray-100 dark:bg-gray-800 rounded"></div></div></div>
|
|
</div>
|
|
|
|
<!-- Charts Row 2 + Recent Activity -->
|
|
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6 mb-6">
|
|
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10" hx-get="/dashboard/charts/user-statistics/" hx-trigger="load" hx-swap="outerHTML"><div class="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-600"><div><h3 class="text-base font-semibold text-gray-900 dark:text-white">User Statistics</h3><p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Individual contributions</p></div></div><div class="p-6"><div class="h-[280px] animate-pulse bg-gray-100 dark:bg-gray-800 rounded"></div></div></div>
|
|
<div class="xl:col-span-2 rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10" hx-get="/dashboard/recent-activity/" hx-trigger="load, every[30s]" hx-swap="outerHTML"><div class="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-600"><div><h3 class="text-base font-semibold text-gray-900 dark:text-white">Recent Activity</h3><p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Latest updates across projects</p></div></div><div class="p-6"><div class="h-[280px] animate-pulse bg-gray-100 dark:bg-gray-800 rounded"></div></div></div>
|
|
</div>
|
|
|
|
<!-- Project Timeline -->
|
|
<div class="rounded-xl bg-white dark:bg-gray-950 shadow-sm ring-1 ring-gray-950/5 dark:ring-white/10 mb-6" hx-get="/dashboard/project-timeline-widget/" hx-trigger="load, every[60s]" hx-swap="outerHTML"><div class="flex items-center gap-3 px-6 py-4 border-b border-gray-100 dark:border-gray-600"><div><h3 class="text-base font-semibold text-gray-900 dark:text-white">Project Timeline</h3><p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Upcoming milestones and deadlines</p></div></div><div class="p-6"><div class="h-20 animate-pulse bg-gray-100 dark:bg-gray-800 rounded"></div></div></div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
(function() {
|
|
var el = document.getElementById('greeting-text');
|
|
if (!el) return;
|
|
var hour = new Date().getHours();
|
|
var g;
|
|
if (hour >= 5 && hour < 12) g = 'Good morning';
|
|
else if (hour >= 12 && hour < 17) g = 'Good afternoon';
|
|
else if (hour >= 17 && hour < 21) g = 'Good evening';
|
|
else g = 'Good night';
|
|
el.textContent = g;
|
|
})();
|
|
</script>
|
|
{% endblock %}
|