UI redesign step 1/N: Tailwind foundation + Stitch theme + Login + Wizard

Foundation
- Pull in Tailwind via CDN with the full Stitch design tokens
  (palette, typography, spacing, radius) inline in <head>. Preflight
  disabled so the rest of the legacy inline CSS keeps applying until
  each screen is migrated.
- Add Hanken Grotesk + JetBrains Mono + Material Symbols Outlined
  via Google Fonts.
- Replace the page header with the Stitch dark-slate top bar: brand
  on the left, segmented tab nav in the middle, user pill + logout
  on the right. Floating userBadge removed; renderUserBadge() now
  writes the display name into #headerUserName and wires the
  existing logout button.

Login
- Single card mirroring the Stitch login mockup with icon-prefixed
  inputs (person/lock), focus ring, error banner and a primary CTA
  that includes the login glyph. Setup mode swaps the heading,
  shows the orange info bar with admin_panel_settings glyph and
  changes the button to "Administrator anlegen".

Wizard (Schaltung erfassen)
- Step 1: anbieter button grid in Stitch style — bordered cards
  with a material icon, hover lift and primary-coloured outline.
- Step 2: white card with a "← Anbieter ändern" pill in the
  header, two-column layout (Aktivierungsdaten / Vertragsdaten),
  label-caps form labels, JetBrains-Mono numeric inputs for
  dates/ID/phone and a dashed file-drop zone. Save button gains
  the save glyph.

Other screens (Übersicht, Provisionen, Gelöschte, Administration,
Benutzer) still render in the legacy style — those migrations
follow in the next steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kerimatilganandClaude Opus 4.7 committed 2026-05-15 17:38:55 +02:00
1 parent bea45b9d2b
commit a535dcee38
5 files changed
+433 -277

No files matched your search

