Files
boc/landvex-site/landvex-customer.html
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

694 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>LandveX — Mina objekt</title>
<link rel="stylesheet" >
<style>
/*
LANDVEX KUNDPORTAL — iOS Native
*/
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--font-text);
background: var(--grouped-background-primary);
color: var(--label-primary);
min-height: 100vh;
}
/* LARGE TITLE NAV */
.nav-bar {
position: sticky;
top: 0;
z-index: 100;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 0.5px solid var(--separator);
}
.nav-top {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.nav-title-sm {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.4px;
}
.nav-large {
padding: 4px 16px 12px;
}
.nav-title-lg {
font-family: var(--font-display);
font-size: 34px;
font-weight: 700;
letter-spacing: -0.5px;
}
.nav-avatar {
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: linear-gradient(135deg, #007AFF, #5856D6);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
/* SEARCH BAR */
.search-bar {
padding: 0 16px 12px;
}
.search-input-wrap {
display: flex;
align-items: center;
gap: 8px;
background: var(--fill-primary);
border-radius: var(--radius-md);
padding: 0 12px;
height: 36px;
}
.search-icon { font-size: 16px; color: var(--label-tertiary); }
.search-input {
flex: 1;
background: none;
border: none;
outline: none;
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
}
.search-input::placeholder { color: var(--label-tertiary); }
/* CONTENT */
.content { padding: 0 16px 100px; }
/* INSET GROUP */
.inset-group {
margin-bottom: 28px;
}
.inset-label {
font-size: 13px;
font-weight: 600;
color: var(--label-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0 4px 6px;
padding: 0 12px;
}
.inset-card {
background: var(--grouped-background-secondary);
border-radius: var(--radius-lg);
overflow: hidden;
border: 0.5px solid var(--separator);
}
/* HERO STATS */
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--separator);
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 28px;
border: 0.5px solid var(--separator);
}
.hero-stat {
background: var(--grouped-background-secondary);
padding: 16px;
text-align: center;
}
.hero-stat-value {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
.hero-stat-label {
font-size: 12px;
color: var(--label-secondary);
margin-top: 2px;
}
/* OBJECT CARD */
.object-card {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
gap: 14px;
cursor: pointer;
transition: background 0.15s;
}
.object-card:last-child { border-bottom: none; }
.object-card:active { background: var(--fill-quaternary); }
.object-thumb {
width: 52px;
height: 52px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.object-body { flex: 1; min-width: 0; }
.object-name {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.object-meta {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
}
.object-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.risk-chip {
font-size: 13px;
font-weight: 700;
padding: 4px 10px;
border-radius: var(--radius-xl);
letter-spacing: -0.2px;
}
.risk-chip-low { background: rgba(52,199,89,0.15); color: var(--ios-green); }
.risk-chip-medium { background: rgba(255,149,0,0.15); color: var(--ios-orange); }
.risk-chip-high { background: rgba(255,59,48,0.15); color: var(--ios-red); }
.object-date {
font-size: 12px;
color: var(--label-tertiary);
}
.chevron {
font-size: 18px;
color: var(--label-quaternary);
}
/* INSPECTION HISTORY */
.history-item {
display: flex;
align-items: flex-start;
padding: 14px 16px;
border-bottom: 0.5px solid var(--separator);
gap: 12px;
}
.history-item:last-child { border-bottom: none; }
.history-dot-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
padding-top: 3px;
}
.history-dot {
width: 10px;
height: 10px;
border-radius: var(--radius-full);
flex-shrink: 0;
}
.history-line {
width: 2px;
flex: 1;
min-height: 20px;
background: var(--separator);
margin-top: 4px;
}
.history-body { flex: 1; }
.history-title {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.2px;
}
.history-desc {
font-size: 13px;
color: var(--label-secondary);
margin-top: 3px;
line-height: 1.4;
}
.history-date {
font-size: 12px;
color: var(--label-tertiary);
margin-top: 4px;
}
/* ACTION ROW */
.action-row {
display: flex;
padding: 14px 16px;
align-items: center;
justify-content: space-between;
border-bottom: 0.5px solid var(--separator);
cursor: pointer;
transition: background 0.15s;
}
.action-row:last-child { border-bottom: none; }
.action-row:active { background: var(--fill-quaternary); }
.action-label {
font-size: 17px;
color: var(--ios-blue);
}
/* ORDER INSPECTION BUTTON */
.order-btn {
width: 100%;
height: 54px;
background: var(--ios-blue);
color: white;
border: none;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
margin-bottom: 28px;
transition: opacity 0.15s, transform 0.1s;
box-shadow: 0 4px 16px rgba(0,122,255,0.3);
}
.order-btn:active {
opacity: 0.85;
transform: scale(0.98);
}
/* NOTIFICATION BANNER */
.alert-banner {
background: rgba(255,59,48,0.1);
border: 0.5px solid rgba(255,59,48,0.3);
border-radius: var(--radius-md);
padding: 12px 14px;
margin-bottom: 16px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.alert-icon { font-size: 18px; flex-shrink: 0; }
.alert-title {
font-size: 14px;
font-weight: 600;
color: var(--ios-red);
}
.alert-text {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
}
/* TAB BAR */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: calc(49px + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-top: 0.5px solid var(--separator);
display: flex;
align-items: flex-start;
justify-content: space-around;
padding-top: 8px;
z-index: 90;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
cursor: pointer;
flex: 1;
opacity: 0.55;
transition: opacity 0.15s;
}
.tab-item.active { opacity: 1; }
.tab-icon { font-size: 22px; }
.tab-label { font-size: 10px; font-weight: 500; color: var(--label-secondary); }
.tab-item.active .tab-label { color: var(--ios-blue); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<style id="wcag-contrast-enforcement">
/* AAMOS GLOBAL CONTRAST ENFORCEMENT — WCAG AA (4.5:1 minimum) */
/* Regel: Ingen text under 70% opacity. Aldrig. */
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
/* Fix common low-contrast patterns */
[style*="rgba(60,60,67,0.4)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(60,60,67,0.3)"] { color: rgba(60,60,67,0.65) !important; }
[style*="rgba(0,0,0,0.4)"] { color: rgba(0,0,0,0.72) !important; }
[style*="rgba(0,0,0,0.3)"] { color: rgba(0,0,0,0.72) !important; }
[style*="opacity: 0.4"] { opacity: 0.72 !important; }
[style*="opacity: 0.3"] { opacity: 0.72 !important; }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav-bar">
<div class="nav-top">
<button style="background:none;border:none;color:var(--ios-blue);font-size:15px;cursor:pointer;">← Tillbaka</button>
<div class="nav-title-sm">Mina objekt</div>
<div class="nav-avatar">K</div>
</div>
<div class="nav-large">
<div class="nav-title-lg">Mina objekt</div>
</div>
<div class="search-bar">
<div class="search-input-wrap">
<span class="search-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg></span>
<input class="search-input" placeholder="Sök objekt..." type="search">
</div>
</div>
</nav>
<!-- CONTENT -->
<main class="content">
<!-- ALERT BANNER -->
<div class="alert-banner">
<div class="alert-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
<div>
<div class="alert-title">Åtgärd krävs</div>
<div class="alert-text">Kungliga Slottet Tak har kritisk riskpoäng. Inspektion rekommenderas omedelbart.</div>
</div>
</div>
<!-- HERO STATS -->
<div class="hero-stats">
<div class="hero-stat">
<div class="hero-stat-value" style="color:var(--ios-blue)">5</div>
<div class="hero-stat-label">Mina objekt</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value" style="color:var(--ios-green)">12</div>
<div class="hero-stat-label">Inspektioner</div>
</div>
<div class="hero-stat">
<div class="hero-stat-value" style="color:var(--ios-orange)">2</div>
<div class="hero-stat-label">Öppna ärenden</div>
</div>
</div>
<!-- OBJECTS -->
<div class="inset-group">
<div class="inset-label">Registrerade objekt</div>
<div class="inset-card">
<div class="object-card" onclick="showDetail('E4 Norrtull Avsnitt 12')">
<div class="object-thumb" style="background:rgba(255,149,0,0.15)"></div>
<div class="object-body">
<div class="object-name">E4 Norrtull Avsnitt 12</div>
<div class="object-meta">Stockholm · Väg</div>
</div>
<div class="object-right">
<span class="risk-chip risk-chip-medium">Risk 58</span>
<span class="object-date">Inspekterad 10 maj</span>
</div>
<span class="chevron"></span>
</div>
<div class="object-card" onclick="showDetail('Gamla Stan Bro')">
<div class="object-thumb" style="background:rgba(52,199,89,0.15)"></div>
<div class="object-body">
<div class="object-name">Gamla Stan Bro</div>
<div class="object-meta">Stockholm · Bro</div>
</div>
<div class="object-right">
<span class="risk-chip risk-chip-low">Risk 22</span>
<span class="object-date">Inspekterad 14 maj</span>
</div>
<span class="chevron"></span>
</div>
<div class="object-card" onclick="showDetail('Kungliga Slottet Tak')">
<div class="object-thumb" style="background:rgba(255,59,48,0.15)"></div>
<div class="object-body">
<div class="object-name">Kungliga Slottet Tak</div>
<div class="object-meta">Stockholm · Byggnad</div>
</div>
<div class="object-right">
<span class="risk-chip risk-chip-high">Risk 87</span>
<span class="object-date"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Omedelbart</span>
</div>
<span class="chevron"></span>
</div>
<div class="object-card" onclick="showDetail('Arlanda Terminal 5')">
<div class="object-thumb" style="background:rgba(0,122,255,0.15)"></div>
<div class="object-body">
<div class="object-name">Arlanda Terminal 5</div>
<div class="object-meta">Sigtuna · Byggnad</div>
</div>
<div class="object-right">
<span class="risk-chip risk-chip-low">Risk 15</span>
<span class="object-date">Inspekterad 16 maj</span>
</div>
<span class="chevron"></span>
</div>
<div class="object-card" onclick="showDetail('Öresundsbron Sektion 3')">
<div class="object-thumb" style="background:rgba(255,149,0,0.15)"></div>
<div class="object-body">
<div class="object-name">Öresundsbron Sektion 3</div>
<div class="object-meta">Malmö · Bro</div>
</div>
<div class="object-right">
<span class="risk-chip risk-chip-medium">Risk 63</span>
<span class="object-date">Inspekterad 28 apr</span>
</div>
<span class="chevron"></span>
</div>
</div>
</div>
<!-- BESTÄLL INSPEKTION -->
<button class="order-btn" onclick="orderInspection()">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Beställ ny inspektion
</button>
<!-- INSPECTION HISTORY -->
<div class="inset-group">
<div class="inset-label">Inspektionshistorik</div>
<div class="inset-card">
<div class="history-item">
<div class="history-dot-wrap">
<div class="history-dot" style="background:var(--ios-green)"></div>
<div class="history-line"></div>
</div>
<div class="history-body">
<div class="history-title">Arlanda T5 — Godkänd</div>
<div class="history-desc">Fasadinspektion genomförd. Inga anmärkningar. Nästa inspektion om 6 månader.</div>
<div class="history-date">16 maj 2026 · Erik Svensson</div>
</div>
</div>
<div class="history-item">
<div class="history-dot-wrap">
<div class="history-dot" style="background:var(--ios-green)"></div>
<div class="history-line"></div>
</div>
<div class="history-body">
<div class="history-title">Gamla Stan Bro — Godkänd</div>
<div class="history-desc">Strukturkontroll genomförd. Mindre sprickor dokumenterade, ingen åtgärd krävs.</div>
<div class="history-date">14 maj 2026 · Anna Lindqvist</div>
</div>
</div>
<div class="history-item">
<div class="history-dot-wrap">
<div class="history-dot" style="background:var(--ios-orange)"></div>
<div class="history-line"></div>
</div>
<div class="history-body">
<div class="history-title">E4 Norrtull — Åtgärder krävs</div>
<div class="history-desc">Ytinspektion visar asfaltsförslitning på 340 meter. Reparation inom 2 veckor rekommenderas.</div>
<div class="history-date">10 maj 2026 · Marcus Holm</div>
</div>
</div>
<div class="history-item">
<div class="history-dot-wrap">
<div class="history-dot" style="background:var(--ios-red)"></div>
<div class="history-line"></div>
</div>
<div class="history-body">
<div class="history-title">Kungliga Slottet Tak — Kritisk</div>
<div class="history-desc">Takpaneler med korrision och strukturskador. Omedelbar åtgärd nödvändig. Rapport skickad till myndighet.</div>
<div class="history-date">1 maj 2026 · Sofia Berg</div>
</div>
</div>
<div class="history-item">
<div class="history-dot-wrap">
<div class="history-dot" style="background:var(--ios-orange)"></div>
</div>
<div class="history-body">
<div class="history-title">Öresundsbron — Åtgärder krävs</div>
<div class="history-desc">Korrosion påträffad i sektion 3B. Behandling bör ske inom 30 dagar för att undvika eskalering.</div>
<div class="history-date">28 april 2026 · Thomas Larsson</div>
</div>
</div>
</div>
</div>
<!-- SETTINGS -->
<div class="inset-group">
<div class="inset-label">Konto</div>
<div class="inset-card">
<div class="action-row">
<span class="action-label">Kontoinformation</span>
<span style="color:var(--label-tertiary);font-size:18px"></span>
</div>
<div class="action-row">
<span class="action-label">Notifieringsinställningar</span>
<span style="color:var(--label-tertiary);font-size:18px"></span>
</div>
<div class="action-row">
<span class="action-label">Ladda ned rapporter</span>
<span style="color:var(--label-tertiary);font-size:18px"></span>
</div>
<div class="action-row" onclick="alert('Loggar ut...')">
<span style="font-size:17px;color:var(--ios-red)">Logga ut</span>
<span style="color:var(--label-tertiary);font-size:18px"></span>
</div>
</div>
</div>
</main>
<!-- TAB BAR -->
<nav class="tab-bar">
<div class="tab-item active">
<div class="tab-icon"></div>
<div class="tab-label">Mina objekt</div>
</div>
<div class="tab-item">
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg></div>
<div class="tab-label">Inspektioner</div>
</div>
<div class="tab-item">
<div class="tab-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0066FF" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></div>
<div class="tab-label">Rapporter</div>
</div>
<div class="tab-item">
<div class="tab-icon"></div>
<div class="tab-label">Notiser</div>
</div>
<div class="tab-item">
<div class="tab-icon"></div>
<div class="tab-label">Konto</div>
</div>
</nav>
<script>
function showDetail(name) {
// Simulate navigation to object detail
const details = {
'E4 Norrtull Avsnitt 12': 'Riskpoäng: 58/100\nSenaste inspektion: 10 maj 2026\nStatus: Åtgärder krävs\nNästa: 25 maj 2026',
'Gamla Stan Bro': 'Riskpoäng: 22/100\nSenaste inspektion: 14 maj 2026\nStatus: OK\nNästa: 14 aug 2026',
'Kungliga Slottet Tak': 'Riskpoäng: 87/100\nSenaste inspektion: 1 maj 2026\nStatus: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> KRITISK\nÅtgärd: Omedelbart',
'Arlanda Terminal 5': 'Riskpoäng: 15/100\nSenaste inspektion: 16 maj 2026\nStatus: OK\nNästa: 16 nov 2026',
'Öresundsbron Sektion 3': 'Riskpoäng: 63/100\nSenaste inspektion: 28 apr 2026\nStatus: Åtgärder krävs\nNästa: 20 maj 2026',
};
alert(`${name}\n\n${details[name] || 'Detaljsida under uppbyggnad.'}`);
}
function orderInspection() {
const msg = `Beställ inspektion\n\nVälj objekt och datum i nästa steg.\nEn LandveX-inspektör kontaktar dig inom 24 timmar.\n\n[Bekräfta beställning]`;
if (confirm(msg)) {
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-blue);color:white;padding:12px 20px;border-radius: var(--radius-xl);font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(0,122,255,0.4);white-space:nowrap;';
toast.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Beställning skickad';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
}
document.querySelectorAll('.tab-item').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
</script>
</body>
</html>