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:
1 parent
bea45b9d2b
commit
a535dcee38
5 files changed
+434
-278
No files matched your search
+58
-38
@@ -54,31 +54,34 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
async function renderWizardAnbieterButtons() {
|
||||
const container = document.getElementById('wizardAnbieterButtons');
|
||||
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 {
|
||||
const anbieter = await apiRequest('/api/anbieter');
|
||||
container.innerHTML = '';
|
||||
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;
|
||||
}
|
||||
anbieter.forEach(a => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn-success';
|
||||
btn.style.cssText = 'padding:14px 22px; font-size:15px; min-width:140px;';
|
||||
btn.textContent = a.name;
|
||||
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.innerHTML = `
|
||||
<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));
|
||||
container.appendChild(btn);
|
||||
});
|
||||
} 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) {
|
||||
document.getElementById('anbieter').value = anbieterId;
|
||||
document.getElementById('wizardAnbieterLabel').textContent = '· ' + anbieterName;
|
||||
document.getElementById('wizardAnbieterLabel').textContent = ' (' + anbieterName + ')';
|
||||
document.getElementById('wizardStep1').style.display = 'none';
|
||||
document.getElementById('wizardStep2').style.display = 'block';
|
||||
|
||||
@@ -141,7 +144,7 @@ async function loadTarifoptionenForWizard() {
|
||||
section.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
section.style.display = 'block';
|
||||
section.style.display = 'flex';
|
||||
try {
|
||||
const options = await apiRequest(`/api/tarife/${tarifId}/tarifoptionen`);
|
||||
if (!options || options.length === 0) {
|
||||
@@ -150,18 +153,39 @@ async function loadTarifoptionenForWizard() {
|
||||
}
|
||||
options.forEach(opt => {
|
||||
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');
|
||||
cb.type = 'checkbox';
|
||||
cb.name = 'tarifoptionen';
|
||||
cb.value = opt.id;
|
||||
cb.className = 'mt-0.5 text-primary rounded-sm border-outline-variant focus:ring-primary/20';
|
||||
wrapper.appendChild(cb);
|
||||
const text = document.createElement('div');
|
||||
const price = (opt.provision_euro >= 0 ? '+' : '') + opt.provision_euro + '€';
|
||||
text.innerHTML = `<div style="font-weight:bold;">${escapeHtml(opt.name)} <span style="color:#666; font-weight:normal;">(${escapeHtml(price)})</span></div>
|
||||
${opt.code ? `<div style="font-size:0.85em; color:#666;">Code: ${escapeHtml(opt.code)}</div>` : ''}
|
||||
${opt.beschreibung ? `<div style="font-size:0.85em; color:#555;">${escapeHtml(opt.beschreibung)}</div>` : ''}`;
|
||||
wrapper.appendChild(text);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'flex flex-col';
|
||||
const nameEl = document.createElement('span');
|
||||
nameEl.className = 'text-body-sm text-on-surface font-semibold';
|
||||
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);
|
||||
});
|
||||
} catch (e) { /* error shown */ }
|
||||
@@ -169,33 +193,29 @@ async function loadTarifoptionenForWizard() {
|
||||
|
||||
function renderUserBadge() {
|
||||
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 => {
|
||||
el.style.display = currentUser.isAdmin ? '' : 'none';
|
||||
});
|
||||
let badge = document.getElementById('userBadge');
|
||||
if (!badge) {
|
||||
badge = document.createElement('div');
|
||||
badge.id = 'userBadge';
|
||||
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);
|
||||
|
||||
// Fill the user name in the new top header.
|
||||
const nameEl = document.getElementById('headerUserName');
|
||||
if (nameEl) nameEl.textContent = currentUser.displayName || currentUser.username;
|
||||
|
||||
// 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 = '';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = name + ' · ';
|
||||
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);
|
||||
|
||||
// Remove the legacy floating badge if it was rendered by an older session.
|
||||
const legacy = document.getElementById('userBadge');
|
||||
if (legacy) legacy.remove();
|
||||
}
|
||||
|
||||
// Preload all data in background
|
||||
|
||||
Reference in new issue
Block a user