feat: Implement dashboard with risk overview, controls, and incidents; add dark mode support

This commit is contained in:
= 2025-09-09 21:34:03 +02:00
parent bf0a3c22c0
commit 8afa7363d0
7 changed files with 275 additions and 44 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-09 14:20+0200\n"
"POT-Creation-Date: 2025-09-09 21:18+0200\n"
"PO-Revision-Date: 2025-09-09 13:45+0200\n"
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
"Language-Team: German\n"
@ -40,73 +40,55 @@ msgstr "Risikomanagement"
msgid "Risk"
msgstr "Risiko"
#: risks/models.py:36
#: risks/models.py:36 templates/risks/dashboard.html:85
msgid "Risks"
msgstr "Risiken"
#: risks/models.py:39
#, fuzzy
#| msgid "Very low occurs less than once every 5 years"
msgid "Very low occurs less than once every 5 years"
msgstr "Sehr niedrig tritt seltener als einmal in fünf Jahren auf"
#: risks/models.py:40
#, fuzzy
#| msgid "Low once every 15 years"
msgid "Low once every 15 years"
msgstr "Niedrig einmal in 15 Jahren"
#: risks/models.py:41
#, fuzzy
#| msgid "Likely once per year or more"
msgid "Likely once per year or more"
msgstr "Wahrscheinlich einmal pro Jahr oder öfter"
#: risks/models.py:42
#, fuzzy
#| msgid "Very likely multiple times per year/monthly"
msgid "Very likely multiple times per year/monthly"
msgstr "Sehr wahrscheinlich mehrmals pro Jahr/monatlich"
#: risks/models.py:45
#, fuzzy
#| msgid "Low (< 1,000 minor operational impact)"
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr "Sehr Gering (< 1.000 € geringe betriebliche Auswirkungen)"
#: risks/models.py:46
#, fuzzy
#| msgid "Medium (1,0005,000 local impact)"
msgid "Low (1,0005,000 € local impact)"
msgstr "Gering (1.0005.000 € lokale Auswirkungen)"
#: risks/models.py:47
#, fuzzy
#| msgid "High (5,00015,000 team-level impact)"
msgid "High (5,00015,000 € team-level impact)"
msgstr "Hoch (5.00015.000 € Auswirkungen auf Teamebene)"
#: risks/models.py:48
#, fuzzy
#| msgid "Severe (50,000100,000 regional impact)"
msgid "Severe (50,000100,000 € regional impact)"
msgstr "Schwerwiegend (50.000100.000 € regionale Auswirkungen)"
#: risks/models.py:49
#, fuzzy
#| msgid "Critical (> 100,000 existential threat)"
msgid "Critical (> 100,000 € existential threat)"
msgstr "Kritisch (> 100.000 € existenzielle Bedrohung)"
#: risks/models.py:52
#: risks/models.py:52 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr "Vertraulichkeit"
#: risks/models.py:53
#: risks/models.py:53 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr "Integrität"
#: risks/models.py:54
#: risks/models.py:54 templates/risks/dashboard.html:84
msgid "Availability"
msgstr "Verfügbarkeit"
@ -209,3 +191,35 @@ msgstr "Meldedatum"
#: risks/models.py:255
msgid "Reported by"
msgstr "Gemeldet von"
#: templates/risks/dashboard.html:9
msgid "Dashboard"
msgstr "Dashboard"
#: templates/risks/dashboard.html:12
msgid "Overview of Risks, Controls and Incidents"
msgstr "Übersicht der Risiken, Maßnahmen und Vorfälle"
#: templates/risks/dashboard.html:25
msgid "Total Risks"
msgstr "Restrisiken"
#: templates/risks/dashboard.html:33
msgid "Residual Risks Needing Review"
msgstr "Restrisiken ohne Verifizierung"
#: templates/risks/dashboard.html:41
msgid "Unread Notifications"
msgstr "Ungelesene Nachrichten"
#: templates/risks/dashboard.html:48
msgid "Controls by Status"
msgstr "Maßnahmen nach Status"
#: templates/risks/dashboard.html:58
msgid "Incidents by Status"
msgstr "Vorfälle nach Status"
#: templates/risks/dashboard.html:68
msgid "Risks by CIA"
msgstr "CIA Risiken"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-09 14:20+0200\n"
"POT-Creation-Date: 2025-09-09 21:18+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"
@ -46,7 +46,7 @@ msgstr ""
msgid "Risk"
msgstr ""
#: risks/models.py:36
#: risks/models.py:36 templates/risks/dashboard.html:85
msgid "Risks"
msgstr ""
@ -86,15 +86,15 @@ msgstr ""
msgid "Critical (> 100,000 € existential threat)"
msgstr ""
#: risks/models.py:52
#: risks/models.py:52 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr ""
#: risks/models.py:53
#: risks/models.py:53 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr ""
#: risks/models.py:54
#: risks/models.py:54 templates/risks/dashboard.html:84
msgid "Availability"
msgstr ""
@ -197,3 +197,35 @@ msgstr ""
#: risks/models.py:255
msgid "Reported by"
msgstr ""
#: templates/risks/dashboard.html:9
msgid "Dashboard"
msgstr ""
#: templates/risks/dashboard.html:12
msgid "Overview of Risks, Controls and Incidents"
msgstr ""
#: templates/risks/dashboard.html:25
msgid "Total Risks"
msgstr ""
#: templates/risks/dashboard.html:33
msgid "Residual Risks Needing Review"
msgstr ""
#: templates/risks/dashboard.html:41
msgid "Unread Notifications"
msgstr ""
#: templates/risks/dashboard.html:48
msgid "Controls by Status"
msgstr ""
#: templates/risks/dashboard.html:58
msgid "Incidents by Status"
msgstr ""
#: templates/risks/dashboard.html:68
msgid "Risks by CIA"
msgstr ""

