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:
parent
b0f12db106
commit
f133a247f9
6 changed files with 158 additions and 26 deletions
Binary file not shown.
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
@ -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 %}
|
Loading…
Add table
Reference in a new issue