From 9f42cc77ccbd277d4eaa52837075c4bfdb623fa1 Mon Sep 17 00:00:00 2001 From: smueller Date: Fri, 3 Jul 2026 13:44:57 +0200 Subject: [PATCH] Enhance UI and functionality for admin dashboard and user management - Update `_base_context` to include `active_nav` for dynamic navigation highlighting. - Revamp `base.html` to improve header layout and navigation links for better user experience. - Redesign `dashboard.html` to include statistics for new and edited articles, enhancing visibility of content changes. - Improve `users.html` layout for user management, adding detailed user roles and status indicators. - Update CSS styles for a cohesive design across the application, introducing new color variables and layout adjustments. --- app/main.py | 5 + app/services/newsletter.py | 160 +++++--- app/static/css/main.css | 686 +++++++++++++++++++++++++++++++++-- app/templates/base.html | 37 +- app/templates/dashboard.html | 477 ++++++++++++++---------- app/templates/login.html | 13 +- app/templates/users.html | 113 ++++-- 7 files changed, 1168 insertions(+), 323 deletions(-) diff --git a/app/main.py b/app/main.py index fc7ca55..8729996 100644 --- a/app/main.py +++ b/app/main.py @@ -93,10 +93,15 @@ def _bootstrap_admin() -> None: def _base_context(request: Request, user: User, db: Session) -> dict: + path = request.url.path + active_nav = "dashboard" + if path.startswith("/admin/users"): + active_nav = "users" return { "user": user, "csrf_token": request.cookies.get(CSRF_COOKIE_NAME) or ensure_csrf_cookie(request), "smtp": get_smtp_settings(db), + "active_nav": active_nav, } diff --git a/app/services/newsletter.py b/app/services/newsletter.py index 0a8aa7e..dabf124 100644 --- a/app/services/newsletter.py +++ b/app/services/newsletter.py @@ -10,17 +10,24 @@ LINE = "=" * 78 SUBLINE = "-" * 78 FONT = "Arial, Helvetica, sans-serif" +# Thomas-Krenn.AG Corporate Design (Orange + Anthrazit) +COLOR_TK_ORANGE = "#ED6B06" +COLOR_TK_ORANGE_DARK = "#C95605" +COLOR_TK_ORANGE_LIGHT = "#FFF4ED" +COLOR_TK_GRAY_DARK = "#4A4A4A" +COLOR_TK_GRAY = "#6B6B6B" +COLOR_TK_GRAY_LIGHT = "#F5F5F5" COLOR_TEXT = "#1A1A1A" -COLOR_MUTED = "#5C6670" -COLOR_HEADING = "#003366" -COLOR_LINK = "#0055A4" -COLOR_BORDER = "#D7DEE8" -COLOR_BG = "#EEF2F7" +COLOR_MUTED = COLOR_TK_GRAY +COLOR_HEADING = COLOR_TK_GRAY_DARK +COLOR_LINK = COLOR_TK_ORANGE +COLOR_BORDER = "#E0E0E0" +COLOR_BG = COLOR_TK_GRAY_LIGHT COLOR_WHITE = "#FFFFFF" -COLOR_ACCENT_NEW = "#1F7A4D" -COLOR_ACCENT_NEW_BG = "#E8F6EE" -COLOR_ACCENT_EDIT = "#0055A4" -COLOR_ACCENT_EDIT_BG = "#E8F1FA" +COLOR_ACCENT_NEW = COLOR_TK_ORANGE +COLOR_ACCENT_NEW_BG = COLOR_TK_ORANGE_LIGHT +COLOR_ACCENT_EDIT = COLOR_TK_GRAY_DARK +COLOR_ACCENT_EDIT_BG = "#EEEEEE" class DisplayOptions(TypedDict): @@ -204,18 +211,18 @@ def create_html( - + -
  
-

Interner Newsletter

+
+

Interner Newsletter

Thomas-Krenn Wiki Update

-

{escape(ORG_NAME)}

+

{escape(ORG_NAME)}

- +

Liebe Kolleginnen und Kollegen,