View file

@ -2,10 +2,12 @@ from django.contrib.admin.models import LogEntry
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, Q
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from django.shortcuts import render, get_object_or_404
from .models import Risk, Control, ResidualRisk, AuditLog, Incident
from collections import Counter
from .models import Risk, Control, ResidualRisk, AuditLog, Incident, Notification
from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer
User = get_user_model()
@ -93,13 +95,9 @@ class IncidentViewSet(viewsets.ModelViewSet):
instance._changed_by = self.request.user
# ---------------------------------------------------------------------------
# Web
# Web => Risks, Controls, Incidents
# ---------------------------------------------------------------------------
@login_required
def dashboard(request):
return render(request, "risks/dashboard.html")
@login_required
def stats(request):
return render(request, "risks/statistics.html")
@ -220,3 +218,49 @@ def show_incident(request, id):
).order_by("-action_time")
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})
# ---------------------------------------------------------------------------
# Web => Dashboard
# ---------------------------------------------------------------------------
@login_required
def dashboard(request):
# Risikoübersicht
risks_total = Risk.objects.count()
risks_by_level = Risk.objects.values('level').annotate(count=Count('id'))
# CIA-Zähler für MultiSelectField
risks_cia = Risk.objects.values_list('cia', flat=True)
cia_counter = Counter()
for cia_list in risks_cia:
if isinstance(cia_list, list): # MultiSelectField gibt Liste zurück
for c in cia_list:
cia_counter[c] += 1
elif cia_list: # Falls irgendwie noch ein String drin ist
cia_counter[cia_list] += 1
# Residualrisiken
residual_review_required = ResidualRisk.objects.filter(review_required=True).count()
# Kontrollen
controls_by_status = Control.objects.values('status').annotate(count=Count('id'))
# Incidents
incidents_status = Incident.objects.values('status').annotate(count=Count('id'))
# Benachrichtigungen
notifications_unread = Notification.objects.filter(user=request.user, read=False).count()
print(type(cia_counter), cia_counter)
# Context für Template
context = {
'risks_total': risks_total,
'risks_by_level': risks_by_level,
'risks_by_cia': dict(cia_counter), # <-- hier Counter in dict umwandeln
'residual_review_required': residual_review_required,
'controls_by_status': controls_by_status,
'incidents_status': incidents_status,
'notifications_unread': notifications_unread,
}
return render(request, 'risks/dashboard.html', context)

View file

@ -141,4 +141,23 @@
.content li+li {
margin: 0 !important;
}
/* DARK MODE */
/* static/css/custom.css */
body.dark-mode {
background-color: #121212;
color: #f5f5f5;
}
body.dark-mode .box {
background-color: #1e1e1e;
color: #f5f5f5;
}
/* Optional: Buttons, Links etc. anpassen */
body.dark-mode a {
color: #bb86fc;
}

View file

