Add risk trend chart to dashboard

- Updated the dashboard template to include a new section for displaying the risk trend per month using Chart.js.
- Loaded the static files correctly with the addition of the static template tag.
- Implemented JavaScript to render a line chart with risk data categorized by severity levels (Low, Medium, High, Critical).
- Utilized CSS variables for dynamic color assignment in the chart.
This commit is contained in:
= 2025-09-12 22:31:05 +02:00
parent b0f12db106
commit f133a247f9
6 changed files with 158 additions and 26 deletions

Binary file not shown.

View file

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: wira-risk-management\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-12 16:50+0200\n"
"POT-Creation-Date: 2025-09-12 22:29+0200\n"
"PO-Revision-Date: 2025-09-09 13:45+0200\n"
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
"Language-Team: German\n"
@ -437,6 +437,7 @@ msgid "Risk deleted: {t}"
msgstr "Risiko gelöscht: {t}"
#: risks/signals.py:181
#, python-brace-format
msgid "Control {e}: {t}"
msgstr "Maßnahme {e}: {t}"
@ -454,9 +455,8 @@ msgid "Control deleted: {t}"
msgstr "Maßnahme gelöscht: {t}"
#: risks/signals.py:213
#, python-brace-format
msgid "Review required for risk '{t}' due to control change"
msgstr "Prüfung nötig für: '{t}', da Maßnahmen geändert wurden"
msgid "Residual review required for risk '{t}' due to control change"
msgstr "Restrisikoprüfung nötig für das Risiko: '{t}', da Maßnahmen geändert wurden"
#: risks/signals.py:239
#, python-brace-format
@ -484,6 +484,7 @@ msgid "Residual deleted for risk: {t}"
msgstr "Restrisiko gelöscht für das Risiko: {t}"
#: risks/signals.py:316
#, python-brace-format
msgid "Incident {e}: {t}"
msgstr "Vorfall {e}: {t}"
@ -497,27 +498,27 @@ msgstr "Vorfall gelöscht: {t}"
msgid "Follow-up reached: review required for risk '{t}'"
msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{t}'"
#: risks/views.py:277
#: risks/views.py:353
msgid "Notification marked as read."
msgstr "Nachricht als gelesen markiert"
#: risks/views.py:287
#: risks/views.py:363
msgid "All notifications marked as read."
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
#: risks/views.py:306
#: risks/views.py:382
msgid "Risk status updated."
msgstr "Risikostatus Aktualisiert"
#: risks/views.py:322
#: risks/views.py:398
msgid "Control status updated."
msgstr "Maßnahmenstatus Aktualisiert"
#: risks/views.py:338
#: risks/views.py:414
msgid "Incident status updated."
msgstr "Vorfallstatus Aktualisiert"
#: risks/views.py:355
#: risks/views.py:431
msgid "Residual review flag updated."
msgstr "Restrisiko geprüft"
@ -596,6 +597,10 @@ msgstr "Keine Daten"
msgid "Incidents by Status"
msgstr "Vorfälle nach Status"
#: templates/risks/dashboard.html:127
msgid "Risk Trend (per Month)"
msgstr "Risikotrend (pro Monat)"
#: templates/risks/item_control.html:13 templates/risks/item_incident.html:13
#: templates/risks/item_risk.html:12
msgid "Overview"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-12 16:50+0200\n"
"POT-Creation-Date: 2025-09-12 22:29+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -462,7 +462,7 @@ msgstr ""
#: risks/signals.py:213
#, python-brace-format
msgid "Review required for risk '{t}' due to control change"
msgid "Residual review required for risk '{t}' due to control change"
msgstr ""
#: risks/signals.py:239
@ -505,27 +505,27 @@ msgstr ""
msgid "Follow-up reached: review required for risk '{t}'"
msgstr ""
#: risks/views.py:277
#: risks/views.py:353
msgid "Notification marked as read."
msgstr ""
#: risks/views.py:287
#: risks/views.py:363
msgid "All notifications marked as read."
msgstr ""
#: risks/views.py:306
#: risks/views.py:382
msgid "Risk status updated."
msgstr ""
#: risks/views.py:322
#: risks/views.py:398
msgid "Control status updated."
msgstr ""
#: risks/views.py:338
#: risks/views.py:414
msgid "Incident status updated."
msgstr ""
#: risks/views.py:355
#: risks/views.py:431
msgid "Residual review flag updated."
msgstr ""
@ -604,6 +604,10 @@ msgstr ""
msgid "Incidents by Status"
msgstr ""
#: templates/risks/dashboard.html:127
msgid "Risk Trend (per Month)"
msgstr ""
#: templates/risks/item_control.html:13 templates/risks/item_incident.html:13
#: templates/risks/item_risk.html:12
msgid "Overview"

