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 ""
|
||||
"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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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 %}
|
Loading…
Add table
Reference in a new issue