@@ -237,7 +244,7 @@ def create_html( {summary} - @@ -258,12 +265,37 @@ def create_html( OUTLOOK_FONT = "Arial, sans-serif" -OUTLOOK_TEXT = "#000000" -OUTLOOK_MUTED = "#666666" -OUTLOOK_LINK = "#0563C1" -OUTLOOK_HEADING = "#003366" -OUTLOOK_BORDER = "#DDDDDD" -OUTLOOK_WHITE = "#FFFFFF" +OUTLOOK_TEXT = COLOR_TEXT +OUTLOOK_MUTED = COLOR_TK_GRAY +OUTLOOK_LINK = COLOR_TK_ORANGE +OUTLOOK_HEADING = COLOR_TK_GRAY_DARK +OUTLOOK_BORDER = COLOR_BORDER +OUTLOOK_WHITE = COLOR_WHITE +OUTLOOK_ORANGE = COLOR_TK_ORANGE +OUTLOOK_GRAY_DARK = COLOR_TK_GRAY_DARK + + +def _outlook_td_attrs( + bg: str = OUTLOOK_WHITE, + *, + colspan: int | None = None, + width: str | None = None, + valign: str | None = None, + extra_style: str = "", +) -> str: + """Tabellen-Zelle mit Outlook-Dunkelmodus-Schutz (data-ogsb/data-ogsc).""" + parts = [ + f'bgcolor="{bg}"', + f'style="background-color:{bg} !important;color:{OUTLOOK_TEXT} !important;{extra_style}"', + f'data-ogsb="{bg}"', + ] + if colspan: + parts.insert(0, f'colspan="{colspan}"') + if width: + parts.append(f'width="{width}"') + if valign: + parts.append(f'valign="{valign}"') + return " ".join(parts) def _outlook_font( @@ -276,14 +308,23 @@ def _outlook_font( inner = escape(text) if bold: inner = f"{inner}" - return f'{inner}' - - -def _outlook_link(title: str, url: str) -> str: return ( - f'' - f'{escape(title)}' - f"" + f'' + f'{inner}' + f"" + ) + + +def _outlook_link(title: str, url: str, *, bold: bool = True) -> str: + weight = "" if bold else "" + weight_end = "" if bold else "" + return ( + f'' + f'' + f'' + f"{weight}{escape(title)}{weight_end}" + f"" ) @@ -303,19 +344,20 @@ def create_outlook_html( if not new_articles and not edited_articles: body_rows = f""" - """ else: body_rows = "\n".join( [ - _outlook_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum."), + _outlook_section("Neue Artikel", new_articles, display, "Keine neuen Artikel im Zeitraum.", kind="new"), _outlook_section( "Bearbeitete Artikel", edited_articles, display, "Keine bearbeiteten Artikel im Zeitraum.", + kind="edited", ), ] ) @@ -331,7 +373,7 @@ def create_outlook_html( total = len(new_articles) + len(edited_articles) summary_row = f""" - @@ -343,41 +385,55 @@ def create_outlook_html( - - + + {subject} + - -
+ Zum Thomas-Krenn-Wiki →
+ {_outlook_font("Im gewählten Zeitraum wurden keine passenden Wiki-Änderungen gefunden.", color=OUTLOOK_MUTED)}
+ {_outlook_font("Zusammenfassung: ", color=OUTLOOK_HEADING, bold=True, size="1")} {_outlook_font(f"{len(new_articles)} neu · {len(edited_articles)} bearbeitet · {total} gesamt", color=OUTLOOK_MUTED, size="1")}
+ +
- + + + -
- {_outlook_font("Interner Newsletter", color="#A9C6E8", size="1")}
+
 
+ {_outlook_font("Interner Newsletter", color=OUTLOOK_ORANGE, size="1")}
{_outlook_font("Thomas-Krenn Wiki Update", color=OUTLOOK_WHITE, size="5", bold=True)}
- {_outlook_font(ORG_NAME, color="#D9E8F8", size="2")} + {_outlook_font(ORG_NAME, color="#CCCCCC", size="2")}
+

{_outlook_font("Liebe Kolleginnen und Kollegen,")}

{_outlook_font(intro)}

{_outlook_font(f"Erstellt am: {created_at}", color=OUTLOOK_MUTED, size="1")}

{category_block} - +
{body_rows} {summary_row} - - @@ -412,10 +468,10 @@ def _outlook_article_rows(items: list[dict[str, Any]], display: DisplayOptions) meta_html = f"
{_outlook_font(meta, color=OUTLOOK_MUTED, size='1')}" rows.append( f""" - - """ @@ -428,15 +484,17 @@ def _outlook_section( items: list[dict[str, Any]], display: DisplayOptions, empty_text: str, + kind: str = "edited", ) -> str: + accent = OUTLOOK_ORANGE if kind == "new" else OUTLOOK_GRAY_DARK header = f""" - """ if not items: return header + f""" - """ @@ -593,7 +651,7 @@ def _html_item_row(idx: int, item: dict[str, Any], display: DisplayOptions, acce diff --git a/app/static/css/main.css b/app/static/css/main.css index 571546f..167fda0 100644 --- a/app/static/css/main.css +++ b/app/static/css/main.css @@ -1,33 +1,659 @@ +:root { + --tk-orange: #ED6B06; + --tk-orange-dark: #C95605; + --tk-orange-light: #FFF4ED; + --tk-gray-dark: #4A4A4A; + --tk-gray: #6B6B6B; + --tk-gray-light: #F5F5F5; + --tk-border: #E0E0E0; + --tk-text: #1A1A1A; + --tk-white: #FFFFFF; + --tk-success: #027A48; + --tk-success-bg: #ECFDF3; + --tk-error: #B42318; + --tk-error-bg: #FEF3F2; + --tk-warning: #B54708; + --tk-warning-bg: #FFFAEB; + --shadow-sm: 0 1px 3px rgba(74, 74, 74, 0.08); + --shadow-md: 0 4px 16px rgba(74, 74, 74, 0.1); + --radius: 8px; + --radius-lg: 12px; +} + * { box-sizing: border-box; } -body { margin: 0; font-family: Arial, sans-serif; background: #f5f7fb; color: #1f2430; } -.topbar { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; background: #0c2c5a; color: #fff; } -.container { max-width: 1100px; margin: 1rem auto; padding: 0 1rem; } -.card { background: #fff; border: 1px solid #dde3ef; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; } -.narrow { max-width: 420px; margin: 2rem auto; } -.form-grid { display: grid; gap: 0.75rem; } -label { display: grid; gap: 0.3rem; font-weight: 600; } -input, button, textarea { font: inherit; padding: 0.55rem; } + +body { + margin: 0; + font-family: Arial, Helvetica, sans-serif; + background: var(--tk-gray-light); + color: var(--tk-text); + line-height: 1.5; + min-height: 100vh; +} + +a { color: var(--tk-orange); text-decoration: none; } +a:hover { color: var(--tk-orange-dark); text-decoration: underline; } + +/* ── Header ── */ +.topbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.85rem 1.5rem; + background: var(--tk-gray-dark); + color: var(--tk-white); + border-bottom: 4px solid var(--tk-orange); +} + +.topbar-brand { + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 0; +} + +.topbar-logo { + width: 36px; + height: 36px; + background: var(--tk-orange); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.1rem; + flex-shrink: 0; +} + +.topbar h1 { + margin: 0; + font-size: 1.05rem; + font-weight: 700; + line-height: 1.2; +} + +.topbar-sub { + margin: 0; + font-size: 0.78rem; + color: #BBBBBB; + font-weight: normal; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.topbar-nav { + display: flex; + gap: 0.25rem; +} + +.topbar-nav a { + color: #DDDDDD; + padding: 0.35rem 0.65rem; + border-radius: 4px; + font-size: 0.88rem; + text-decoration: none; +} + +.topbar-nav a:hover, +.topbar-nav a.active { + background: rgba(255, 255, 255, 0.1); + color: var(--tk-white); + text-decoration: none; +} + +.user-chip { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + color: #CCCCCC; +} + +.topbar .btn-logout { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.35); + color: var(--tk-white); + padding: 0.35rem 0.75rem; + font-size: 0.85rem; +} + +.topbar .btn-logout:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.5); +} + +/* ── Layout ── */ +.container { + max-width: 1140px; + margin: 0 auto; + padding: 1.5rem 1rem 2.5rem; +} + +.page-header { + margin-bottom: 1.5rem; +} + +.page-header h2 { + margin: 0 0 0.35rem; + font-size: 1.65rem; + color: var(--tk-gray-dark); +} + +.page-lead { + margin: 0; + color: var(--tk-gray); + font-size: 0.95rem; +} + +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.stat-card { + background: var(--tk-white); + border: 1px solid var(--tk-border); + border-radius: var(--radius); + padding: 1rem 1.15rem; + box-shadow: var(--shadow-sm); +} + +.stat-card.accent-new { border-left: 4px solid var(--tk-orange); } +.stat-card.accent-edit { border-left: 4px solid var(--tk-gray-dark); } +.stat-card.accent-total { border-left: 4px solid var(--tk-gray); } + +.stat-value { + font-size: 2rem; + font-weight: 700; + line-height: 1.1; + color: var(--tk-gray-dark); +} + +.stat-card.accent-new .stat-value { color: var(--tk-orange); } + +.stat-label { + margin-top: 0.25rem; + font-size: 0.85rem; + color: var(--tk-gray); +} + +.layout-2col { + display: grid; + grid-template-columns: 1fr 280px; + gap: 1rem; + align-items: start; +} + +@media (max-width: 900px) { + .layout-2col { grid-template-columns: 1fr; } + .topbar { flex-wrap: wrap; } +} + +/* ── Cards ── */ +.card { + background: var(--tk-white); + border: 1px solid var(--tk-border); + border-radius: var(--radius-lg); + padding: 1.25rem 1.35rem; + margin-bottom: 1rem; + box-shadow: var(--shadow-sm); +} + +.card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + padding-bottom: 0.85rem; + border-bottom: 1px solid var(--tk-border); +} + +.card-header h2, +.card-header h3, +.card > h2, +.card > h3 { + margin: 0; + color: var(--tk-gray-dark); + font-size: 1.1rem; +} + +.card > h2 { font-size: 1.2rem; margin-bottom: 1rem; } + +.card-desc { + margin: 0.35rem 0 0; + font-size: 0.88rem; + color: var(--tk-gray); + font-weight: normal; +} + +.card-accent-orange { border-top: 3px solid var(--tk-orange); } +.card-accent-gray { border-top: 3px solid var(--tk-gray-dark); } + +.sidebar-card { + background: var(--tk-orange-light); + border: 1px solid #F5D5BC; + border-radius: var(--radius); + padding: 1rem; + margin-bottom: 0.75rem; + font-size: 0.88rem; +} + +.sidebar-card h4 { + margin: 0 0 0.5rem; + font-size: 0.9rem; + color: var(--tk-gray-dark); +} + +.sidebar-card ul { + margin: 0; + padding-left: 1.1rem; + color: var(--tk-gray); +} + +.sidebar-card li { margin-bottom: 0.3rem; } + +.narrow { max-width: 420px; margin: 3rem auto; } + +/* ── Forms ── */ +.form-grid { display: grid; gap: 0.85rem; } + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.85rem; +} + +@media (max-width: 600px) { + .form-row { grid-template-columns: 1fr; } +} + +label { + display: grid; + gap: 0.35rem; + font-weight: 600; + font-size: 0.88rem; + color: var(--tk-gray-dark); +} + +input, select, textarea, button { + font: inherit; +} + +input, select, textarea { + padding: 0.6rem 0.75rem; + border: 1px solid var(--tk-border); + border-radius: 6px; + background: var(--tk-white); + color: var(--tk-text); + transition: border-color 0.15s, box-shadow 0.15s; +} + +input:focus, select:focus, textarea:focus { + outline: none; + border-color: var(--tk-orange); + box-shadow: 0 0 0 3px rgba(237, 107, 6, 0.15); +} + textarea { width: 100%; resize: vertical; } -button { width: fit-content; background: #1b5dbf; color: #fff; border: 0; border-radius: 4px; cursor: pointer; } -.btn-secondary { background: #334155; } -.copy-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-bottom: 0.75rem; } -.copy-status { font-size: 0.92rem; color: #64748b; } -.copy-status.success { color: #027a48; } -.copy-status.error { color: #b42318; } -.newsletter-preview-frame { width: 100%; min-height: 720px; border: 1px solid #D7DEE8; border-radius: 6px; background: #EEF2F7; box-shadow: 0 2px 8px rgba(0, 51, 102, 0.08); } -.card h3 { color: #003366; margin-top: 0; } + +.checkbox { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + cursor: pointer; +} + +.checkbox input[type="checkbox"] { + width: 1rem; + height: 1rem; + accent-color: var(--tk-orange); + cursor: pointer; +} + +.display-options { + border: 1px solid var(--tk-border); + border-radius: var(--radius); + padding: 0.85rem 1rem; + display: grid; + gap: 0.5rem; + background: var(--tk-gray-light); +} + +.display-options legend { + font-weight: 600; + padding: 0 0.35rem; + color: var(--tk-gray-dark); + font-size: 0.88rem; +} + +/* ── Buttons ── */ +button { + width: fit-content; + background: var(--tk-orange); + color: var(--tk-white); + border: 0; + border-radius: 6px; + cursor: pointer; + padding: 0.6rem 1.1rem; + font-weight: 600; + font-size: 0.9rem; + transition: background 0.15s, transform 0.1s; +} + +button:hover { background: var(--tk-orange-dark); } +button:active { transform: scale(0.98); } + +.btn-primary { + background: var(--tk-orange); + padding: 0.7rem 1.4rem; + font-size: 0.95rem; +} + +.btn-block { width: 100%; } + +.btn-secondary { + background: var(--tk-gray-dark); +} + +.btn-secondary:hover { background: #3A3A3A; } + +.btn-outline { + background: transparent; + border: 1px solid var(--tk-border); + color: var(--tk-gray-dark); +} + +.btn-outline:hover { + background: var(--tk-gray-light); + border-color: var(--tk-gray); +} + +.btn-group { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} + +/* ── Badges ── */ +.badge { + display: inline-block; + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.badge-admin { background: var(--tk-orange-light); color: var(--tk-orange-dark); } +.badge-editor { background: #EEEEEE; color: var(--tk-gray-dark); } +.badge-reader { background: var(--tk-gray-light); color: var(--tk-gray); } + +.badge-sent { background: var(--tk-success-bg); color: var(--tk-success); } +.badge-failed { background: var(--tk-error-bg); color: var(--tk-error); } +.badge-skipped { background: var(--tk-warning-bg); color: var(--tk-warning); } + +.count-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.6rem; + padding: 0.15rem 0.5rem; + background: var(--tk-gray-light); + border: 1px solid var(--tk-border); + border-radius: 999px; + font-size: 0.82rem; + font-weight: 600; + color: var(--tk-gray-dark); +} + +.count-badge.orange { + background: var(--tk-orange-light); + border-color: #F5D5BC; + color: var(--tk-orange-dark); +} + +/* ── Tables ── */ +.table-wrap { overflow-x: auto; } + +table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +th { + background: var(--tk-gray-light); + color: var(--tk-gray-dark); + font-weight: 600; + font-size: 0.82rem; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.65rem 0.75rem; + text-align: left; + border-bottom: 2px solid var(--tk-border); +} + +td { + padding: 0.65rem 0.75rem; + border-bottom: 1px solid var(--tk-border); + vertical-align: top; +} + +tbody tr:hover { background: #FAFAFA; } + +.empty-row td { + color: var(--tk-gray); + font-style: italic; + text-align: center; + padding: 1.5rem; +} + +/* ── Preview & Copy ── */ +.copy-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + padding: 0.85rem; + background: var(--tk-gray-light); + border-radius: var(--radius); + border: 1px solid var(--tk-border); +} + +.copy-status { + font-size: 0.88rem; + color: var(--tk-gray); + flex: 1 1 100%; + margin-top: 0.25rem; +} + +@media (min-width: 700px) { + .copy-status { flex: 1 1 auto; margin-top: 0; margin-left: 0.5rem; } +} + +.copy-status.success { color: var(--tk-success); } +.copy-status.error { color: var(--tk-error); } + +.newsletter-preview-frame { + width: 100%; + min-height: 640px; + border: 1px solid var(--tk-border); + border-radius: var(--radius); + background: var(--tk-gray-light); + box-shadow: var(--shadow-md); +} + +.textarea-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.75rem; +} + +.readonly-output { + font-family: Consolas, "Courier New", monospace; + font-size: 0.82rem; + line-height: 1.45; + background: var(--tk-gray-light); +} + .hidden-source { display: none; } -table { width: 100%; border-collapse: collapse; } -th, td { border-bottom: 1px solid #e6ebf4; text-align: left; padding: 0.55rem; vertical-align: top; } -.checkbox { display: flex; align-items: center; gap: 0.45rem; font-weight: 500; } -.display-options { border: 1px solid #dde3ef; border-radius: 6px; padding: 0.75rem; display: grid; gap: 0.45rem; } -.display-options legend { font-weight: 600; padding: 0 0.25rem; } -.error { color: #b42318; background: #fef3f2; border: 1px solid #fecdca; padding: 0.6rem; border-radius: 4px; margin: 0 0 0.75rem; } -.hint { color: #64748b; font-size: 0.92rem; margin: 0 0 0.6rem; } -.copy-dialog-overlay { position: fixed; inset: 0; z-index: 1000; background: rgba(15, 23, 42, 0.55); display: flex; align-items: center; justify-content: center; padding: 1rem; } -.copy-dialog { width: min(900px, 100%); max-height: 90vh; background: #fff; border-radius: 8px; padding: 1rem 1.25rem; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2); display: grid; gap: 0.75rem; } -.copy-dialog h4 { margin: 0; color: #003366; } -.copy-dialog-hint { margin: 0; color: #334155; } -.copy-dialog-textarea { width: 100%; min-height: 320px; font-family: Consolas, "Courier New", monospace; font-size: 0.85rem; line-height: 1.4; border: 1px solid #cbd5e1; border-radius: 4px; padding: 0.75rem; resize: vertical; } -.copy-dialog-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; } -.textarea-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.6rem; } + +/* ── Collapsible ── */ +details.card-details { + background: var(--tk-white); + border: 1px solid var(--tk-border); + border-radius: var(--radius-lg); + margin-bottom: 1rem; + box-shadow: var(--shadow-sm); +} + +details.card-details > summary { + padding: 1rem 1.35rem; + cursor: pointer; + font-weight: 600; + color: var(--tk-gray-dark); + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + user-select: none; +} + +details.card-details > summary::-webkit-details-marker { display: none; } + +details.card-details > summary::after { + content: "▸"; + color: var(--tk-gray); + transition: transform 0.2s; +} + +details.card-details[open] > summary::after { + transform: rotate(90deg); +} + +details.card-details > summary:hover { background: var(--tk-gray-light); } + +details.card-details .details-body { + padding: 0 1.35rem 1.25rem; + border-top: 1px solid var(--tk-border); +} + +/* ── Alerts ── */ +.error, .alert-error { + color: var(--tk-error); + background: var(--tk-error-bg); + border: 1px solid #FECDCA; + padding: 0.75rem 1rem; + border-radius: var(--radius); + margin: 0 0 0.85rem; + font-size: 0.9rem; +} + +.hint { + color: var(--tk-gray); + font-size: 0.88rem; + margin: 0 0 0.75rem; + line-height: 1.5; +} + +/* ── Login ── */ +body.login-page { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +body.login-page .topbar { flex-shrink: 0; } + +body.login-page .container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.login-card { + width: 100%; + max-width: 400px; + border-top: 4px solid var(--tk-orange); +} + +.login-card h2 { + margin: 0 0 0.35rem; + color: var(--tk-gray-dark); +} + +.login-subtitle { + margin: 0 0 1.25rem; + color: var(--tk-gray); + font-size: 0.9rem; +} + +/* ── Copy dialog ── */ +.copy-dialog-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(26, 26, 26, 0.55); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} + +.copy-dialog { + width: min(900px, 100%); + max-height: 90vh; + background: var(--tk-white); + border-radius: var(--radius-lg); + padding: 1.25rem 1.5rem; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2); + border-top: 4px solid var(--tk-orange); + display: grid; + gap: 0.85rem; +} + +.copy-dialog h4 { margin: 0; color: var(--tk-gray-dark); } +.copy-dialog-hint { margin: 0; color: var(--tk-gray); } +.copy-dialog-textarea { + width: 100%; + min-height: 320px; + font-family: Consolas, "Courier New", monospace; + font-size: 0.85rem; + line-height: 1.4; + border: 1px solid var(--tk-border); + border-radius: 6px; + padding: 0.75rem; + resize: vertical; +} + +.copy-dialog-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* ── Footer ── */ +.site-footer { + text-align: center; + padding: 1rem; + font-size: 0.8rem; + color: var(--tk-gray); + border-top: 1px solid var(--tk-border); + background: var(--tk-white); +} diff --git a/app/templates/base.html b/app/templates/base.html index e8be257..1cad5c6 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -3,22 +3,43 @@ - {{ title if title else "TK Wiki Newsletter Admin" }} - + {% block title %}{{ title if title else "Wiki Newsletter Admin" }}{% endblock %} · Thomas-Krenn.AG + - +
-

TK Wiki Newsletter Admin

+
+ +
+

Wiki Newsletter Admin

+

Thomas-Krenn.AG · Intern

+
+
{% if user %} -
- - - +
+ +
+ {{ user.full_name }} + {{ user.role }} +
+
+ + + +
{% endif %}
{% block content %}{% endblock %}
+
+ Thomas-Krenn.AG · Interner Wiki-Newsletter · Nur für den internen Gebrauch +
{% block scripts %}{% endblock %} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 37bbbdc..b20d3d2 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,204 +1,301 @@ {% extends "base.html" %} +{% block title %}Dashboard{% endblock %} {% block content %} -
-

Newsletter erstellen

-
- - - - -
- Anzeige im Output - - - -
+ + + +
+
+
{{ articles_new|length }}
+
Neue Artikel
+
+
+
{{ articles_edited|length }}
+
Bearbeitete Artikel
+
+
+
{{ articles_new|length + articles_edited|length }}
+
Gesamt
+
+
+ +
+
+ +
+
+
+

Newsletter erstellen

+

Zeitraum und Filter wählen, dann generieren.

+
+
+ + +
+ + +
+ +
+ Anzeige im Newsletter + + + +
+ {% if wiki_error %} +

{{ wiki_error }}

+ {% endif %} + {% if user.role in ["admin", "editor"] %} + + {% else %} +

Sie haben Leserechte. Bitte einen Editor oder Admin bitten, den Newsletter zu generieren.

+ {% endif %} + +
+ +
+
+
+

Vorschau & Export

+

Formatierten Newsletter ansehen und für Outlook kopieren.

+
+
+

Empfohlen: „Im Browser öffnen“ → Strg+A, Strg+C → in Outlook Strg+V.

+
+ + + + + +
+ + + + +
+ +
+
+

Neue Artikel {{ articles_new|length }}

+
+
+
- Zum Thomas-Krenn-Wiki → + + {_outlook_link("Zum Thomas-Krenn-Wiki →", WIKI_HOME_URL)}
+ {_outlook_font(f"Automatisch erstellter interner Newsletter · Nur für den Gebrauch bei {ORG_NAME}", color=OUTLOOK_MUTED, size="1")}
+ {_outlook_font(f"{idx}.")} + {_outlook_link(title, url)}{meta_html}
- {_outlook_font(f"{heading} ({len(items)})", color=OUTLOOK_HEADING, size="3", bold=True)} + + {_outlook_font(f"{heading} ({len(items)})", color=accent, size="3", bold=True)}
+ {_outlook_font(empty_text, color=OUTLOOK_MUTED)}
{idx}. - {title} + {title} {meta_html}
+ + + + {% if filters.display.show_date %}{% endif %} + {% if filters.display.show_user %}{% endif %} + {% if filters.display.show_category %}{% endif %} + + + + {% for item in articles_new %} + + + {% if filters.display.show_date %}{% endif %} + {% if filters.display.show_user %}{% endif %} + {% if filters.display.show_category %}{% endif %} + + {% else %} + + {% endfor %} + +
TitelZeitBenutzerKategorien
{{ item.title }}{{ item.timestamp }}{{ item.user }}{{ item.categories|join(", ") }}
Keine neuen Artikel im gewählten Zeitraum.
+ + + +
+
+

Bearbeitete Artikel {{ articles_edited|length }}

+
+
+ + + + + {% if filters.display.show_date %}{% endif %} + {% if filters.display.show_user %}{% endif %} + {% if filters.display.show_category %}{% endif %} + + + + {% for item in articles_edited %} + + + {% if filters.display.show_date %}{% endif %} + {% if filters.display.show_user %}{% endif %} + {% if filters.display.show_category %}{% endif %} + + {% else %} + + {% endfor %} + +
TitelZeitBenutzerKategorien
{{ item.title }}{{ item.timestamp }}{{ item.user }}{{ item.categories|join(", ") }}
Keine bearbeiteten Artikel im gewählten Zeitraum.
+
+
+ +
+ Weitere Ausgabeformate +
+

Nur-Text (Fallback)

+

URLs als Klartext – für klickbare Links die Vorschau nutzen.

+ + +

HTML-Quelltext

+

Öffnet ein Fenster mit markiertem Text – dort Strg+C drücken.

+
+ + +
+ +
+
+ {% if user.role in ["admin", "editor"] %} - +
+ SMTP-Versand +
+

Newsletter direkt per E-Mail versenden (aktuell gewählte Filter).

+
+ + + + + + + + +
+
+
{% endif %} - - {% if user.role == "admin" %} -

Zur Benutzerverwaltung

- {% endif %} - {% if wiki_error %} -

{{ wiki_error }}

- {% endif %} - -{% if user.role in ["admin", "editor"] %} -
-

Optionaler SMTP-Versand

-
- - - - - - - - -
-
-{% endif %} + {% if user.role == "admin" %} +
+ SMTP-Konfiguration +
+
+ + +
+ + +
+
+ + +
+ + + + + +
+
+
+ {% endif %} -{% if user.role == "admin" %} -
-

SMTP-Konfiguration (optional)

-
- - - - - - - - - - - -
-
-{% endif %} +
+
+

Versandprotokoll

+ Letzte 20 Einträge +
+
+ + + + + + {% for log in send_logs %} + + + + + + + + {% else %} + + {% endfor %} + +
ZeitStatusBetreffEmpfängerDetails
{{ log.created_at }}{{ log.status }}{{ log.subject }}{{ log.recipients }}{{ log.detail }}
Noch keine Versände protokolliert.
+
+
-
-

Newsletter-Vorschau (mit Hyperlinks)

-

Empfohlen für Outlook: „Im Browser öffnen“ → dort Strg+A, Strg+C → in Outlook Strg+V. Alternativ „Für Outlook kopieren“ und mit Rechtsklick → „Format beibehalten“ einfügen.

-
- - - - -
- - - - -
-
-

Output: Nur-Text (Fallback)

-

Enthält URLs als Text. Für klickbare Links bitte die Vorschau bzw. „Für Outlook kopieren“ verwenden.

- -
+ + -
-

Output: HTML-Quelltext

-

Öffnet ein Fenster mit markiertem Text – dort Strg+C drücken. Funktioniert auch ohne HTTPS.

-
- - -
- -
- -
-

Neue Artikel ({{ articles_new|length }})

- - - - - {% if filters.display.show_date %}{% endif %} - {% if filters.display.show_user %}{% endif %} - {% if filters.display.show_category %}{% endif %} - - - - {% for item in articles_new %} - - - {% if filters.display.show_date %}{% endif %} - {% if filters.display.show_user %}{% endif %} - {% if filters.display.show_category %}{% endif %} - - {% else %} - - {% endfor %} - -
TitelZeitBenutzerKategorien
{{ item.title }}{{ item.timestamp }}{{ item.user }}{{ item.categories|join(", ") }}
Keine neuen Artikel im gewählten Zeitraum.
-
- -
-

Bearbeitete Artikel ({{ articles_edited|length }})

- - - - - {% if filters.display.show_date %}{% endif %} - {% if filters.display.show_user %}{% endif %} - {% if filters.display.show_category %}{% endif %} - - - - {% for item in articles_edited %} - - - {% if filters.display.show_date %}{% endif %} - {% if filters.display.show_user %}{% endif %} - {% if filters.display.show_category %}{% endif %} - - {% else %} - - {% endfor %} - -
TitelZeitBenutzerKategorien
{{ item.title }}{{ item.timestamp }}{{ item.user }}{{ item.categories|join(", ") }}
Keine bearbeiteten Artikel im gewählten Zeitraum.
-
- -
-

Versandprotokoll (letzte 20)

- - - - {% for log in send_logs %} - - - - - - - - {% endfor %} - -
ZeitStatusBetreffEmpfängerDetails
{{ log.created_at }}{{ log.status }}{{ log.subject }}{{ log.recipients }}{{ log.detail }}
-
{% endblock %} {% block scripts %} - + {% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html index 07dd991..508551f 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -1,19 +1,22 @@ {% extends "base.html" %} +{% block title %}Anmelden{% endblock %} +{% block body_class %}login-page{% endblock %} {% block content %} -
-

Login

+ {% endblock %} diff --git a/app/templates/users.html b/app/templates/users.html index be87f3d..fb7cdb1 100644 --- a/app/templates/users.html +++ b/app/templates/users.html @@ -1,43 +1,78 @@ {% extends "base.html" %} +{% block title %}Benutzerverwaltung{% endblock %} {% block content %} -
-

Benutzerverwaltung

-
- - - - - - -
-
-
-

Bestehende Benutzer

- - - - {% for u in users %} - - - - - - - {% endfor %} - -
E-MailNameRolleAktiv
{{ u.email }}{{ u.full_name }}{{ u.role }}{{ "Ja" if u.is_active else "Nein" }}
-
+ + +
+
+
+
+

Neuen Benutzer anlegen

+
+
+ +
+ + +
+
+ + +
+ +
+
+ +
+
+

Bestehende Benutzer {{ users|length }}

+
+
+ + + + + + {% for u in users %} + + + + + + + {% endfor %} + +
E-MailNameRolleStatus
{{ u.email }}{{ u.full_name }}{{ u.role }}{{ "Aktiv" if u.is_active else "Inaktiv" }}
+
+
+
+ + +
+ {% endblock %}