+4
View File
@@ -19,6 +19,10 @@ uploads/
.env.*.local .env.*.local
sessions.db sessions.db
# Stitch design exports / drafts
stitch_*.zip
stitch_*/
# OS / Editor # OS / Editor
.DS_Store .DS_Store
Thumbs.db Thumbs.db
+246 -142
View File
@@ -4,6 +4,90 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Provisions-Tracker</title> <title>Provisions-Tracker</title>
<!-- Tailwind via CDN + Stitch design theme. Preflight is disabled so the
remaining legacy inline CSS keeps applying to screens we haven't
migrated yet. -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;600;700&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<script>
tailwind.config = {
corePlugins: { preflight: false },
theme: {
extend: {
colors: {
"primary": "#004ac6",
"primary-container": "#2563eb",
"primary-fixed": "#dbe1ff",
"primary-fixed-dim": "#b4c5ff",
"on-primary": "#ffffff",
"on-primary-container": "#eeefff",
"secondary": "#4e6073",
"secondary-container": "#cfe2f9",
"on-secondary": "#ffffff",
"tertiary": "#943700",
"tertiary-container": "#bc4800",
"on-tertiary": "#ffffff",
"error": "#ba1a1a",
"error-container": "#ffdad6",
"on-error": "#ffffff",
"on-error-container": "#93000a",
"background": "#f8f9ff",
"on-background": "#0b1c30",
"surface": "#f8f9ff",
"surface-bright": "#f8f9ff",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#eff4ff",
"surface-container": "#e5eeff",
"surface-container-high": "#dce9ff",
"surface-container-highest": "#d3e4fe",
"surface-variant": "#d3e4fe",
"surface-dim": "#cbdbf5",
"on-surface": "#0b1c30",
"on-surface-variant": "#434655",
"inverse-surface": "#213145",
"inverse-on-surface": "#eaf1ff",
"outline": "#737686",
"outline-variant": "#c3c6d7"
},
spacing: {
"base": "4px", "xs": "4px", "sm": "8px", "md": "16px",
"lg": "24px", "xl": "32px", "gutter": "16px", "margin": "24px"
},
fontFamily: {
"body-md": ["Hanken Grotesk", "sans-serif"],
"body-sm": ["Hanken Grotesk", "sans-serif"],
"title-sm": ["Hanken Grotesk", "sans-serif"],
"headline-md": ["Hanken Grotesk", "sans-serif"],
"display-lg": ["Hanken Grotesk", "sans-serif"],
"label-caps": ["Hanken Grotesk", "sans-serif"],
"label-numeric": ["JetBrains Mono", "monospace"]
},
fontSize: {
"body-md": ["14px", { lineHeight: "20px", fontWeight: "400" }],
"body-sm": ["13px", { lineHeight: "18px", fontWeight: "400" }],
"title-sm": ["18px", { lineHeight: "24px", fontWeight: "600" }],
"headline-md": ["24px", { lineHeight: "32px", letterSpacing: "-0.01em", fontWeight: "600" }],
"display-lg": ["32px", { lineHeight: "40px", letterSpacing: "-0.02em", fontWeight: "700" }],
"label-caps": ["11px", { lineHeight: "16px", letterSpacing: "0.05em", fontWeight: "700" }],
"label-numeric": ["13px", { lineHeight: "16px", fontWeight: "500" }]
},
borderRadius: {
"DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px"
}
}
}
};
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -18,74 +102,39 @@
background-color: #f4f4f4; background-color: #f4f4f4;
} }
/* Layout primitives now use Tailwind in the markup; the legacy
.container/.tab-content keep working so non-migrated screens stay
styled. body padding-top reserves space for the fixed header. */
body {
padding-top: 56px;
}
.container { .container {
max-width: 1600px; max-width: 1440px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 24px;
} }
header {
background: #2c3e50;
color: white;
text-align: center;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 8px;
}
.nav-tabs {
display: flex;
margin-bottom: 20px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.nav-tab {
flex: 1;
padding: 15px 20px;
background: #95a5a6;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
border-right: 1px solid #7f8c8d;
}
.nav-tab:last-child {
border-right: none;
}
.nav-tab.active {
background: #3498db;
color: white;
box-shadow: inset 0 -3px 0 #2980b9;
}
.nav-tab:hover:not(.active) {
background: #7f8c8d;
transform: translateY(-1px);
}
.nav-tab.active:hover {
background: #2980b9;
}
.tab-content { .tab-content {
display: none; display: none;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
} }
.tab-content.active { .tab-content.active {
display: block; display: block;
} }
/* Nav-tab active state — paired with the Tailwind classes on the
inactive baseline. Override colour + bottom border for active tab. */
header .nav-tab {
background: transparent;
border: none;
cursor: pointer;
padding: 0 8px;
font-family: inherit;
}
header .nav-tab.active {
color: #eaf1ff !important;
border-bottom: 2px solid #b4c5ff !important;
padding-top: 2px;
}
.form-group { .form-group {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -909,143 +958,198 @@
} }
</style> </style>
</head> </head>
<body> <body class="bg-background min-h-screen text-on-background" style="font-family: 'Hanken Grotesk', sans-serif;">
<div class="container">
<header>
<h1>Provisions-Tracker</h1>
<p>Erfassung und Verwaltung von Mitarbeiter-Provisionen</p>
</header>
<div class="nav-tabs"> <!-- Stitch-style top nav: dark slate header with brand on the left,
<button class="nav-tab active" onclick="showTab('schaltung')">Schaltung erfassen</button> tab navigation in the middle, user pill on the right. The nav-tab
<button class="nav-tab" onclick="showTab('overview')">Übersicht</button> class is kept as a JS hook; CSS is now Tailwind. -->
<button class="nav-tab" onclick="showTab('provisionen')">Provisionen</button> <header class="bg-inverse-surface border-b border-outline-variant fixed top-0 left-0 right-0 z-50 h-14">
<button class="nav-tab" onclick="showTab('geloeschte')">Gelöschte Schaltungen</button> <div class="flex justify-between items-center w-full px-lg h-full max-w-[1440px] mx-auto">
<button class="nav-tab" onclick="showTab('admin')">Administration</button> <div class="flex items-center gap-sm">
<button class="nav-tab admin-only" onclick="showTab('users')" style="display:none">Benutzer</button> <span class="material-symbols-outlined text-inverse-on-surface">monitoring</span>
<span class="text-title-sm font-semibold text-inverse-on-surface">Provisions-Tracker</span>
</div>
<nav class="hidden md:flex items-end h-full">
<ul class="flex gap-lg h-full">
<li class="h-full flex items-center">
<button class="nav-tab active h-full flex items-center px-sm text-body-md text-inverse-on-surface border-b-2 border-primary-fixed-dim pt-[2px] transition-colors" onclick="showTab('schaltung')">Schaltung erfassen</button>
</li>
<li class="h-full flex items-center">
<button class="nav-tab h-full flex items-center px-sm text-body-md text-on-surface-variant hover:text-inverse-on-surface transition-colors" onclick="showTab('overview')">Übersicht</button>
</li>
<li class="h-full flex items-center">
<button class="nav-tab h-full flex items-center px-sm text-body-md text-on-surface-variant hover:text-inverse-on-surface transition-colors" onclick="showTab('provisionen')">Provisionen</button>
</li>
<li class="h-full flex items-center">
<button class="nav-tab h-full flex items-center px-sm text-body-md text-on-surface-variant hover:text-inverse-on-surface transition-colors" onclick="showTab('geloeschte')">Gelöschte Schaltungen</button>
</li>
<li class="h-full flex items-center">
<button class="nav-tab h-full flex items-center px-sm text-body-md text-on-surface-variant hover:text-inverse-on-surface transition-colors" onclick="showTab('admin')">Administration</button>
</li>
<li class="h-full flex items-center admin-only" style="display:none">
<button class="nav-tab h-full flex items-center px-sm text-body-md text-on-surface-variant hover:text-inverse-on-surface transition-colors" onclick="showTab('users')">Benutzer</button>
</li>
</ul>
</nav>
<div class="flex items-center gap-md">
<span id="headerUserName" class="text-body-sm text-inverse-on-surface/90 hidden lg:inline-block"></span>
<div class="h-6 w-px bg-outline-variant/30"></div>
<button id="headerLogoutBtn" class="text-body-md text-primary-fixed-dim hover:text-inverse-on-surface transition-colors flex items-center gap-xs">
<span class="material-symbols-outlined text-[18px]">logout</span>
Abmelden
</button>
</div>
</div> </div>
</header>
<div class="container">
<!-- Schaltung erfassen Tab (2-step wizard) --> <!-- Schaltung erfassen Tab (2-step wizard) -->
<div id="schaltung" class="tab-content active"> <div id="schaltung" class="tab-content active">
<!-- Step 1: Anbieter wählen --> <!-- Step 1: Anbieter wählen -->
<div id="wizardStep1"> <div id="wizardStep1" class="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-[0_2px_4px_rgba(0,0,0,0.04)]">
<h2>Neue Schaltung — Schritt 1 von 2</h2> <div class="mb-lg border-b border-outline-variant pb-md">
<p style="color:#666; margin-bottom:20px;">Wählen Sie einen Anbieter.</p> <h1 class="text-headline-md text-on-surface mb-xs">Neue Schaltung</h1>
<div id="wizardAnbieterButtons" style="display:flex; flex-wrap:wrap; gap:10px;"></div> <p class="text-body-sm text-on-surface-variant">Schritt 1 von 2: Anbieter wählen</p>
</div>
<div id="wizardAnbieterButtons" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-md"></div>
</div> </div>
<!-- Step 2: VO + Tarif + Tarifoptionen + Kundendaten --> <!-- Step 2: VO + Tarif + Tarifoptionen + Kundendaten -->
<div id="wizardStep2" style="display:none;"> <div id="wizardStep2" style="display:none;" class="bg-surface-container-lowest border border-outline-variant rounded-lg p-lg shadow-[0_2px_4px_rgba(0,0,0,0.04)]">
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:20px;"> <div class="mb-lg border-b border-outline-variant pb-md flex justify-between items-start gap-md">
<h2 style="margin:0;">Neue Schaltung — Schritt 2 von 2 <small id="wizardAnbieterLabel" style="color:#3498db; font-weight:normal;"></small></h2> <div>
<button type="button" onclick="wizardBackToStep1()" style="padding:6px 12px;">&larr; Anbieter ändern</button> <h2 class="text-headline-md text-on-surface mb-xs">Neue Schaltung</h2>
<p class="text-body-sm text-on-surface-variant">Schritt 2 von 2: Vertragsdetails erfassen<span id="wizardAnbieterLabel" class="text-primary"></span></p>
</div>
<button type="button" onclick="wizardBackToStep1()" class="px-md py-sm rounded-lg border border-outline-variant bg-surface text-on-surface text-body-sm hover:bg-surface-container-low transition-colors flex items-center gap-xs">
<span class="material-symbols-outlined text-[18px]">arrow_back</span>
Anbieter ändern
</button>
</div> </div>
<form id="schaltungForm"> <form id="schaltungForm">
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:30px;"> <div class="flex flex-col xl:flex-row gap-lg">
<!-- Linke Spalte: Aktivierungsdaten --> <!-- Linke Spalte: Aktivierungsdaten -->
<div> <div class="flex-1 flex flex-col gap-md">
<h3 style="margin-top:0; margin-bottom:15px;">Aktivierungsdaten</h3> <h3 class="text-title-sm text-on-surface border-b border-outline-variant pb-xs">Aktivierungsdaten</h3>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="wizardVo">VO-Auswahl <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="wizardVo">VO-Auswahl <span class="text-primary">*</span></label>
<select id="wizardVo" required onchange="loadTarifeForWizard()"> <select id="wizardVo" required onchange="loadTarifeForWizard()"
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
<option value="">VO wählen...</option> <option value="">VO wählen...</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="wizardTarif">Tarif <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="wizardTarif">Tarif <span class="text-primary">*</span></label>
<select id="wizardTarif" name="tarif_id" required onchange="loadTarifoptionenForWizard()"> <select id="wizardTarif" name="tarif_id" required onchange="loadTarifoptionenForWizard()"
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
<option value="">Erst VO wählen...</option> <option value="">Erst VO wählen...</option>
</select> </select>
</div> </div>
<div class="form-group" id="tarifoptionenSection" style="display:none;"> <div class="flex flex-col gap-base" id="tarifoptionenSection" style="display:none;">
<label>Tarifoptionen</label> <label class="text-label-caps text-on-surface-variant">Tarifoptionen</label>
<div id="tarifoptionenList" style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:8px; margin-top:8px;"></div> <div id="tarifoptionenList" class="grid grid-cols-1 gap-xs"></div>
<div id="noTarifoptionen" style="display:none; color:#666; font-style:italic; margin-top:8px;">Keine Tarifoptionen verfügbar</div> <div id="noTarifoptionen" style="display:none;" class="text-body-sm text-on-surface-variant italic">Keine Tarifoptionen verfügbar</div>
</div> </div>
<div class="form-row"> <div class="grid grid-cols-2 gap-md">
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="eingabeDatum">Erfassung <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="eingabeDatum">Erfassung <span class="text-primary">*</span></label>
<input type="date" id="eingabeDatum" name="eingabe_datum" required> <input type="date" id="eingabeDatum" name="eingabe_datum" required
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="schaltungsDatum">Vertragsbeginn <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="schaltungsDatum">Vertragsbeginn <span class="text-primary">*</span></label>
<input type="date" id="schaltungsDatum" name="schaltungs_datum" required> <input type="date" id="schaltungsDatum" name="schaltungs_datum" required
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label>Erfasser</label> <label class="text-label-caps text-on-surface-variant">Erfasser</label>
<input type="text" id="wizardErfasser" readonly style="background:#f5f5f5;"> <input type="text" id="wizardErfasser" readonly
class="w-full bg-surface-container-low border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface-variant outline-none">
</div> </div>
</div> </div>
<!-- Rechte Spalte: Vertrags-/Kundendaten --> <!-- Rechte Spalte: Vertragsdaten -->
<div> <div class="flex-1 flex flex-col gap-md">
<h3 style="margin-top:0; margin-bottom:15px;">Vertragsdaten</h3> <h3 class="text-title-sm text-on-surface border-b border-outline-variant pb-xs">Vertragsdaten</h3>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="schaltungsId">Schaltungs-ID <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="schaltungsId">Schaltungs-ID <span class="text-primary">*</span></label>
<input type="text" id="schaltungsId" name="schaltungs_id" required> <input type="text" id="schaltungsId" name="schaltungs_id" required
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div class="form-row-thirds"> <div class="flex flex-col gap-base">
<div class="form-group"> <label class="text-label-caps text-on-surface-variant" for="firma">Firma (optional)</label>
<label for="firma">Firma (optional)</label> <input type="text" id="firma" name="firma"
<input type="text" id="firma" name="firma"> class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div>
<div class="grid grid-cols-2 gap-md">
<div class="flex flex-col gap-base">
<label class="text-label-caps text-on-surface-variant" for="vorname">Vorname <span class="text-primary">*</span></label>
<input type="text" id="vorname" name="vorname" required
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="vorname">Vorname <span class="required">*</span></label> <label class="text-label-caps text-on-surface-variant" for="nachname">Nachname <span class="text-primary">*</span></label>
<input type="text" id="vorname" name="vorname" required> <input type="text" id="nachname" name="nachname" required
</div> class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
<div class="form-group">
<label for="nachname">Nachname <span class="required">*</span></label>
<input type="text" id="nachname" name="nachname" required>
</div> </div>
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="rufnummer">Rufnummer (optional)</label> <label class="text-label-caps text-on-surface-variant" for="rufnummer">Rufnummer (optional)</label>
<input type="text" id="rufnummer" name="rufnummer"> <input type="text" id="rufnummer" name="rufnummer"
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="hardware">Hardware (optional, 15 Zeichen)</label> <label class="text-label-caps text-on-surface-variant" for="hardware">Hardware (optional, 15 Zeichen)</label>
<input type="text" id="hardware" name="hardware" onchange="toggleHardwarePricing()"> <input type="text" id="hardware" name="hardware" onchange="toggleHardwarePricing()"
class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div id="hardwarePricingFields" style="display:none;"> <div id="hardwarePricingFields" style="display:none;" class="grid grid-cols-2 gap-md">
<div class="form-row"> <div class="flex flex-col gap-base">
<div class="form-group"> <label class="text-label-caps text-on-surface-variant" for="hardwareEk">Hardware-EK</label>
<label for="hardwareEk">Hardware-EK</label> <input type="number" id="hardwareEk" name="hardware_ek" step="0.01" min="0" placeholder="0.00"
<input type="number" id="hardwareEk" name="hardware_ek" step="0.01" min="0" placeholder="0.00"> class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label for="kundenverkaufspreis">Kundenverkaufspreis</label> <label class="text-label-caps text-on-surface-variant" for="kundenverkaufspreis">Kundenverkaufspreis</label>
<input type="number" id="kundenverkaufspreis" name="kundenverkaufspreis_netto" step="0.01" min="0" placeholder="0.00"> <input type="number" id="kundenverkaufspreis" name="kundenverkaufspreis_netto" step="0.01" min="0" placeholder="0.00"
</div> class="w-full bg-surface-container-lowest border border-outline-variant rounded-lg h-10 px-sm font-label-numeric text-label-numeric text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 outline-none">
</div> </div>
</div> </div>
<div class="form-group"> <div class="flex flex-col gap-base">
<label>Dokumente hochladen</label> <label class="text-label-caps text-on-surface-variant">Dokumente hochladen</label>
<div class="file-upload-area" id="fileUploadArea"> <div class="file-upload-area border border-dashed border-outline-variant rounded-lg p-md flex flex-col items-center justify-center bg-surface-container-lowest hover:bg-surface-container-low transition-colors cursor-pointer" id="fileUploadArea">
<p>Dateien hier hineinziehen oder klicken zum Auswählen</p> <span class="material-symbols-outlined text-on-surface-variant text-[28px] mb-xs">upload_file</span>
<p class="text-body-sm text-on-surface-variant">Dateien hier ablegen oder klicken</p>
<input type="file" id="documentUpload" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" style="display:none;"> <input type="file" id="documentUpload" multiple accept=".pdf,.jpg,.jpeg,.png,.doc,.docx" style="display:none;">
</div> </div>
<div id="uploadedFiles" class="documents-list"></div> <div id="uploadedFiles" class="flex flex-col gap-xs mt-xs"></div>
</div> </div>
</div> </div>
</div> </div>
<input type="hidden" id="anbieter" name="anbieter_id"> <input type="hidden" id="anbieter" name="anbieter_id">
<div style="margin-top:25px;"> <div class="mt-xl pt-md border-t border-outline-variant flex justify-end">
<button type="submit" class="btn-success">Schaltung speichern</button> <button type="submit" class="px-md py-sm rounded-lg bg-primary-container text-on-primary text-title-sm hover:bg-primary transition-colors flex items-center gap-xs shadow-sm">
<span class="material-symbols-outlined text-[18px]">save</span>
Schaltung speichern
</button>
</div> </div>
</form> </form>
</div> </div>
+102 -88
View File
@@ -3,103 +3,117 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anmeldung - Provisions-Tracker</title> <title>Anmeldung — Provisions-Tracker</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;600;700&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
"primary": "#004ac6",
"primary-container": "#2563eb",
"primary-fixed-dim": "#b4c5ff",
"on-primary": "#ffffff",
"tertiary-container": "#bc4800",
"on-surface": "#0b1c30",
"on-surface-variant": "#434655",
"outline": "#737686",
"outline-variant": "#c3c6d7",
"surface-bright": "#f8f9ff",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#eff4ff",
"surface-variant": "#d3e4fe",
"error": "#ba1a1a",
"error-container": "#ffdad6",
"on-error-container": "#93000a"
},
spacing: { "xs":"4px","sm":"8px","md":"16px","lg":"24px","xl":"32px","margin":"24px","gutter":"16px","base":"4px" },
fontFamily: {
"body-md": ["Hanken Grotesk", "sans-serif"],
"body-sm": ["Hanken Grotesk", "sans-serif"],
"title-sm": ["Hanken Grotesk", "sans-serif"],
"headline-md": ["Hanken Grotesk", "sans-serif"],
"label-caps": ["Hanken Grotesk", "sans-serif"]
},
fontSize: {
"body-md": ["14px", { lineHeight: "20px", fontWeight: "400" }],
"body-sm": ["13px", { lineHeight: "18px", fontWeight: "400" }],
"title-sm": ["18px", { lineHeight: "24px", fontWeight: "600" }],
"headline-md": ["24px", { lineHeight: "32px", letterSpacing: "-0.01em", fontWeight: "600" }],
"label-caps": ["11px", { lineHeight: "16px", letterSpacing: "0.05em", fontWeight: "700" }]
},
borderRadius: { "DEFAULT":"0.25rem","lg":"0.5rem","xl":"0.75rem","full":"9999px" }
}
}
};
</script>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Hanken Grotesk', sans-serif; }
body { .material-symbols-outlined {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
background: #2c3e50;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.card {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
width: 100%;
max-width: 400px;
}
h1 {
color: #2c3e50;
margin-bottom: 8px;
font-size: 24px;
}
.subtitle {
color: #7f8c8d;
margin-bottom: 28px;
font-size: 14px;
}
label {
display: block;
margin-bottom: 6px;
color: #34495e;
font-weight: 500;
font-size: 14px;
}
input[type=text], input[type=password] {
width: 100%;
padding: 12px;
border: 1px solid #bdc3c7;
border-radius: 6px;
font-size: 15px;
margin-bottom: 18px;
}
input:focus { outline: none; border-color: #3498db; }
button {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
}
button:hover { background: #2980b9; }
button:disabled { background: #95a5a6; cursor: not-allowed; }
.error {
background: #fee;
color: #c0392b;
padding: 10px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
display: none;
}
.error.visible { display: block; }
.setup-note {
background: #fff8e1;
color: #8a6d3b;
padding: 10px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 13px;
} }
</style> </style>
</head> </head>
<body> <body class="bg-surface-container-low min-h-screen text-on-surface flex items-center justify-center p-margin">
<div class="card">
<h1 id="title">Anmeldung</h1> <div class="w-full max-w-[400px] bg-surface-container-lowest rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.06)] border border-outline-variant/30 p-xl flex flex-col gap-lg relative overflow-hidden">
<p class="subtitle" id="subtitle">Provisions-Tracker</p>
<div id="setupNote" class="setup-note" style="display:none"> <!-- Decorative top border, switched on in setup mode -->
Noch kein Benutzer eingerichtet. Dieser Account wird als Administrator angelegt. <div id="setupTopBar" class="absolute top-0 left-0 right-0 h-1 bg-tertiary-container" style="display:none;"></div>
<!-- Logo & Brand -->
<div class="flex flex-col items-center gap-sm mb-sm text-center pt-xs">
<div id="brandIcon" class="w-12 h-12 bg-primary rounded-lg flex items-center justify-center text-on-primary mb-sm">
<span class="material-symbols-outlined">monitoring</span>
</div>
<h1 id="title" class="text-headline-md text-on-surface">Anmeldung</h1>
<p id="subtitle" class="text-body-md text-on-surface-variant">Provisions-Tracker</p>
</div> </div>
<div id="errorBox" class="error"></div>
<form id="loginForm" autocomplete="on">
<label for="username">Benutzername</label>
<input type="text" id="username" name="username" required autocomplete="username">
<label for="password">Passwort</label> <!-- Setup-Info-Banner (only shown in setup mode) -->
<input type="password" id="password" name="password" required autocomplete="current-password"> <div id="setupNote" style="display:none;" class="bg-[#fffbeb] border-l-4 border-[#f59e0b] p-md rounded-r-lg flex items-start gap-md">
<span class="material-symbols-outlined text-[#f59e0b] shrink-0">info</span>
<p class="text-body-sm text-[#92400e]">Noch kein Benutzer eingerichtet. Dieser Account wird als Administrator angelegt.</p>
</div>
<button type="submit" id="submitBtn">Anmelden</button> <!-- Error banner -->
<div id="errorBox" style="display:none;" class="bg-error-container border border-error/20 text-on-error-container p-md rounded-lg text-body-sm flex items-start gap-sm">
<span class="material-symbols-outlined text-error text-[18px] shrink-0">error</span>
<span id="errorText"></span>
</div>
<form id="loginForm" autocomplete="on" class="flex flex-col gap-md">
<div class="flex flex-col gap-xs">
<label class="text-label-caps text-on-surface-variant" for="username">Benutzername</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-[20px]">person</span>
<input type="text" id="username" name="username" required autocomplete="username"
class="w-full h-10 pl-10 pr-3 rounded-lg border border-outline-variant bg-surface-bright text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all outline-none">
</div>
</div>
<div class="flex flex-col gap-xs">
<label class="text-label-caps text-on-surface-variant" for="password">Passwort</label>
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-[20px]">lock</span>
<input type="password" id="password" name="password" required autocomplete="current-password"
class="w-full h-10 pl-10 pr-3 rounded-lg border border-outline-variant bg-surface-bright text-body-md text-on-surface focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all outline-none">
</div>
</div>
<button type="submit" id="submitBtn"
class="w-full h-10 bg-primary-container text-on-primary rounded-lg text-title-sm hover:bg-primary-container/90 transition-colors mt-sm shadow-sm flex items-center justify-center gap-sm disabled:opacity-60 disabled:cursor-not-allowed">
<span id="submitLabel">Anmelden</span>
<span class="material-symbols-outlined text-[20px]" id="submitIcon">login</span>
</button>
</form> </form>
</div> </div>
<script src="/login.js"></script> <script src="/login.js"></script>
</body> </body>
</html> </html>
+23 -9
View File
@@ -1,22 +1,29 @@
// Login page logic. Detects whether initial setup is required and adjusts the form. // Login page logic: detects first-run setup mode and swaps the form into
// admin-creation mode. Talks to /api/auth/setup or /api/auth/login.
(async function init() { (async function init() {
const errorBox = document.getElementById('errorBox'); const errorBox = document.getElementById('errorBox');
const errorText = document.getElementById('errorText');
const setupNote = document.getElementById('setupNote'); const setupNote = document.getElementById('setupNote');
const setupTopBar = document.getElementById('setupTopBar');
const brandIcon = document.getElementById('brandIcon');
const title = document.getElementById('title'); const title = document.getElementById('title');
const subtitle = document.getElementById('subtitle');
const submitBtn = document.getElementById('submitBtn'); const submitBtn = document.getElementById('submitBtn');
const submitLabel = document.getElementById('submitLabel');
const submitIcon = document.getElementById('submitIcon');
const form = document.getElementById('loginForm'); const form = document.getElementById('loginForm');
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');
let setupMode = false; let setupMode = false;
function showError(msg) { function showError(msg) {
errorBox.textContent = msg; errorText.textContent = msg;
errorBox.classList.add('visible'); errorBox.style.display = 'flex';
} }
function clearError() { function clearError() {
errorBox.textContent = ''; errorText.textContent = '';
errorBox.classList.remove('visible'); errorBox.style.display = 'none';
} }
try { try {
@@ -25,14 +32,21 @@
const data = await r.json(); const data = await r.json();
setupMode = !!data.setupRequired; setupMode = !!data.setupRequired;
} }
} catch (e) { } catch (_) {
// Fall through: stay in login mode // network error -> stay in login mode
} }
if (setupMode) { if (setupMode) {
title.textContent = 'Ersteinrichtung'; title.textContent = 'Ersteinrichtung';
setupNote.style.display = 'block'; subtitle.textContent = 'Willkommen beim Provisions-Tracker.';
submitBtn.textContent = 'Administrator anlegen'; setupNote.style.display = 'flex';
setupTopBar.style.display = 'block';
submitLabel.textContent = 'Administrator anlegen';
submitIcon.textContent = 'person_add';
// Brand icon switches to admin look in setup mode
brandIcon.classList.remove('bg-primary', 'text-on-primary');
brandIcon.classList.add('bg-surface-variant', 'text-on-surface');
brandIcon.querySelector('span').textContent = 'admin_panel_settings';
passwordInput.setAttribute('autocomplete', 'new-password'); passwordInput.setAttribute('autocomplete', 'new-password');
passwordInput.setAttribute('minlength', '8'); passwordInput.setAttribute('minlength', '8');
} }
+58 -38
View File
@@ -54,31 +54,34 @@ document.addEventListener('DOMContentLoaded', async function() {
async function renderWizardAnbieterButtons() { async function renderWizardAnbieterButtons() {
const container = document.getElementById('wizardAnbieterButtons'); const container = document.getElementById('wizardAnbieterButtons');
if (!container) return; if (!container) return;
container.innerHTML = '<div style="color:#666;">Lade...</div>'; container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Lade...</div>';
try { try {
const anbieter = await apiRequest('/api/anbieter'); const anbieter = await apiRequest('/api/anbieter');
container.innerHTML = ''; container.innerHTML = '';
if (!anbieter.length) { if (!anbieter.length) {
container.innerHTML = '<div style="color:#666;">Keine Anbieter vorhanden. Bitte zuerst unter Administration einen Anbieter anlegen.</div>'; container.innerHTML = '<div class="text-body-sm text-on-surface-variant col-span-full">Keine Anbieter vorhanden. Bitte zuerst unter Administration einen Anbieter anlegen.</div>';
return; return;
} }
anbieter.forEach(a => { anbieter.forEach(a => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = 'btn-success'; btn.className = 'group flex flex-col items-center justify-center p-lg border border-outline-variant rounded-lg bg-surface-container-lowest hover:-translate-y-1 hover:shadow-sm hover:border-primary transition-all duration-200 h-32';
btn.style.cssText = 'padding:14px 22px; font-size:15px; min-width:140px;'; btn.innerHTML = `
btn.textContent = a.name; <span class="material-symbols-outlined text-4xl text-on-surface-variant group-hover:text-primary mb-sm" style="font-variation-settings: 'FILL' 1;">cell_tower</span>
<span class="text-title-sm text-on-surface"></span>
`;
btn.querySelector('span.text-title-sm').textContent = a.name;
btn.addEventListener('click', () => wizardSelectAnbieter(a.id, a.name, a.default_vo_id)); btn.addEventListener('click', () => wizardSelectAnbieter(a.id, a.name, a.default_vo_id));
container.appendChild(btn); container.appendChild(btn);
}); });
} catch (e) { } catch (e) {
container.innerHTML = '<div style="color:#c0392b;">Fehler beim Laden der Anbieter</div>'; container.innerHTML = '<div class="text-body-sm text-error col-span-full">Fehler beim Laden der Anbieter</div>';
} }
} }
async function wizardSelectAnbieter(anbieterId, anbieterName, defaultVoId) { async function wizardSelectAnbieter(anbieterId, anbieterName, defaultVoId) {
document.getElementById('anbieter').value = anbieterId; document.getElementById('anbieter').value = anbieterId;
document.getElementById('wizardAnbieterLabel').textContent = '· ' + anbieterName; document.getElementById('wizardAnbieterLabel').textContent = ' (' + anbieterName + ')';
document.getElementById('wizardStep1').style.display = 'none'; document.getElementById('wizardStep1').style.display = 'none';
document.getElementById('wizardStep2').style.display = 'block'; document.getElementById('wizardStep2').style.display = 'block';
@@ -141,7 +144,7 @@ async function loadTarifoptionenForWizard() {
section.style.display = 'none'; section.style.display = 'none';
return; return;
} }
section.style.display = 'block'; section.style.display = 'flex';
try { try {
const options = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`); const options = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
if (!options || options.length === 0) { if (!options || options.length === 0) {
@@ -150,18 +153,39 @@ async function loadTarifoptionenForWizard() {
} }
options.forEach(opt => { options.forEach(opt => {
const wrapper = document.createElement('label'); const wrapper = document.createElement('label');
wrapper.style.cssText = 'display:flex; align-items:flex-start; gap:8px; padding:8px; border:1px solid #ddd; border-radius:5px; background:#f9f9f9; cursor:pointer;'; wrapper.className = 'flex items-start gap-sm p-sm border border-outline-variant rounded-lg bg-surface-container-lowest hover:bg-surface-container-low cursor-pointer';
const cb = document.createElement('input'); const cb = document.createElement('input');
cb.type = 'checkbox'; cb.type = 'checkbox';
cb.name = 'tarifoptionen'; cb.name = 'tarifoptionen';
cb.value = opt.id; cb.value = opt.id;
cb.className = 'mt-0.5 text-primary rounded-sm border-outline-variant focus:ring-primary/20';
wrapper.appendChild(cb); wrapper.appendChild(cb);
const text = document.createElement('div');
const price = (opt.provision_euro >= 0 ? '+' : '') + opt.provision_euro + '€'; const body = document.createElement('div');
text.innerHTML = `<div style="font-weight:bold;">${escapeHtml(opt.name)} <span style="color:#666; font-weight:normal;">(${escapeHtml(price)})</span></div> body.className = 'flex flex-col';
${opt.code ? `<div style="font-size:0.85em; color:#666;">Code: ${escapeHtml(opt.code)}</div>` : ''} const nameEl = document.createElement('span');
${opt.beschreibung ? `<div style="font-size:0.85em; color:#555;">${escapeHtml(opt.beschreibung)}</div>` : ''}`; nameEl.className = 'text-body-sm text-on-surface font-semibold';
wrapper.appendChild(text); nameEl.textContent = opt.name;
body.appendChild(nameEl);
if (opt.code) {
const codeEl = document.createElement('span');
codeEl.className = 'text-label-caps text-on-surface-variant';
codeEl.textContent = 'Code: ' + opt.code;
body.appendChild(codeEl);
}
if (opt.beschreibung) {
const desc = document.createElement('span');
desc.className = 'text-body-sm text-on-surface-variant';
desc.textContent = opt.beschreibung;
body.appendChild(desc);
}
wrapper.appendChild(body);
const price = document.createElement('span');
price.className = 'ml-auto font-label-numeric text-label-numeric ' + (opt.provision_euro >= 0 ? 'text-primary' : 'text-error');
price.textContent = (opt.provision_euro >= 0 ? '+' : '') + opt.provision_euro + ' €';
wrapper.appendChild(price);
list.appendChild(wrapper); list.appendChild(wrapper);
}); });
} catch (e) { /* error shown */ } } catch (e) { /* error shown */ }
@@ -169,33 +193,29 @@ async function loadTarifoptionenForWizard() {
function renderUserBadge() { function renderUserBadge() {
if (!currentUser) return; if (!currentUser) return;
// Show admin-only navigation elements (e.g. the Benutzer tab) once we know the role. // Show admin-only nav items now that we know the role.
document.querySelectorAll('.admin-only').forEach(el => { document.querySelectorAll('.admin-only').forEach(el => {
el.style.display = currentUser.isAdmin ? '' : 'none'; el.style.display = currentUser.isAdmin ? '' : 'none';
}); });
let badge = document.getElementById('userBadge');
if (!badge) { // Fill the user name in the new top header.
badge = document.createElement('div'); const nameEl = document.getElementById('headerUserName');
badge.id = 'userBadge'; if (nameEl) nameEl.textContent = currentUser.displayName || currentUser.username;
badge.style.cssText = 'position:fixed;top:12px;right:16px;z-index:9000;background:rgba(255,255,255,0.95);padding:8px 14px;border-radius:6px;font-size:13px;box-shadow:0 2px 6px rgba(0,0,0,0.15);font-family:Segoe UI,Tahoma,sans-serif;';
document.body.appendChild(badge); // Wire the header logout button (idempotent: only attach once).
const logoutBtn = document.getElementById('headerLogoutBtn');
if (logoutBtn && !logoutBtn.dataset.wired) {
logoutBtn.dataset.wired = '1';
logoutBtn.addEventListener('click', async (e) => {
e.preventDefault();
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); } catch (_) {}
window.location.href = '/login';
});
} }
const name = currentUser.displayName || currentUser.username;
badge.innerHTML = ''; // Remove the legacy floating badge if it was rendered by an older session.
const span = document.createElement('span'); const legacy = document.getElementById('userBadge');
span.textContent = name + ' · '; if (legacy) legacy.remove();
const link = document.createElement('a');
link.href = '#';
link.textContent = 'Abmelden';
link.style.color = '#c0392b';
link.style.textDecoration = 'none';
link.addEventListener('click', async (e) => {
e.preventDefault();
try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }); } catch (_) {}
window.location.href = '/login';
});
badge.appendChild(span);
badge.appendChild(link);
} }
// Preload all data in background // Preload all data in background