Rework Frontend UI

This commit is contained in:
Kevin Heyer 2025-09-11 15:02:29 +02:00
parent 9d02badf14
commit 324347e849
7 changed files with 594 additions and 548 deletions

View file

@ -10,6 +10,17 @@ _LIKELIHOOD_LABELS = dict(Risk.LIKELIHOOD_CHOICES)
_IMPACT_LABELS = dict(Risk.IMPACT_CHOICES) _IMPACT_LABELS = dict(Risk.IMPACT_CHOICES)
LEVEL_ID_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4} LEVEL_ID_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4}
@register.simple_tag
def sort_url(request, field, current_sort, current_dir):
query = request.GET.copy()
# Richtung bestimmen
if current_sort == field and current_dir == "asc":
query["dir"] = "desc"
else:
query["dir"] = "asc"
query["sort"] = field
return f"?{query.urlencode()}"
@register.filter @register.filter
def dict_get(d, key): def dict_get(d, key):
try: try:

View file

@ -116,12 +116,9 @@ class IncidentViewSet(viewsets.ModelViewSet):
@login_required @login_required
def list_risks(request): def list_risks(request):
"""
View for listing all Risks
"""
qs = Risk.objects.all().select_related("owner", "residual_risk") qs = Risk.objects.all().select_related("owner", "residual_risk")
# GET-Parameter lesen # Filter
risk_id = request.GET.get("risk") risk_id = request.GET.get("risk")
control_id = request.GET.get("control") control_id = request.GET.get("control")
owner_id = request.GET.get("owner") owner_id = request.GET.get("owner")
@ -142,24 +139,29 @@ def list_risks(request):
if process: if process:
qs = qs.filter(process=process) qs = qs.filter(process=process)
risks = qs.order_by("title").distinct() sort = request.GET.get("sort") or "title"
direction = request.GET.get("dir") or "asc"
if direction == "desc":
qs = qs.order_by(f"-{sort}")
else:
qs = qs.order_by(sort)
controls = Control.objects.all().order_by("title") risks = qs.distinct()
owners = User.objects.filter(owned_risks__isnull=False).distinct().order_by("username")
categories = (Risk.objects risk_choices = Risk.objects.all().order_by("title")
.exclude(category__isnull=True) control_choices = Control.objects.all().order_by("title")
owner_choices = User.objects.filter(owned_risks__isnull=False).distinct().order_by("username")
category_choices = (Risk.objects.exclude(category__isnull=True)
.exclude(category__exact="") .exclude(category__exact="")
.values_list("category", flat=True) .values_list("category", flat=True)
.distinct() .distinct()
.order_by("category")) .order_by("category"))
assets = (Risk.objects asset_choices = (Risk.objects.exclude(asset__isnull=True)
.exclude(asset__isnull=True)
.exclude(asset__exact="") .exclude(asset__exact="")
.values_list("asset", flat=True) .values_list("asset", flat=True)
.distinct() .distinct()
.order_by("asset")) .order_by("asset"))
processes = (Risk.objects process_choices = (Risk.objects.exclude(process__isnull=True)
.exclude(process__isnull=True)
.exclude(process__exact="") .exclude(process__exact="")
.values_list("process", flat=True) .values_list("process", flat=True)
.distinct() .distinct()
@ -167,11 +169,14 @@ def list_risks(request):
return render(request, "risks/list_risks.html", { return render(request, "risks/list_risks.html", {
"risks": risks, "risks": risks,
"controls": controls, "risk_choices": risk_choices,
"owners": owners, "control_choices": control_choices,
"categories": categories, "owner_choices": owner_choices,
"assets": assets, "category_choices": category_choices,
"processes": processes, "asset_choices": asset_choices,
"process_choices": process_choices,
"current_sort": sort,
"current_dir": direction,
}) })
@login_required @login_required

View file

@ -1,176 +1,197 @@
/* Base palette */ /* =========================
Base Palette
========================= */
:root { :root {
--c-verylow:#22c55e; --c-verylow-100:#dcfce7; --c-verylow-300:#86efac; --c-verylow-inv:#000; --c-verylow:#22c55e; --c-verylow-100:#dcfce7; --c-verylow-300:#86efac; --c-verylow-inv:#000;
--c-low:#84cc16; --c-low-100:#ecfccb; --c-low-300:#bef264; --c-low-inv:#111; --c-low:#84cc16; --c-low-100:#ecfccb; --c-low-300:#bef264; --c-low-inv:#111;
--c-mid:#eab308; --c-mid-100:#fef9c3; --c-mid-300:#fde047; --c-mid-inv:#111; --c-mid:#eab308; --c-mid-100:#fef9c3; --c-mid-300:#fde047; --c-mid-inv:#111;
--c-high:#f97316; --c-high-100:#ffedd5; --c-high-300:#fbbf24; --c-high-inv:#111; --c-high:#f97316; --c-high-100:#ffedd5; --c-high-300:#fbbf24; --c-high-inv:#111;
--c-veryhigh:#dc2626; --c-veryhigh-100:#fee2e2;--c-veryhigh-300:#fca5a5; --c-veryhigh-inv:#000; --c-veryhigh:#dc2626; --c-veryhigh-100:#fee2e2;--c-veryhigh-300:#fca5a5; --c-veryhigh-inv:#000;
--prosoft-normal:#6f3165;
--prosoft-inv:#fff;
} }
/* Helpers (wie Bulma) */ /* =========================
.has-text-control-verylow{color:var(--c-verylow)!important} Helpers (similar to Bulma)
.has-text-control-low{color:var(--c-low)!important} ========================= */
.has-text-control-mid{color:var(--c-mid)!important} .has-text-prosoft { color: var(--prosoft-inv) !important; }
.has-text-control-high{color:var(--c-high)!important} .has-background-prosoft { background: var(--prosoft-normal) !important; color: var(--prosoft-inv) !important; }
.has-text-control-veryhigh{color:var(--c-veryhigh)!important}
.has-background-control-verylow{background:var(--c-verylow)!important;color:var(--c-verylow-inv)!important} .has-background-prosoft a,
.has-background-control-low{background:var(--c-low)!important;color:var(--c-low-inv)!important} .has-background-prosoft abbr { color: var(--prosoft-inv); }
.has-background-control-mid{background:var(--c-mid)!important;color:var(--c-mid-inv)!important}
.has-background-control-high{background:var(--c-high)!important;color:var(--c-high-inv)!important}
.has-background-control-veryhigh{background:var(--c-veryhigh)!important;color:var(--c-veryhigh-inv)!important}
/* Buttons */ abbr { text-decoration: none; }
.button.is-control-verylow{background:var(--c-verylow);border-color:transparent;color:var(--c-verylow-inv)}
.button.is-control-low{background:var(--c-low);border-color:transparent;color:var(--c-low-inv)}
.button.is-control-mid{background:var(--c-mid);border-color:transparent;color:var(--c-mid-inv)}
.button.is-control-high{background:var(--c-high);border-color:transparent;color:var(--c-high-inv)}
.button.is-control-veryhigh{background:var(--c-veryhigh);border-color:transparent;color:var(--c-veryhigh-inv)}
.button.is-control-verylow:hover{filter:brightness(.92)}
.button.is-control-low:hover{filter:brightness(.92)}
.button.is-control-mid:hover{filter:brightness(.92)}
.button.is-control-high:hover{filter:brightness(.92)}
.button.is-control-veryhigh:hover{filter:brightness(.92)}
.button.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)}
.button.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)}
.button.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)}
.button.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)}
.button.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)}
/* Tags */ /* =========================
.tag.is-control-verylow{background:var(--c-verylow);color:var(--c-verylow-inv)} Colorized Text / Background
.tag.is-control-low{background:var(--c-low);color:var(--c-low-inv)} ========================= */
.tag.is-control-mid{background:var(--c-mid);color:var(--c-mid-inv)} .has-text-control-verylow { color: var(--c-verylow) !important; }
.tag.is-control-high{background:var(--c-high);color:var(--c-high-inv)} .has-text-control-low { color: var(--c-low) !important; }
.tag.is-control-veryhigh{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)} .has-text-control-mid { color: var(--c-mid) !important; }
.tag.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)} .has-text-control-high { color: var(--c-high) !important; }
.tag.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)} .has-text-control-veryhigh{ color: var(--c-veryhigh) !important; }
.tag.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)}
.tag.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)}
.tag.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)}
/* Notifications */ .has-background-control-verylow { background: var(--c-verylow) !important; color: var(--c-verylow-inv) !important; }
.notification.is-control-verylow{background:var(--c-verylow-100);border-left:4px solid var(--c-verylow);color:#111} .has-background-control-low { background: var(--c-low) !important; color: var(--c-low-inv) !important; }
.notification.is-control-low{background:var(--c-low-100);border-left:4px solid var(--c-low);color:#111} .has-background-control-mid { background: var(--c-mid) !important; color: var(--c-mid-inv) !important; }
.notification.is-control-mid{background:var(--c-mid-100);border-left:4px solid var(--c-mid);color:#111} .has-background-control-high { background: var(--c-high) !important; color: var(--c-high-inv) !important; }
.notification.is-control-high{background:var(--c-high-100);border-left:4px solid var(--c-high);color:#111} .has-background-control-veryhigh{ background: var(--c-veryhigh) !important; color: var(--c-veryhigh-inv) !important; }
.notification.is-control-veryhigh{background:var(--c-veryhigh-100);border-left:4px solid var(--c-veryhigh);color:#111}
/* Messages */ /* =========================
.message.is-control-verylow .message-header{background:var(--c-verylow);color:var(--c-verylow-inv)} Buttons
.message.is-control-low .message-header{background:var(--c-low);color:var(--c-low-inv)} ========================= */
.message.is-control-mid .message-header{background:var(--c-mid);color:var(--c-mid-inv)} .button.is-prosoft { background: var(--prosoft-normal) !important; color: var(--prosoft-inv) !important; }
.message.is-control-high .message-header{background:var(--c-high);color:var(--c-high-inv)}
.message.is-control-veryhigh .message-header{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)}
.message.is-control-verylow .message-body{border-color:var(--c-verylow-300)}
.message.is-control-low .message-body{border-color:var(--c-low-300)}
.message.is-control-mid .message-body{border-color:var(--c-mid-300)}
.message.is-control-high .message-body{border-color:var(--c-high-300)}
.message.is-control-veryhigh .message-body{border-color:var(--c-veryhigh-300)}
/* Progress (optional) */ .button.is-control-verylow,
.progress.is-control-verylow::-webkit-progress-value{background:var(--c-verylow)} .button.is-control-low,
.progress.is-control-low::-webkit-progress-value{background:var(--c-low)} .button.is-control-mid,
.progress.is-control-mid::-webkit-progress-value{background:var(--c-mid)} .button.is-control-high,
.progress.is-control-high::-webkit-progress-value{background:var(--c-high)} .button.is-control-veryhigh {
.progress.is-control-veryhigh::-webkit-progress-value{background:var(--c-veryhigh)} border-color: transparent;
.progress.is-control-verylow::-moz-progress-bar{background:var(--c-verylow)} }
.progress.is-control-low::-moz-progress-bar{background:var(--c-low)} .button.is-control-verylow { background: var(--c-verylow); color: var(--c-verylow-inv); }
.progress.is-control-mid::-moz-progress-bar{background:var(--c-mid)} .button.is-control-low { background: var(--c-low); color: var(--c-low-inv); }
.progress.is-control-high::-moz-progress-bar{background:var(--c-high)} .button.is-control-mid { background: var(--c-mid); color: var(--c-mid-inv); }
.progress.is-control-veryhigh::-moz-progress-bar{background:var(--c-veryhigh)} .button.is-control-high { background: var(--c-high); color: var(--c-high-inv); }
.button.is-control-veryhigh{ background: var(--c-veryhigh);color: var(--c-veryhigh-inv); }
abbr { .button.is-control-verylow:hover,
text-decoration: none; .button.is-control-low:hover,
.button.is-control-mid:hover,
.button.is-control-high:hover,
.button.is-control-veryhigh:hover {
filter: brightness(.92);
} }
/* Topbar-Farbe erzwingen (Bulma überschreibt sonst mit weiß) */ .button.is-control-verylow.is-light { background: var(--c-verylow-100); color: var(--c-verylow); }
.button.is-control-low.is-light { background: var(--c-low-100); color: var(--c-low); }
.button.is-control-mid.is-light { background: var(--c-mid-100); color: var(--c-mid); }
.button.is-control-high.is-light { background: var(--c-high-100); color: var(--c-high); }
.button.is-control-veryhigh.is-light{ background: var(--c-veryhigh-100);color: var(--c-veryhigh); }
/* =========================
Tags
========================= */
.tag.is-prosoft { background: var(--prosoft-normal) !important; color: var(--prosoft-inv) !important; }
.tag.is-control-verylow { background: var(--c-verylow); color: var(--c-verylow-inv); }
.tag.is-control-low { background: var(--c-low); color: var(--c-low-inv); }
.tag.is-control-mid { background: var(--c-mid); color: var(--c-mid-inv); }
.tag.is-control-high { background: var(--c-high); color: var(--c-high-inv); }
.tag.is-control-veryhigh{ background: var(--c-veryhigh);color: var(--c-veryhigh-inv); }
.tag.is-control-verylow.is-light { background: var(--c-verylow-100); color: var(--c-verylow); }
.tag.is-control-low.is-light { background: var(--c-low-100); color: var(--c-low); }
.tag.is-control-mid.is-light { background: var(--c-mid-100); color: var(--c-mid); }
.tag.is-control-high.is-light { background: var(--c-high-100); color: var(--c-high); }
.tag.is-control-veryhigh.is-light{ background: var(--c-veryhigh-100);color: var(--c-veryhigh); }
/* =========================
Notifications
========================= */
.notification.is-control-verylow { background: var(--c-verylow-100); border-left: 4px solid var(--c-verylow); color:#111; }
.notification.is-control-low { background: var(--c-low-100); border-left: 4px solid var(--c-low); color:#111; }
.notification.is-control-mid { background: var(--c-mid-100); border-left: 4px solid var(--c-mid); color:#111; }
.notification.is-control-high { background: var(--c-high-100); border-left: 4px solid var(--c-high); color:#111; }
.notification.is-control-veryhigh{ background: var(--c-veryhigh-100);border-left: 4px solid var(--c-veryhigh);color:#111; }
.notification.is-prosoft { background: var(--prosoft-normal) !important; border-left: var(--prosoft-inv) !important; }
/* =========================
Messages
========================= */
.message.is-control-verylow .message-header { background: var(--c-verylow); color: var(--c-verylow-inv); }
.message.is-control-low .message-header { background: var(--c-low); color: var(--c-low-inv); }
.message.is-control-mid .message-header { background: var(--c-mid); color: var(--c-mid-inv); }
.message.is-control-high .message-header { background: var(--c-high); color: var(--c-high-inv); }
.message.is-control-veryhigh .message-header{ background: var(--c-veryhigh);color: var(--c-veryhigh-inv); }
.message.is-control-verylow .message-body { border-color: var(--c-verylow-300); }
.message.is-control-low .message-body { border-color: var(--c-low-300); }
.message.is-control-mid .message-body { border-color: var(--c-mid-300); }
.message.is-control-high .message-body { border-color: var(--c-high-300); }
.message.is-control-veryhigh .message-body{ border-color: var(--c-veryhigh-300); }
.message.is-prosoft { border-color: var(--prosoft-normal); }
/* =========================
Progress bars
========================= */
.progress.is-control-verylow::-webkit-progress-value { background: var(--c-verylow); }
.progress.is-control-low::-webkit-progress-value { background: var(--c-low); }
.progress.is-control-mid::-webkit-progress-value { background: var(--c-mid); }
.progress.is-control-high::-webkit-progress-value { background: var(--c-high); }
.progress.is-control-veryhigh::-webkit-progress-value{ background: var(--c-veryhigh); }
.progress.is-control-verylow::-moz-progress-bar { background: var(--c-verylow); }
.progress.is-control-low::-moz-progress-bar { background: var(--c-low); }
.progress.is-control-mid::-moz-progress-bar { background: var(--c-mid); }
.progress.is-control-high::-moz-progress-bar { background: var(--c-high); }
.progress.is-control-veryhigh::-moz-progress-bar{ background: var(--c-veryhigh); }
.progress.is-prosoft { background: var(--prosoft-normal) !important; }
/* =========================
Navbar / Topbar
========================= */
.navbar.topbar-nav { .navbar.topbar-nav {
background-color: #d6801e !important; /* Orange wie im Screenshot */ background-color: #d6801e !important; /* Orange as in screenshot */
box-shadow: none; box-shadow: none;
} }
/* Textfarben in der Topbar */
.navbar.topbar-nav .navbar-item, .navbar.topbar-nav .navbar-item,
.navbar.topbar-nav .navbar-link { .navbar.topbar-nav .navbar-link { color: #111; }
color: #111;
}
.navbar.topbar-nav .navbar-item:hover, .navbar.topbar-nav .navbar-item:hover,
.navbar.topbar-nav .navbar-link:hover { .navbar.topbar-nav .navbar-link:hover {
background-color: rgba(0,0,0,.04); background-color: rgba(0,0,0,.04);
color: #111; color: #111;
} }
.navbar.topbar-nav .navbar-link::after { border-color: #6b2bbd; } /* purple arrow */
/* Dropdown-Pfeil in lila */ /* Logo block */
.navbar.topbar-nav .navbar-link::after { .logo { background-color: var(--prosoft-normal) !important; color: #fff; }
border-color: #6b2bbd; /* lila */ .logo .logo-text { color: #fff; font-weight: 700; }
}
/* Lila Logo-Kachel links */ /* =========================
.logo { Layout Elements
background: #5a2a82 !important; ========================= */
color: #fff;
}
.logo .logo-text {
color: #fff;
font-weight: 700;
}
/* Rechte Seite: Suche + Profil */
.actions { display: flex; align-items: center; gap: 10px; padding-right: 10px; } .actions { display: flex; align-items: center; gap: 10px; padding-right: 10px; }
.search { border: 1px solid #c7c7c7; border-radius: 4px; padding: 4px 8px; font-size: 14px; } .search { border: 1px solid #c7c7c7; border-radius: 4px; padding: 4px 8px; font-size: 14px; }
.profile { background: #3c7d74; color: #fff; width: 28px; height: 28px; border-radius: 9999px; .profile { background: #3c7d74; color: #fff; width: 28px; height: 28px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-weight: 700; }
display: flex; align-items: center; justify-content: center; font-weight: 700; }
/* Inhalt darunter */
.content { background: #fafafa; min-height: calc(100vh - 3.25rem); } .content { background: #fafafa; min-height: calc(100vh - 3.25rem); }
.home-icon { font-size: 20px; display: inline-block; margin: 10px; } .home-icon { font-size: 20px; display: inline-block; margin: 10px; }
/* Dropdown optisch näher am Screenshot */
.navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); } .navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); }
/* Breadcrumbs */ /* =========================
Breadcrumbs
========================= */
.top-breadcrumb { padding: 10px 0; } .top-breadcrumb { padding: 10px 0; }
.breadcrumb:not(:last-child) { margin-bottom: 0; border-bottom: 1px solid var(--prosoft-normal); }
.breadcrumb { background-color: #f0ebeb; }
.breadcrumb a { color: var(--prosoft-normal) !important; }
.breadcrumb { /* =========================
margin-bottom: 20px; Lists inside .content
background-color: #f0ebeb; ========================= */
} .content li { margin-top: 5px !important; }
.content li+li { margin: 0 !important; }
.content li{ /* =========================
margin-top: 5px !important; Dark Mode
} ========================= */
body.dark-mode { background-color: #121212; color: #f5f5f5; }
body.dark-mode .box { background-color: #1e1e1e; color: #f5f5f5; }
body.dark-mode a { color: #bb86fc; }
.content li+li { /* =========================
margin: 0 !important; Risk Chip (custom widget)
} ========================= */
/* 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;
}
/* Ticket-Button (ID links, Text rechts) */
.risk-chip { .risk-chip {
--chip-w: 260px; --chip-w: 260px;
--chip-id-w: 40px; --chip-id-w: 40px;
width: var(--chip-w); width: var(--chip-w);
display: inline-flex; display: inline-flex;
flex-direction: column; /* stacked style */
align-items: stretch; align-items: stretch;
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
@ -180,12 +201,6 @@ body.dark-mode a {
background: var(--chip-bg, #eee); background: var(--chip-bg, #eee);
color: var(--chip-fg, #111); color: var(--chip-fg, #111);
} }
.risk-chip{
display:inline-flex;
flex-direction:column; /* <— neu */
}
.risk-chip .chip-head { .risk-chip .chip-head {
padding:.35rem .6rem; padding:.35rem .6rem;
font-size:.75rem; font-size:.75rem;
@ -196,13 +211,9 @@ body.dark-mode a {
background:rgba(0,0,0,.10); background:rgba(0,0,0,.10);
border-bottom:1px solid rgba(0,0,0,.12); border-bottom:1px solid rgba(0,0,0,.12);
} }
.risk-chip .chip-main { display:flex; align-items:stretch; }
.risk-chip .chip-main{ /* left ID column with texture */
display:flex;
align-items:stretch;
}
/* linke ID-Spalte mit leichter Textur */
.risk-chip .chip-id { .risk-chip .chip-id {
flex: 0 0 var(--chip-id-w); flex: 0 0 var(--chip-id-w);
display: grid; display: grid;
@ -221,7 +232,7 @@ body.dark-mode a {
opacity:.6; opacity:.6;
} }
/* rechte Text-Spalte */ /* right text column */
.risk-chip .chip-label { .risk-chip .chip-label {
flex: 1 1 auto; flex: 1 1 auto;
padding: .5rem .75rem; padding: .5rem .75rem;
@ -232,49 +243,61 @@ body.dark-mode a {
min-height: 2.25rem; min-height: 2.25rem;
} }
/* Farbzuteilung aus deinen Custom-Klassen */ /* Color assignments for chip */
.risk-chip.is-control-verylow { --chip-bg: var(--c-verylow); --chip-fg: var(--c-verylow-inv); } .risk-chip.is-control-verylow { --chip-bg: var(--c-verylow); --chip-fg: var(--c-verylow-inv); }
.risk-chip.is-control-low { --chip-bg: var(--c-low); --chip-fg: var(--c-low-inv); } .risk-chip.is-control-low { --chip-bg: var(--c-low); --chip-fg: var(--c-low-inv); }
.risk-chip.is-control-mid { --chip-bg: var(--c-mid); --chip-fg: var(--c-mid-inv); } .risk-chip.is-control-mid { --chip-bg: var(--c-mid); --chip-fg: var(--c-mid-inv); }
.risk-chip.is-control-high { --chip-bg: var(--c-high); --chip-fg: var(--c-high-inv); } .risk-chip.is-control-high { --chip-bg: var(--c-high); --chip-fg: var(--c-high-inv); }
.risk-chip.is-control-veryhigh{ --chip-bg: var(--c-veryhigh); --chip-fg: var(--c-veryhigh-inv); } .risk-chip.is-control-veryhigh{ --chip-bg: var(--c-veryhigh); --chip-fg: var(--c-veryhigh-inv); }
/* Responsiv: auf schmalen Screens volle Breite */ /* Responsive */
@media (max-width: 480px){ @media (max-width: 480px) { .risk-chip{ width: 100%; } }
.risk-chip{ width: 100%; } @media (max-width: 1215px){ .risk-chip{ --chip-w: 100%; width: var(--chip-w); } }
}
@media (max-width: 1215px) {
.risk-chip { --chip-w: 100%; width: var(--chip-w); }
}
/* Container für Avatar + Badge */
.avatar-wrap {
position: relative;
display: inline-block;
}
/* =========================
Avatar with badge
========================= */
.avatar-wrap { position: relative; display: inline-block; }
.avatar-wrap .badge { .avatar-wrap .badge {
position: absolute; position: absolute;
top: -0.35rem; top: -0.35rem; right: -0.35rem;
right: -0.35rem; min-width: 1.25rem; height: 1.25rem;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 .25rem; padding: 0 .25rem;
font-size: 0.75rem; font-size: 0.75rem; line-height: 1.25rem;
line-height: 1.25rem; display: flex; align-items: center; justify-content: center;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px #fff; box-shadow: 0 0 0 2px #fff;
} }
.avatar-wrap .tag.is-medium + .badge { .avatar-wrap .tag.is-medium + .badge {
min-width: 1.15rem; min-width: 1.15rem; height: 1.15rem;
height: 1.15rem; font-size: 0.70rem; line-height: 1.15rem;
font-size: 0.70rem; }
line-height: 1.15rem; /* Dark navbar badge shadow */
.navbar.is-dark .avatar-wrap .badge { box-shadow: 0 0 0 2px hsl(229, 53%, 18%); }
/* =========================
Bulma Design Changes
========================= */
/* ERP-style horizontal tabs */
.erp-tabs {
display: flex;
border-bottom: 2px solid var(--prosoft-normal);
margin-bottom: 1rem;
} }
/* Dark-Mode/ .erp-tabs a {
.navbar.is-dark .avatar-wrap .badge { box-shadow: 0 0 0 2px hsl(229, 53%, 18%); } padding: 0.5rem 1rem;
color: #111;
text-decoration: none;
font-weight: 500;
border-bottom: 3px solid transparent;
}
.erp-tabs a.is-active {
color: var(--prosoft-normal);
border-color: var(--prosoft-normal);
}
.erp-tabs a:hover {
background: rgba(0,0,0,.03);
}

View file

@ -122,7 +122,7 @@
<li> <li>
<a href="/risks/index"> <a href="/risks/index">
<span class="icon is-small"> <span class="icon is-small">
<i class="fas fa-home" aria-hidden="true" style="color: #6b2bbd"></i> <i class="fas fa-home" aria-hidden="true" style="color: #6f3165"></i>
</span> </span>
</a> </a>
</li> </li>

View file

@ -1,95 +1,129 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n risk_extras %} {% load i18n risk_extras %}
{% block crumbs %}
<li><a href="{% url 'risks:index' %}">{% trans "Dashboard" %}</a></li>
{% endblock %}
{% block content %} {% block content %}
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero is-small is-bold"> <section class="hero is-small has-background-prosoft">
<div class="hero-body"> <div class="hero-body">
<div class="container"> <div class="container">
<h1 class="title is-1 has-text-black"> <h2 class="subtitle is-5 has-text-white">
{% trans "Dashboard" %}
</h1>
<h2 class="subtitle is-4 has-text-black">
{% trans "Overview of Risks, Controls and Incidents" %} {% trans "Overview of Risks, Controls and Incidents" %}
</h2> </h2>
</div> </div>
</div> </div>
</section> </section><!-- Hero Section End -->
<section class="section"> <section class="section">
<div class="container"> <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>
<!-- Restrisiken --> <!-- KPI Cards -->
<div class="columns is-multiline">
<!-- Total Risks -->
<div class="column is-one-quarter">
<div class="box has-text-centered">
<p class="heading">{% trans "Total Risks" %}</p>
<p class="title is-4">{{ risks_total }}</p>
</div>
</div><!-- Total Risks End -->
<!-- Residual Risks -->
<div class="column is-one-quarter"> <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 %}"> <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 class="heading">{% trans "Residual Risks Needing Review" %}</p>
<p>{% trans "Residual Risks Needing Review" %}</p> <p class="title is-4">{{ residual_review_required }}</p>
</div>
</div> </div>
</div><!-- Residual Risks End -->
<!-- Ungelesene Notifications --> <!-- Unread Notifications -->
<div class="column is-one-quarter"> <div class="column is-one-quarter">
<div class="box has-background-control-mid has-text-centered"> <div class="box has-text-centered">
<h2 class="title is-4">{{ notifications_unread }}</h2> <p class="heading">{% trans "Unread Notifications" %}</p>
<p>{% trans "Unread Notifications" %}</p> <p class="title is-4">{{ notifications_unread }}</p>
</div>
</div>
</div> </div>
</div><!-- Unread Notifications End -->
<!-- Controls by Status --> </div><!-- KPI Cards End -->
<div class="box">
<h2 class="subtitle">{% trans "Controls by Status" %}</h2>
<ul>
{% for row in controls_by_status %}
<li>{{ row.status|control_status_label }}: {{ row.count }}</li>
{% endfor %}
</ul>
</div>
<!-- Incidents by Status -->
<div class="box">
<h2 class="subtitle">{% trans "Incidents by Status" %}</h2>
<ul>
{% for row in incidents_status %}
<li>{{ row.status|incident_status_label }}: {{ row.count }}</li>
{% endfor %}
</ul>
</div>
<!-- Risks by CIA --> <!-- Risks by CIA -->
<div class="box"> <div class="box">
<h2 class="subtitle">{% trans "Risks by CIA" %}</h2> <h2 class="title is-5">{% trans "Risks by CIA" %}</h2>
<div class="columns is-multiline"> <div class="columns is-multiline">
{% for cia, count in risks_by_cia.items %} <div class="column">
<div class="column is-one-quarter"> <div class="notification is-control-verylow has-text-centered">
{% if cia == '1' %} <strong>{% trans "Confidentiality" %}</strong><br>
<div class="box has-background-control-verylow has-text-centered"> {{ risks_by_cia.1|default:0 }}
<h3 class="title is-5">{% trans "Confidentiality" %}</h3>
<p>{{ count }} {% trans "Risks" %}</p>
</div> </div>
{% elif cia == '2' %}
<div class="box has-background-control-mid has-text-centered">
<h3 class="title is-5">{% trans "Integrity" %}</h3>
<p>{{ count }} {% trans "Risks" %}</p>
</div> </div>
{% elif cia == '3' %} <div class="column">
<div class="box has-background-control-high has-text-centered"> <div class="notification is-control-mid has-text-centered">
<h3 class="title is-5">{% trans "Availability" %}</h3> <strong>{% trans "Integrity" %}</strong><br>
<p>{{ count }} {% trans "Risks" %}</p> {{ risks_by_cia.2|default:0 }}
</div> </div>
{% endif %}
</div> </div>
<div class="column">
<div class="notification is-control-high has-text-centered">
<strong>{% trans "Availability" %}</strong><br>
{{ risks_by_cia.3|default:0 }}
</div>
</div>
</div>
</div><!-- Risks by CIA End -->
<!-- Controls by Status -->
<div class="box">
<h2 class="title is-5">{% trans "Controls by Status" %}</h2>
<div class="table-container">
<table class="table is-fullwidth is-narrow is-hoverable">
<thead>
<tr>
<th>{% trans "Status" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
{% for row in controls_by_status %}
<tr>
<td>{{ row.status|control_status_label }}</td>
<td>{{ row.count }}</td>
</tr>
{% empty %}
<tr><td colspan="2" class="has-text-grey has-text-centered">{% trans "No data" %}</td></tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div> </div>
</div><!-- Controls by Status End -->
<!-- Incidents by Status -->
<div class="box">
<h2 class="title is-5">{% trans "Incidents by Status" %}</h2>
<div class="table-container">
<table class="table is-fullwidth is-narrow is-hoverable">
<thead>
<tr>
<th>{% trans "Status" %}</th>
<th>{% trans "Count" %}</th>
</tr>
</thead>
<tbody>
{% for row in incidents_status %}
<tr>
<td>{{ row.status|incident_status_label }}</td>
<td>{{ row.count }}</td>
</tr>
{% empty %}
<tr><td colspan="2" class="has-text-grey has-text-centered">{% trans "No data" %}</td></tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div><!-- Incidents by Status End -->
</div> </div>
</section> </section><!-- Dashboard Content End -->
{% endblock %} {% endblock %}

View file

@ -4,206 +4,156 @@
<li><a href="{% url 'risks:list_risks' %}">{% trans "Risk analysis" %}</a></li> <li><a href="{% url 'risks:list_risks' %}">{% trans "Risk analysis" %}</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<section class="section">
<div class="box">
<h2 class="title is-5">{% trans "Filter" %}</h2>
<!-- Filter --> <!-- Filter Section -->
<section class="section has-background-light py-2">
<form method="get" class="mb-4"> <form method="get" class="mb-4">
<div class="columns is-multiline"> <div class="columns is-multiline is-vcentered">
<!-- Filter: Risk -->
<div class="column is-2"> <div class="column is-2">
<label class="label is-small">{% trans "Risks" %}</label>
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select name="risk" onchange="this.form.submit()"> <select name="risk" onchange="this.form.submit()">
<option value="">{% trans "Risk" %}</option> <option value="">{% trans "All" %}</option>
{% for r in risks %} {% for r in risk_choices %}
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}> <option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
{{ r.title }} {{ r.title }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div><!-- Filter: Risk End -->
<div class="column is-2">
<div class="select is-small is-fullwidth">
<select name="control" onchange="this.form.submit()">
<option value="">{% trans "Controls" %}</option>
{% for c in controls %}
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
{{ c.title }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="column is-2">
<div class="select is-small is-fullwidth">
<select name="category" onchange="this.form.submit()">
<option value="">{% trans "Category" %}</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if request.GET.category == cat|stringformat:"s" %}selected{% endif %}>
{{ cat }}
</option>
{% endfor %}
</select>
</div>
</div>
<!-- Filter: Asset -->
<div class="column is-2"> <div class="column is-2">
<label class="label is-small">{% trans "Assets" %}</label>
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select name="asset" onchange="this.form.submit()"> <select name="asset" onchange="this.form.submit()">
<option value="">{% trans "Asset" %}</option> <option value="">{% trans "All" %}</option>
{% for a in assets %} {% for a in asset_choices %}
<option value="{{ a }}" {% if request.GET.asset == a|stringformat:"s" %}selected{% endif %}> <option value="{{ a }}" {% if request.GET.asset == a|stringformat:"s" %}selected{% endif %}>
{{ a }} {{ a }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div><!-- Filter: Asset End -->
<!-- Filter: Category -->
<div class="column is-2"> <div class="column is-2">
<label class="label is-small">{% trans "Categories" %}</label>
<div class="select is-small is-fullwidth">
<select name="category" onchange="this.form.submit()">
<option value="">{% trans "All" %}</option>
{% for cat in category_choices %}
<option value="{{ cat }}" {% if request.GET.category == cat|stringformat:"s" %}selected{% endif %}>
{{ cat }}
</option>
{% endfor %}
</select>
</div>
</div><!-- Filter: Category End -->
<!-- Filter: Process -->
<div class="column is-2">
<label class="label is-small">{% trans "Processes" %}</label>
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select name="process" onchange="this.form.submit()"> <select name="process" onchange="this.form.submit()">
<option value="">{% trans "Process" %}</option> <option value="">{% trans "All" %}</option>
{% for p in processes %} {% for p in process_choices %}
<option value="{{ p }}" {% if request.GET.process == p|stringformat:"s" %}selected{% endif %}> <option value="{{ p }}" {% if request.GET.process == p|stringformat:"s" %}selected{% endif %}>
{{ p }} {{ p }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div><!-- Filter: Process End -->
<!-- Filter: Owner -->
<div class="column is-2"> <div class="column is-2">
<label class="label is-small">{% trans "Owners" %}</label>
<div class="select is-small is-fullwidth"> <div class="select is-small is-fullwidth">
<select name="owner" onchange="this.form.submit()"> <select name="owner" onchange="this.form.submit()">
<option value="">{% trans "Owner" %}</option> <option value="">{% trans "All" %}</option>
{% for u in owners %} {% for u in owner_choices %}
<option value="{{ u.id }}" {% if request.GET.owner == u.id|stringformat:"s" %}selected{% endif %}> <option value="{{ u.id }}" {% if request.GET.owner == u.id|stringformat:"s" %}selected{% endif %}>
{{ u.get_full_name|default:u.username }} {{ u.get_full_name|default:u.username }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div><!-- Filter: Owner End -->
</div> <!-- Filter: Reset -->
</form> <!-- Filter Ende --> <div class="column is-2">
<label class="label is-small">&nbsp;</label>
<div class="control">
<h2 class="title is-5">{% trans "Risks" %}</h2> <a href="{% url 'risks:list_risks' %}" class="button is-small is-light is-fullwidth">
<!-- Risiken --> <span class="icon"><i class="fas fa-undo"></i></span>
<div class="table-container"> <span>{% trans "Reset filters" %}</span>
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
{% if request.user.is_staff %}
<th rowspan="2" class="has-text-centered">
<a class="icon has-text-success" href="{% url 'admin:risks_risk_add' %}" title="Risiko hinzufügen">
<i class="fas fa-add"></i>
</a> </a>
</div>
</div><!-- Filter: Reset End -->
</div>
</form>
</section><!-- Filter Section End -->
<!-- Risk Table -->
<div class="table-container">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead>
<tr class="has-background-prosoft">
<th class="has-text-centered">
<a href="{% sort_url request 'id' current_sort current_dir %}">{% trans "No." %}</a>
</th> </th>
{% endif %} <th class="has-text-centered"><abbr title="{% trans 'Confidentiality' %}">C</abbr></th>
<th rowspan="2" class="has-text-centered">{% trans "Risk" %}</th> <th class="has-text-centered"><abbr title="{% trans 'Integrity' %}">I</abbr></th>
<th rowspan="2" class="has-text-centered">{% trans "Asset / Process" %}</th> <th class="has-text-centered"><abbr title="{% trans 'Availability' %}">A</abbr></th>
<th rowspan="2" class="has-text-centered">{% trans "Category" %}</th> <th class="has-text-centered">
<th rowspan="2" class="has-text-centered">{% trans "Risk Owner" %}</th> <a href="{% sort_url request 'title' current_sort current_dir %}">{% trans "Risk" %}</a>
<th colspan="4" class="has-text-centered has-background-light">{% trans "Gross Risk" %}</th> </th>
<th colspan="4" class="has-text-centered has-background-info-light">{% trans "Net Risk" %}</th> <th class="has-text-centered">
</tr> <a href="{% sort_url request 'asset' current_sort current_dir %}">{% trans "Asset" %}</a>
<tr> </th>
<th class="has-text-centered has-background-light">{% trans "Likelihood" %}</th> <th class="has-text-centered">
<th class="has-text-centered has-background-light">{% trans "Impact" %}</th> <a href="{% sort_url request 'category' current_sort current_dir %}">{% trans "Category" %}</a>
<th class="has-text-centered has-background-light">{% trans "Score" %}</th> </th>
<th class="has-text-centered has-background-light">{% trans "Level" %}</th> <th class="has-text-centered">
<th class="has-text-centered has-background-info-light">{% trans "Likelihood" %}</th> <a href="{% sort_url request 'process' current_sort current_dir %}">{% trans "Process" %}</a>
<th class="has-text-centered has-background-info-light">{% trans "Impact" %}</th> </th>
<th class="has-text-centered has-background-info-light">{% trans "Score" %}</th> <th class="has-text-centered">
<th class="has-text-centered has-background-info-light">{% trans "Level" %}</th> <a href="{% sort_url request 'status' current_sort current_dir %}">{% trans "Status" %}</a>
</th>
<th class="has-text-centered has-text-prosoft">{% trans "Created" %}</th>
<th class="has-text-centered has-text-prosoft">{% trans "by" %}</th>
<th class="has-text-centered has-text-prosoft">{% trans "Changed" %}</th>
<th class="has-text-centered has-text-prosoft">{% trans "by" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for r in risks %} {% for r in risks %}
<tr> <tr onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
{% if request.user.is_staff %} <td>{{ r.id }}</td>
<td class="has-text-centered"> <td class="has-text-centered"><input type="checkbox" disabled {% if "1" in r.cia %}checked{% endif %}></td>
<a class="icon has-text-warning" href="{% url 'admin:risks_risk_change' r.id %}" title="Risiko bearbeiten"> <td class="has-text-centered"><input type="checkbox" disabled {% if "2" in r.cia %}checked{% endif %}></td>
<i class="fas fa-edit"></i> <td class="has-text-centered"><input type="checkbox" disabled {% if "3" in r.cia %}checked{% endif %}></td>
</a> <td>{{ r.title }}</td>
</td> <td>{{ r.asset }}</td>
{% endif %} <td>{{ r.category }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.title }}</td> <td>{{ r.process }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;"> <td>{{ r.get_status_display }}</td>
{{ r.asset }} <td>{{ r.created_at|date:"d.m.Y H:i" }}</td>
{% if r.process %} <td>{{ r.owner|user_display }}</td>
<br><small>{{ r.process }}</small> <td>{{ r.updated_at|date:"d.m.Y H:i" }}</td>
{% endif %} <td>{{ r.owner|user_display }}</td>
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.category }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
{% if r.owner %}
{{ r.owner|user_display }}
{% else %}
{% endif %}
</td>
<!-- Brutto Risiko -->
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.likelihood|likelihood_class|to_bg }}" style="cursor:pointer;">
<abbr title="{{ r.likelihood|likelihood_id_label }}">{{ r.likelihood }}</abbr>
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.impact|impact_class|to_bg }}" style="cursor:pointer;">
<abbr title="{{ r.impact|impact_id_label }}">{{ r.impact }}</abbr>
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.score|score_class|to_bg }}" style="cursor:pointer;">
{{ r.score }} / 20
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.level|level_class|to_bg }}" style="cursor:pointer;">
{{ r.level }}
</td>
<!-- Netto Risiko -->
{% if r.residual_risk %}
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'"
class="has-text-centered {{ r.residual_risk.likelihood|likelihood_class|to_bg }}"
style="cursor:pointer;">
<abbr title="{{ r.residual_risk.likelihood|likelihood_id_label }}">
{{ r.residual_risk.likelihood }}
</abbr>
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'"
class="has-text-centered {{ r.residual_risk.impact|impact_class|to_bg }}"
style="cursor:pointer;">
<abbr title="{{ r.residual_risk.impact|impact_id_label }}">
{{ r.residual_risk.impact }}
</abbr>
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'"
class="has-text-centered {{ r.residual_risk.score|score_class|to_bg }}"
style="cursor:pointer;">
{{ r.residual_risk.score }} / 20
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'"
class="has-text-centered {{ r.residual_risk.level|level_class|to_bg }}"
style="cursor:pointer;">
{{ r.residual_risk.level }}
</td>
{% else %}
<td colspan="4" class="has-text-centered has-text-grey">
{% trans "No residual risk defined" %}
</td>
{% endif %}
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr><td colspan="13" class="has-text-grey has-text-centered">{% trans "No data" %}</td></tr>
<td colspan="8" class="has-text-centered has-text-grey">{% trans "No risks present" %}</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> <!-- Ende Risiken --> </div><!-- Risk Table End -->
</div>
</section>
{% endblock %} {% endblock %}

View file

@ -4,39 +4,43 @@
<li><a href="{% url 'risks:risk_matrix' %}">{% trans "Risk Matrix" %}</a></li> <li><a href="{% url 'risks:risk_matrix' %}">{% trans "Risk Matrix" %}</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<section class="section"> <div class="erp-tabs">
<a class="is-active" data-tab="matrix">{% trans "Risk Matrix" %}</a>
<!-- Tabs --> <a data-tab="details">{% trans "Detail View" %}</a>
<div class="tabs is-boxed" role="tablist">
<ul>
<li class="is-active" data-tab="riskmatrix" role="tab" aria-selected="true"><a>{% trans "Risk Matrix" %}</a></li>
<li data-tab="details" role="tab" aria-selected="false"><a>{% trans "Detail View" %}</a></li>
</ul>
</div> </div>
<div class="box"> <section class="section">
<h2 class="title is-4">{% trans "Risk Matrix" %}</h2>
{# Panel: Brutto (Score-Matrix) #} <!-- Main Container -->
<div class="tab-panel" data-tab="riskmatrix"> <div class="box">
<table class="table is-bordered has-text-centered risk-matrix-table"> <!-- Panel: Matrix View -->
<div class="tab-panel" data-tab="matrix">
<table class="table is-bordered is-fullwidth has-text-centered risk-matrix-table">
<thead> <thead>
<tr> <tr>
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th> <th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
<th class="py-6 {{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th> <th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i_val, i_label in impacts reversed %} {% for i_val, i_label in impacts reversed %}
<tr> <tr>
<th class="py-6 has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th> <th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
{% with s=i_val|mul:l_val %} {% with s=i_val|mul:l_val %}
<td class="risk-matrix-cell {{ s|score_bg_class }}"> <td class="risk-matrix-cell {{ s|score_bg_class }}">
<div class="is-flex is-justify-content-center is-align-items-center py-6"> <div class="is-flex is-justify-content-center is-align-items-center">
<span class="tag is-light is-rounded">{% trans "Score" %} {{ s }}</span> {% if s <= 4 %}
<span class="tag is-control-verylow is-light">Low ({{ s }})</span>
{% elif s <= 8 %}
<span class="tag is-control-low is-light">Medium ({{ s }})</span>
{% elif s <= 12 %}
<span class="tag is-control-mid is-light">High ({{ s }})</span>
{% else %}
<span class="tag is-control-veryhigh is-light">Critical ({{ s }})</span>
{% endif %}
</div> </div>
</td> </td>
{% endwith %} {% endwith %}
@ -45,10 +49,12 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div><!-- Panel: Matrix View End -->
{# Panel: Details (Listen je Zelle, mit Brutto/Netto-Umschalter) #} <!-- Panel: Details View -->
<div class="tab-panel is-hidden" data-tab="details"> <div class="tab-panel is-hidden" data-tab="details">
<!-- Mode Toggle (Gross / Net) -->
<div class="level mb-3"> <div class="level mb-3">
<div class="level-left"> <div class="level-left">
<div class="level-item"><strong>{% trans "Show" %}:</strong></div> <div class="level-item"><strong>{% trans "Show" %}:</strong></div>
@ -63,29 +69,29 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div><!-- Mode Toggle End -->
{# Brutto-Listen #} <!-- Gross Risk List -->
<div class="details-table" data-mode="gross"> <div class="details-table" data-mode="gross">
<table class="table is-bordered has-text-centered risk-matrix-table"> <table class="table is-bordered is-fullwidth has-text-centered risk-matrix-table">
<thead> <thead>
<tr> <tr>
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th> <th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
<th class="py-6 {{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th> <th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i_val, i_label in impacts reversed %} {% for i_val, i_label in impacts reversed %}
<tr> <tr>
<th class="py-6 has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th> <th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
{% with row=gross_matrix|dict_get:i_val %} {% with row=gross_matrix|dict_get:i_val %}
{% with cell=row|dict_get:l_val %} {% with cell=row|dict_get:l_val %}
{% with s=i_val|mul:l_val %} {% with s=i_val|mul:l_val %}
<td class="risk-matrix-cell {{ s|score_bg_class }}"> <td class="risk-matrix-cell {{ s|score_bg_class }}">
{% if cell and cell|length %} {% if cell %}
<ul class="risk-cell-list"> <ul class="risk-cell-list">
{% for risk in cell %} {% for risk in cell %}
<li style="list-style:none;"> <li style="list-style:none;">
@ -105,29 +111,29 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div><!-- Gross Risk List End -->
{# Netto-Listen #} <!-- Net Risk List -->
<div class="details-table is-hidden" data-mode="net"> <div class="details-table is-hidden" data-mode="net">
<table class="table is-bordered has-text-centered risk-matrix-table"> <table class="table is-bordered is-fullwidth has-text-centered risk-matrix-table">
<thead> <thead>
<tr> <tr>
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th> <th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
<th class="py-6 {{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th> <th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for i_val, i_label in impacts reversed %} {% for i_val, i_label in impacts reversed %}
<tr> <tr>
<th class="py-6 has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th> <th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
{% for l_val, l_label in likelihoods %} {% for l_val, l_label in likelihoods %}
{% with row=net_matrix|dict_get:i_val %} {% with row=net_matrix|dict_get:i_val %}
{% with cell=row|dict_get:l_val %} {% with cell=row|dict_get:l_val %}
{% with s=i_val|mul:l_val %} {% with s=i_val|mul:l_val %}
<td class="risk-matrix-cell {{ s|score_bg_class }}"> <td class="risk-matrix-cell {{ s|score_bg_class }}">
{% if cell and cell|length %} {% if cell %}
<ul class="risk-cell-list"> <ul class="risk-cell-list">
{% for risk in cell %} {% for risk in cell %}
<li style="list-style:none;"> <li style="list-style:none;">
@ -147,39 +153,56 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div><!-- Net Risk List End -->
</div>
</div><!-- Panel: Details View End -->
</div><!-- Main Container End -->
</section><!-- Section End -->
</div>
</section>
<style>
.risk-matrix-table th, .risk-matrix-table td { padding: .5rem; }
.risk-matrix-cell { min-height: 120px; vertical-align: middle; }
.tab-panel.is-hidden { display: none; }
.risk-cell-list { text-align: left; margin: 0; padding-left: 1rem; }
</style>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', () => {
// Tabs // Handle ERP-style tabs
const tabs = document.querySelectorAll('.tabs li[data-tab]'); const tabs = document.querySelectorAll('.erp-tabs a[data-tab]');
const panels = document.querySelectorAll('.tab-panel'); const panels = document.querySelectorAll('.tab-panel');
tabs.forEach(t => t.addEventListener('click', () => {
tabs.forEach(x => { x.classList.remove('is-active'); x.setAttribute('aria-selected','false'); });
t.classList.add('is-active'); t.setAttribute('aria-selected','true');
const target = t.getAttribute('data-tab');
panels.forEach(p => p.classList.toggle('is-hidden', p.getAttribute('data-tab') !== target));
}));
// Umschalter im „Details“-Tab tabs.forEach(tab => {
tab.addEventListener('click', e => {
e.preventDefault();
// Deactivate all
tabs.forEach(x => x.classList.remove('is-active'));
panels.forEach(p => p.classList.add('is-hidden'));
// Activate clicked
tab.classList.add('is-active');
const target = tab.getAttribute('data-tab');
const activePanel = document.querySelector(`.tab-panel[data-tab="${target}"]`);
if (activePanel) activePanel.classList.remove('is-hidden');
});
});
// Handle Gross/Net toggle buttons in "Details" tab
const toggles = document.querySelectorAll('.details-toggle'); const toggles = document.querySelectorAll('.details-toggle');
const tables = document.querySelectorAll('.details-table'); const tables = document.querySelectorAll('.details-table');
toggles.forEach(btn => btn.addEventListener('click', () => {
const mode = btn.getAttribute('data-mode'); // 'gross' | 'net' toggles.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.getAttribute('data-mode'); // "gross" or "net"
// Toggle active state on buttons
toggles.forEach(b => b.classList.toggle('is-active', b === btn)); toggles.forEach(b => b.classList.toggle('is-active', b === btn));
tables.forEach(t => t.classList.toggle('is-hidden', t.getAttribute('data-mode') !== mode));
})); // Show the right table
tables.forEach(t => {
t.classList.toggle('is-hidden', t.getAttribute('data-mode') !== mode);
});
});
});
}); });
</script> </script>
{% endblock %} {% endblock %}