feat: Implement dashboard with risk overview, controls, and incidents; add dark mode support
This commit is contained in:
parent
bf0a3c22c0
commit
8afa7363d0
7 changed files with 275 additions and 44 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-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 1–5 years"
|
||||
msgstr "Niedrig – einmal in 1–5 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,000–5,000 € – local impact)"
|
||||
msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)"
|
||||
|
||||
#: risks/models.py:47
|
||||
#, fuzzy
|
||||
#| msgid "High (5,00015,000 team-level impact)"
|
||||
msgid "High (5,000–15,000 € – team-level impact)"
|
||||
msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)"
|
||||
|
||||
#: risks/models.py:48
|
||||
#, fuzzy
|
||||
#| msgid "Severe (50,000100,000 regional impact)"
|
||||
msgid "Severe (50,000–100,000 € – regional impact)"
|
||||
msgstr "Schwerwiegend (50.000–100.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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Add table
Reference in a new issue