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

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -462,7 +462,7 @@ msgstr ""
#: risks/signals.py:213 #: risks/signals.py:213
#, python-brace-format #, 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 "" msgstr ""
#: risks/signals.py:239 #: risks/signals.py:239
@ -505,27 +505,27 @@ msgstr ""
msgid "Follow-up reached: review required for risk '{t}'" msgid "Follow-up reached: review required for risk '{t}'"
msgstr "" msgstr ""
#: risks/views.py:277 #: risks/views.py:353
msgid "Notification marked as read." msgid "Notification marked as read."
msgstr "" msgstr ""
#: risks/views.py:287 #: risks/views.py:363
msgid "All notifications marked as read." msgid "All notifications marked as read."
msgstr "" msgstr ""
#: risks/views.py:306 #: risks/views.py:382
msgid "Risk status updated." msgid "Risk status updated."
msgstr "" msgstr ""
#: risks/views.py:322 #: risks/views.py:398
msgid "Control status updated." msgid "Control status updated."
msgstr "" msgstr ""
#: risks/views.py:338 #: risks/views.py:414
msgid "Incident status updated." msgid "Incident status updated."
msgstr "" msgstr ""
#: risks/views.py:355 #: risks/views.py:431
msgid "Residual review flag updated." msgid "Residual review flag updated."
msgstr "" msgstr ""
@ -604,6 +604,10 @@ msgstr ""
msgid "Incidents by Status" msgid "Incidents by Status"
msgstr "" 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_control.html:13 templates/risks/item_incident.html:13
#: templates/risks/item_risk.html:12 #: templates/risks/item_risk.html:12
msgid "Overview" 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.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Count from django.db.models import Count
from django.db.models.functions import TruncMonth
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -271,12 +272,11 @@ def show_incident(request, id):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@login_required @login_required
def dashboard(request): def dashboard(request):
"""Dashboard view with KPIs.""" # Bestehende KPIs
# Risikoübersicht
risks_total = Risk.objects.count() risks_total = Risk.objects.count()
risks_by_level = Risk.objects.values("level").annotate(count=Count("id")) 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) risks_cia = Risk.objects.values_list("cia", flat=True)
cia_counter = Counter() cia_counter = Counter()
for cia_list in risks_cia: for cia_list in risks_cia:
@ -286,14 +286,45 @@ def dashboard(request):
elif cia_list: elif cia_list:
cia_counter[cia_list] += 1 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 = { context = {
"risks_total": risks_total, "risks_total": risks_total,
"risks_by_level": risks_by_level, "risks_by_level": risks_by_level,
"risks_by_cia": dict(cia_counter), "risks_by_cia": dict(cia_counter),
"residual_review_required": ResidualRisk.objects.filter(review_required=True).count(), "residual_review_required": residual_review_required,
"controls_by_status": Control.objects.values("status").annotate(count=Count("id")), "controls_by_status": controls_by_status,
"incidents_status": Incident.objects.values("status").annotate(count=Count("id")), "incidents_status": incidents_status,
"notifications_unread": Notification.objects.filter(user=request.user, read=False).count(), "notifications_unread": notifications_unread,
# Trend-Daten
"months": months,
"trend_data": trend_data,
} }
return render(request, "risks/dashboard.html", context) 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" %} {% extends "base.html" %}
{% load i18n risk_extras %} {% load static i18n risk_extras %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:index' %}">{% trans "Dashboard" %}</a></li> <li><a href="{% url 'risks:index' %}">{% trans "Dashboard" %}</a></li>
{% endblock %} {% endblock %}
@ -123,7 +123,85 @@
</div> </div>
</div><!-- Incidents by Status End --> </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> </div>
</section><!-- Dashboard Content End --> </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 %} {% endblock %}