@ -27,17 +27,10 @@
<div id="mainNavbar" class="navbar-menu">
<div class="navbar-start">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Allgemein</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/risks/index">Dashboard</a>
<a class="navbar-item" href="/risks/stats">Statistiken</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Risikomanagement</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="/risks/index">Dashboard</a>
<a class="navbar-item" href="/risks/list_risks">Risikoanalyse</a>
<a class="navbar-item" href="/risks/list_controls">Maßnahmen</a>
<a class="navbar-item" href="/risks/list_incidents">Vorfälle</a>
@ -77,6 +70,11 @@
<hr class="navbar-divider">
{% endif %}
<!-- Dark Mode Toggle -->
<button id="dark-mode-toggle" class="button is-small is-light">
🌙 Dark Mode
</button>
<!-- Logout als POST über Hidden-Form -->
<a class="navbar-item" href="#"
onclick="document.getElementById('logout-form').submit(); return false;">
@ -123,5 +121,38 @@
{% endblock %}
{% block content %}{% endblock %}
</main>
<script>
const toggleButton = document.getElementById('dark-mode-toggle');
// Dark Mode aus localStorage laden
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
toggleButton.textContent = '☀️ Light Mode';
}
toggleButton.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled');
toggleButton.textContent = '☀️ Light Mode';
} else {
localStorage.setItem('darkMode', 'disabled');
toggleButton.textContent = '🌙 Dark Mode';
}
});
// Burger Menu für mobile
document.addEventListener('DOMContentLoaded', () => {
const burger = document.querySelector('.navbar-burger');
const menu = document.getElementById(burger.dataset.target);
burger.addEventListener('click', () => {
burger.classList.toggle('is-active');
menu.classList.toggle('is-active');
});
});
</script>
</body>
</html>

View file

@ -1,4 +1,95 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
Dashboard
{% endblock %}
<!-- Hero Section -->
<section class="hero is-small is-bold">
<div class="hero-body">
<div class="container">
<h1 class="title is-1 has-text-black">
{% trans "Dashboard" %}
</h1>
<h2 class="subtitle is-4 has-text-black">
{% trans "Overview of Risks, Controls and Incidents" %}
</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="columns is-multiline">
<!-- Gesamt-Risiken -->
<div class="column is-one-quarter">
<div class="box has-background-control-mid has-text-centered">
<h2 class="title is-4">{{ risks_total }}</h2>
<p>{% trans "Total Risks" %}</p>
</div>
</div>
<!-- Residual-Risiken -->
<div class="column is-one-quarter">
<div class="box has-text-centered {% if residual_review_required > 0 %}has-background-control-high{% else %}has-background-control-low{% endif %}">
<h2 class="title is-4">{{ residual_review_required }}</h2>
<p>{% trans "Residual Risks Needing Review" %}</p>
</div>
</div>
<!-- Ungelesene Notifications -->
<div class="column is-one-quarter">
<div class="box has-background-control-mid has-text-centered">
<h2 class="title is-4">{{ notifications_unread }}</h2>
<p>{% trans "Unread Notifications" %}</p>
</div>
</div>
</div>
<!-- Controls by Status -->
<div class="box">
<h2 class="subtitle">{% trans "Controls by Status" %}</h2>
<ul>
{% for status in controls_by_status %}
<li>{{ status.status }}: {{ status.count }}</li>
{% endfor %}
</ul>
</div>
<!-- Incidents by Status -->
<div class="box">
<h2 class="subtitle">{% trans "Incidents by Status" %}</h2>
<ul>
{% for incident in incidents_status %}
<li>{{ incident.status }}: {{ incident.count }}</li>
{% endfor %}
</ul>
</div>
<!-- Risks by CIA -->
<div class="box">
<h2 class="subtitle">{% trans "Risks by CIA" %}</h2>
<div class="columns is-multiline">
{% for cia, count in risks_by_cia.items %}
<div class="column is-one-quarter">
{% if cia == '1' %}
<div class="box has-background-control-verylow has-text-centered">
<h3 class="title is-5">{% trans "Confidentiality" %}</h3>
<p>{{ count }} Risks</p>
</div>
{% elif cia == '2' %}
<div class="box has-background-control-mid has-text-centered">
<h3 class="title is-5">{% trans "Integrity" %}</h3>
<p>{{ count }} Risks</p>
</div>
{% elif cia == '3' %}
<div class="box has-background-control-high has-text-centered">
<h3 class="title is-5">{% trans "Availability" %}</h3>
<p>{{ count }} {% trans "Risks" %}</p>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</section>
{% endblock %}