View file

@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.db.models import Count
from django.db.models.functions import TruncMonth
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
@ -271,12 +272,11 @@ def show_incident(request, id):
# ---------------------------------------------------------------------------
@login_required
def dashboard(request):
"""Dashboard view with KPIs."""
# Risikoübersicht
# Bestehende KPIs
risks_total = Risk.objects.count()
risks_by_level = Risk.objects.values("level").annotate(count=Count("id"))
# CIA-Zähler für MultiSelectField
# CIA Counter
risks_cia = Risk.objects.values_list("cia", flat=True)
cia_counter = Counter()
for cia_list in risks_cia:
@ -286,14 +286,45 @@ def dashboard(request):
elif cia_list:
cia_counter[cia_list] += 1
# Residual Risks
residual_review_required = ResidualRisk.objects.filter(review_required=True).count()
# Controls & Incidents
controls_by_status = Control.objects.values("status").annotate(count=Count("id"))
incidents_status = Incident.objects.values("status").annotate(count=Count("id"))
# Notifications
notifications_unread = Notification.objects.filter(user=request.user, read=False).count()
# Risks by Level per Month (Trend)
risks_trend_qs = (
Risk.objects.annotate(month=TruncMonth("created_at"))
.values("month", "level")
.annotate(count=Count("id"))
.order_by("month")
)
# Daten für ChartJS vorbereiten
months = sorted(set(r["month"].strftime("%Y-%m") for r in risks_trend_qs if r["month"]))
levels = {r["level"] for r in risks_trend_qs}
trend_data = {lvl: [0] * len(months) for lvl in levels}
for r in risks_trend_qs:
if r["month"]:
idx = months.index(r["month"].strftime("%Y-%m"))
trend_data[r["level"]][idx] = r["count"]
context = {
"risks_total": risks_total,
"risks_by_level": risks_by_level,
"risks_by_cia": dict(cia_counter),
"residual_review_required": ResidualRisk.objects.filter(review_required=True).count(),
"controls_by_status": Control.objects.values("status").annotate(count=Count("id")),
"incidents_status": Incident.objects.values("status").annotate(count=Count("id")),
"notifications_unread": Notification.objects.filter(user=request.user, read=False).count(),
"residual_review_required": residual_review_required,
"controls_by_status": controls_by_status,
"incidents_status": incidents_status,
"notifications_unread": notifications_unread,
# Trend-Daten
"months": months,
"trend_data": trend_data,
}
return render(request, "risks/dashboard.html", context)

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load i18n risk_extras %}
{% load static i18n risk_extras %}
{% block crumbs %}
<li><a href="{% url 'risks:index' %}">{% trans "Dashboard" %}</a></li>
{% endblock %}
@ -123,7 +123,85 @@
</div>
</div><!-- Incidents by Status End -->
<div class="box">
<h2 class="title is-5">{% trans "Risk Trend (per Month)" %}</h2>
<canvas id="risksTrendChart"></canvas>
</div>
</div>
</section><!-- Dashboard Content End -->
<!-- ChartJS -->
<script src="{% static 'js/chart.js' %}"></script>
{{ months|json_script:"months-data" }}
{{ trend_data|json_script:"trend-data" }}
<script>
function getCssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
document.addEventListener("DOMContentLoaded", () => {
const ctx = document.getElementById("risksTrendChart").getContext("2d");
const months = JSON.parse(document.getElementById("months-data").textContent);
const trendData = JSON.parse(document.getElementById("trend-data").textContent);
const chartColors = {
verylow: getCssVar("--c-verylow"),
low: getCssVar("--c-low"),
mid: getCssVar("--c-mid"),
high: getCssVar("--c-high"),
veryhigh: getCssVar("--c-veryhigh")
};
const datasets = [
{
label: "Low",
data: trendData["Low"],
borderColor: chartColors.low,
backgroundColor: chartColors.low + "33",
tension: 0.3
},
{
label: "Medium",
data: trendData["Medium"],
borderColor: chartColors.mid,
backgroundColor: chartColors.mid + "33",
tension: 0.3
},
{
label: "High",
data: trendData["High"],
borderColor: chartColors.high,
backgroundColor: chartColors.high + "33",
tension: 0.3
},
{
label: "Critical",
data: trendData["Critical"],
borderColor: chartColors.veryhigh,
backgroundColor: chartColors.veryhigh + "33",
tension: 0.3
}
];
new Chart(ctx, {
type: "line",
data: {
labels: months,
datasets: datasets
},
options: {
responsive: true,
plugins: {
legend: { position: "bottom" }
},
scales: {
y: { beginAtZero: true }
}
}
});
});
</script>
{% endblock %}