Files
boc/landvex-all/landvex-admin.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

1304 lines
41 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 Admin — Infrastrukturportal</title>
<link rel="stylesheet" >
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
<style>
/*
LANDVEX ADMIN PORTAL — iOS Native
Wavult Group · 2026
*/
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
body {
font-family: var(--font-text);
background: var(--background-secondary);
color: var(--label-primary);
min-height: 100vh;
}
/* NAVIGATION BAR */
.nav-bar {
position: sticky;
top: 0;
z-index: 100;
height: 44px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-bottom: 0.5px solid var(--separator);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
safe-area-inset-top: env(safe-area-inset-top);
}
.nav-title {
font-family: var(--font-display);
font-size: 17px;
font-weight: 600;
letter-spacing: -0.4px;
color: var(--label-primary);
}
.nav-logo {
display: flex;
align-items: center;
gap: 16px;
}
.nav-logo .logo-icon {
width: 28px;
height: 28px;
background: linear-gradient(135deg, #007AFF 0%, #0055CC 100%);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: white;
font-weight: 700;
letter-spacing: -0.5px;
}
.badge-admin {
font-size: 11px;
font-weight: 600;
padding: 24px;
background: rgba(255, 149, 0, 0.15);
color: var(--ios-orange);
border-radius: var(--radius-xl);
letter-spacing: 0.3px;
}
/* CONTENT */
.content {
padding: 0 16px 120px;
max-width: 1100px;
margin: 0 auto;
}
/* SECTION HEADER */
.section-header {
padding: 20px 0 8px;
font-family: var(--font-display);
font-size: 20px;
font-weight: 700;
letter-spacing: -0.4px;
color: var(--label-primary);
}
.section-header-large {
padding: 28px 0 4px;
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
.section-sub {
font-size: 15px;
color: var(--label-secondary);
padding-bottom: 12px;
}
/* METRICS GRID */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 8px;
}
.metric-card {
background: var(--background-primary);
border-radius: var(--radius-lg);
padding: 16px;
border: 0.5px solid var(--separator);
}
.metric-value {
font-family: var(--font-display);
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
line-height: 1;
margin-bottom: 4px;
}
.metric-label {
font-size: 13px;
color: var(--label-secondary);
font-weight: 500;
}
.metric-trend {
font-size: 12px;
font-weight: 600;
margin-top: 6px;
}
.trend-up { color: var(--ios-green); }
.trend-down { color: var(--ios-red); }
.trend-flat { color: var(--label-tertiary); }
.metric-blue .metric-value { color: var(--ios-blue); }
.metric-green .metric-value { color: var(--ios-green); }
.metric-orange .metric-value { color: var(--ios-orange); }
.metric-red .metric-value { color: var(--ios-red); }
/* TABLE */
.table-card {
background: var(--background-primary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
}
.table-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px;
border-bottom: 0.5px solid var(--separator);
}
.table-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.3px;
}
.table-action {
font-size: 15px;
color: var(--ios-blue);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.table-scroll {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
thead th {
text-align: left;
padding: 24px;
font-size: 12px;
font-weight: 600;
color: var(--label-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--background-secondary);
border-bottom: 0.5px solid var(--separator);
}
tbody tr {
border-bottom: 0.5px solid var(--separator);
transition: background 0.15s;
cursor: pointer;
}
tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--fill-quaternary); }
tbody td {
padding: 24px;
vertical-align: middle;
}
.td-name {
font-weight: 600;
letter-spacing: -0.2px;
}
.td-sub {
font-size: 12px;
color: var(--label-secondary);
margin-top: 2px;
}
/* STATUS BADGES */
.status-badge {
display: inline-flex;
align-items: center;
gap: 16px;
font-size: 12px;
font-weight: 600;
padding: 24px;
border-radius: var(--radius-xl);
}
.status-ok {
background: rgba(52, 199, 89, 0.15);
color: var(--ios-green);
}
.status-warn {
background: rgba(255, 149, 0, 0.15);
color: var(--ios-orange);
}
.status-critical {
background: rgba(255, 59, 48, 0.15);
color: var(--ios-red);
}
.status-pending {
background: rgba(142, 142, 147, 0.15);
color: var(--ios-gray);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: var(--radius-full);
background: currentColor;
}
/* RISK SCORE */
.risk-bar-wrap {
display: flex;
align-items: center;
gap: 16px;
}
.risk-bar {
flex: 1;
height: 4px;
background: var(--fill-tertiary);
border-radius: var(--radius-sm);
overflow: hidden;
min-width: 60px;
}
.risk-fill {
height: 100%;
border-radius: var(--radius-sm);
transition: width 0.3s;
}
.risk-low .risk-fill { background: var(--ios-green); }
.risk-medium .risk-fill { background: var(--ios-orange); }
.risk-high .risk-fill { background: var(--ios-red); }
.risk-label {
font-size: 12px;
font-weight: 600;
min-width: 28px;
text-align: right;
}
/* MAP PLACEHOLDER */
.map-card {
background: var(--background-primary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
margin-bottom: 12px;
}
.map-container {
height: 320px;
background: #E8EDF2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
@media (prefers-color-scheme: dark) {
.map-container { background: #1C2328; }
}
.map-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 16px;
color: var(--label-secondary);
}
.map-grid {
position: absolute;
inset: 0;
opacity: 0.3;
background-image:
linear-gradient(rgba(0,122,255,0.3) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,122,255,0.3) 1px, transparent 1px);
background-size: 40px 40px;
}
.map-pin {
position: absolute;
width: 28px;
height: 28px;
border-radius: var(--radius-full) 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
transition: transform 0.2s;
}
.map-pin:hover { transform: rotate(-45deg) scale(1.2); }
.map-pin-ok { background: var(--ios-green); }
.map-pin-warn { background: var(--ios-orange); }
.map-pin-critical { background: var(--ios-red); }
.map-pin-inner {
transform: rotate(45deg);
font-size: 11px;
color: white;
}
.map-legend {
position: absolute;
bottom: 12px;
right: 12px;
background: var(--glass-bg);
backdrop-filter: var(--glass-blur);
-webkit-backdrop-filter: var(--glass-blur);
border-radius: var(--radius-md);
padding: 24px;
border: 0.5px solid var(--separator);
display: flex;
gap: 16px;
}
.legend-item {
display: flex;
align-items: center;
gap: 16px;
font-size: 11px;
color: var(--label-secondary);
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
}
/* FAB BUTTON */
.fab {
position: fixed;
bottom: calc(24px + env(safe-area-inset-bottom));
right: 20px;
z-index: 200;
background: var(--ios-blue);
color: white;
border: none;
border-radius: 28px;
height: 56px;
padding: 0 20px;
font-size: 15px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 4px 20px rgba(0, 122, 255, 0.4);
transition: transform 0.15s, box-shadow 0.15s;
}
.fab:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(0, 122, 255, 0.5);
}
.fab:active {
transform: translateY(0) scale(0.97);
box-shadow: 0 2px 12px rgba(0, 122, 255, 0.35);
}
/* INSPECTION CELLS */
.inspection-list {
background: var(--background-primary);
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
.inspection-item {
display: flex;
align-items: center;
padding: 24px;
border-bottom: 0.5px solid var(--separator);
gap: 16px;
cursor: pointer;
transition: background 0.15s;
}
.inspection-item:last-child { border-bottom: none; }
.inspection-item:hover { background: var(--fill-quaternary); }
.inspection-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.inspection-body { flex: 1; min-width: 0; }
.inspection-title {
font-size: 15px;
font-weight: 600;
letter-spacing: -0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.inspection-meta {
font-size: 13px;
color: var(--label-secondary);
margin-top: 2px;
}
.inspection-chevron {
font-size: 17px;
color: var(--label-tertiary);
flex-shrink: 0;
}
/* MODAL */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(242,242,247,0.4);
backdrop-filter: blur(4px);
z-index: 300;
display: flex;
align-items: flex-end;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
}
.modal-overlay.open {
opacity: 1;
pointer-events: all;
}
.modal-sheet {
background: var(--background-primary);
border-radius: var(--radius-xl) 20px 0 0;
width: 100%;
max-width: 640px;
padding: 8px 0 env(safe-area-inset-bottom);
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
.modal-overlay.open .modal-sheet {
transform: translateY(0);
}
.modal-handle {
width: 36px;
height: 4px;
background: var(--fill-primary);
border-radius: var(--radius-sm);
margin: 8px auto 16px;
}
.modal-title {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.4px;
padding: 0 20px 16px;
border-bottom: 0.5px solid var(--separator);
}
.form-group {
padding: 24px;
border-bottom: 0.5px solid var(--separator);
}
.form-label {
font-size: 13px;
font-weight: 600;
color: var(--label-secondary);
margin-bottom: 6px;
}
.form-input {
width: 100%;
padding: 24px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: var(--radius-md);
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
outline: none;
transition: border-color 0.2s;
}
.form-input:focus {
border-color: var(--ios-blue);
}
.form-select {
width: 100%;
padding: 24px;
background: var(--background-secondary);
border: 0.5px solid var(--separator);
border-radius: var(--radius-md);
font-size: 15px;
font-family: var(--font-text);
color: var(--label-primary);
outline: none;
appearance: none;
cursor: pointer;
}
.modal-actions {
padding: 24px;
display: flex;
gap: 16px;
}
.btn-primary {
flex: 1;
height: 50px;
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;
transition: opacity 0.15s;
}
.btn-primary:active { opacity: 0.7; }
.btn-secondary {
flex: 1;
height: 50px;
background: var(--fill-secondary);
color: var(--label-primary);
border: none;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
cursor: pointer;
transition: opacity 0.15s;
}
.btn-secondary:active { opacity: 0.7; }
/* 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: 16px;
cursor: pointer;
flex: 1;
padding: 0 8px;
opacity: 0.6;
transition: opacity 0.15s;
}
.tab-item.active { opacity: 1; }
.tab-icon { font-size: 22px; }
.tab-label { font-size: 10px; font-weight: 500; }
.tab-item.active .tab-label { color: var(--ios-blue); }
.tab-item.active .tab-icon { filter: none; }
/* DARK MODE */
@media (prefers-color-scheme: dark) {
.map-container { background: #1A1E23; }
}
/* RESPONSIVE */
@media (min-width: 768px) {
.metrics-grid { grid-template-columns: repeat(4, 1fr); }
.content { padding: 0 24px 80px; }
}
@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 BAR -->
<nav class="nav-bar" aria-label="Main navigation">
<div class="nav-logo">
<div class="logo-icon">LX</div>
<span class="nav-title">LandveX Admin</span>
</div>
<div>
<span class="badge-admin">ADMIN</span>
<div>A</div>
</div>
</nav>
<!-- MAIN CONTENT -->
<main class="content">
<!-- HEADER -->
<div class="section-header-large">Infrastrukturportal</div>
<div class="section-sub">LandveX · Administration · 18 maj 2026</div>
<!-- METRICS -->
<div class="section-header">Systemöversikt</div>
<div class="metrics-grid">
<div class="metric-card metric-blue">
<div class="metric-value">247</div>
<div class="metric-label">Registrerade objekt</div>
<div class="metric-trend trend-up">↑ 12 denna vecka</div>
</div>
<div class="metric-card metric-green">
<div class="metric-value">189</div>
<div class="metric-label">OK-status</div>
<div class="metric-trend trend-up">↑ 76%</div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">41</div>
<div class="metric-label">Kräver åtgärd</div>
<div class="metric-trend trend-flat">↔ Stabil</div>
</div>
<div class="metric-card metric-red">
<div class="metric-value">17</div>
<div class="metric-label">Kritiska</div>
<div class="metric-trend trend-down">↑ 3 sedan igår</div>
</div>
</div>
<!-- MAP -->
<div class="section-header">Live Operationell Karta</div>
<!-- LIVE MAP WITH ZOOMER POSITIONS -->
<div class="map-card">
<div class="table-header">
<span class="table-title"> Realtids zoomers + objekt</span>
<button class="table-action" onclick="cycleZoomerPos()">↻ Uppdatera</button>
</div>
<div class="map-container" id="map">
<div class="map-grid"></div>
<!-- Object pins -->
<div class="map-pin map-pin-ok" title="Gamla Stan Bron · OK"><div class="map-pin-inner"><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></div></div>
<div class="map-pin map-pin-warn" title="E4 Avsnitt 12 · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-critical" title="E4 Älvsjö · Kritisk"><div class="map-pin-inner"></div></div>
<div class="map-pin map-pin-ok" title="Arlanda Terminal 5 · OK"><div class="map-pin-inner"><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></div></div>
<div class="map-pin map-pin-warn" title="Öresundsbron · Varning"><div class="map-pin-inner">!</div></div>
<div class="map-pin map-pin-ok" title="Göta Älvbron · OK"><div class="map-pin-inner"><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></div></div>
<!-- Live zoomer pins -->
<div id="zoomer-lx01">
<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div>LX-01</div>
</div>
<div id="zoomer-lx02">
<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div>LX-02</div>
</div>
<div id="zoomer-lx03">
<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></div>
<div>LX-03 <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>
<!-- Live indicator -->
<div>
<div></div>
LIVE
</div>
<div class="map-legend">
<div class="legend-item"><div class="legend-dot"></div>OK (189)</div>
<div class="legend-item"><div class="legend-dot"></div>Varning (41)</div>
<div class="legend-item"><div class="legend-dot"></div>Kritisk (17)</div>
<div class="legend-item"><div class="legend-dot"></div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> Zoomer (3)</div>
</div>
</div>
</div>
<!-- UPPDRAGSSTATUS -->
<div class="section-header">Uppdragsstatus</div>
<div>
<div class="metric-card metric-green">
<div class="metric-value">14</div>
<div class="metric-label">Pågående</div>
<div class="metric-trend trend-up"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> Aktiva uppdrag</div>
</div>
<div class="metric-card metric-blue">
<div class="metric-value">218</div>
<div class="metric-label">Klart idag</div>
<div class="metric-trend trend-up">↑ 23% vs plan</div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">3</div>
<div class="metric-label">Avvikelse</div>
<div class="metric-trend trend-down">↓ Kräver åtgärd</div>
</div>
</div>
<!-- ACTIVE MISSIONS TABLE -->
<div class="table-card">
<div class="table-header">
<span class="table-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Pågående uppdrag</span>
<button class="table-action" onclick="openDispatch()">Snabb-dispatch →</button>
</div>
<div class="table-scroll">
<table>
<thead><tr>
<th>Zoomer</th><th>Objekt</th><th>Status</th><th>Progress</th><th>ETA</th><th>Åtgärd</th>
</tr></thead>
<tbody>
<tr>
<td><span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-01</span></td>
<td>E4 Avsnitt 12</td>
<td><span>PÅGÅR</span></td>
<td>
<div>
<div></div>
</div>
<div>72%</div>
</td>
<td>14 min</td>
<td><button>Följ live</button></td>
</tr>
<tr>
<td><span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-02</span></td>
<td>Gamla Stan Bron</td>
<td><span>PÅGÅR</span></td>
<td>
<div>
<div></div>
</div>
<div>35%</div>
</td>
<td>38 min</td>
<td><button>Följ live</button></td>
</tr>
<tr>
<td><span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg> LX-03</span></td>
<td>Öresundsbron S3</td>
<td><span>AVVIKELSE</span></td>
<td>
<div>
<div></div>
</div>
<div>58%</div>
</td>
<td>Försenad</td>
<td><button onclick="openDispatch()">Dispatch ny</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- KOSTNADSEFFEKTIVITET -->
<div class="section-header">Kostnadseffektivitet</div>
<div>
<div class="metric-card metric-green">
<div class="metric-value">1 240</div>
<div class="metric-label">Pris/inspektion (SEK)</div>
<div class="metric-trend trend-up">↓ 62% vs marknad</div>
</div>
<div class="metric-card metric-blue">
<div class="metric-value">3 280</div>
<div class="metric-label">Marknadspris (SEK)</div>
<div class="metric-trend flat">→ Traditionell inspektion</div>
</div>
<div class="metric-card metric-green">
<div class="metric-value">218</div>
<div class="metric-label">Insp. idag</div>
<div class="metric-trend trend-up">Rekord <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#0066FF" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg></div>
</div>
<div class="metric-card metric-orange">
<div class="metric-value">270k</div>
<div class="metric-label">Besparing idag (SEK)</div>
<div class="metric-trend trend-up">↑ vs traditionellt</div>
</div>
</div>
<!-- PIPELINE -->
<div class="section-header">Inkommande beställningar</div>
<div class="table-card">
<div class="table-header">
<span class="table-title">Pipeline · 7 väntande</span>
<button class="table-action">Hantera alla →</button>
</div>
<div class="table-scroll">
<table>
<thead><tr>
<th>Kund</th><th>Objekt</th><th>Typ</th><th>Prio</th><th>Mottagen</th><th>Åtgärd</th>
</tr></thead>
<tbody>
<tr>
<td>Trafikverket</td><td>E4 Älvsjö · Kritisk</td><td>Fullständig</td>
<td><span>KRITISK</span></td>
<td>09:12</td>
<td><button onclick="openDispatch()">Dispatch nu</button></td>
</tr>
<tr>
<td>Region Stockholm</td><td>Gamla Stan Bro</td><td>Ytinspektion</td>
<td><span>HÖG</span></td>
<td>08:45</td>
<td><button onclick="openDispatch()">Dispatch</button></td>
</tr>
<tr>
<td>NCC Sverige</td><td>Rv 40 Borås</td><td>Strukturkontroll</td>
<td><span>NORMAL</span></td>
<td>Igår</td>
<td><button>Schemalägg</button></td>
</tr>
<tr>
<td>Ellevio AB</td><td>Kraftledning N32</td><td>Optisk scanning</td>
<td><span>NORMAL</span></td>
<td>Igår</td>
<td><button>Schemalägg</button></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- KARTVY (original) -->
<div class="section-header">Kartvy (Legacy)</div>
<!-- OBJECTS TABLE -->
<div class="section-header">Registrerade objekt</div>
<div class="table-card">
<div class="table-header">
<span class="table-title">Fastigheter & Infrastruktur</span>
<button class="table-action" onclick="openModal()">+ Nytt objekt</button>
</div>
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Objekt</th>
<th>Typ</th>
<th>Status</th>
<th>Risk</th>
<th>Senaste inspektion</th>
<th>Nästa inspektion</th>
</tr>
</thead>
<tbody>
<tr onclick="alert('Öppnar objektdetaljer: E4 Norrtull')">
<td><div class="td-name">E4 Norrtull Avsnitt 12</div><div class="td-sub">REF-001 · Stockholm</div></td>
<td> Väg</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
<div class="risk-bar"><div class="risk-fill"></div></div>
<span class="risk-label">58</span>
</div>
</td>
<td>2026-05-10</td>
<td>2026-05-25</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Gamla Stan')">
<td><div class="td-name">Gamla Stan Bro</div><div class="td-sub">REF-002 · Stockholm</div></td>
<td> Bro</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
<div class="risk-bar"><div class="risk-fill"></div></div>
<span class="risk-label">22</span>
</div>
</td>
<td>2026-05-14</td>
<td>2026-08-14</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Kungliga Slottet')">
<td><div class="td-name">Kungliga Slottet Tak</div><div class="td-sub">REF-003 · Stockholm</div></td>
<td> Byggnad</td>
<td><span class="status-badge status-critical"><span class="status-dot"></span>Kritisk</span></td>
<td>
<div class="risk-bar-wrap risk-high">
<div class="risk-bar"><div class="risk-fill"></div></div>
<span class="risk-label">87</span>
</div>
</td>
<td>2026-05-01</td>
<td> Omedelbart</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Arlanda Terminal 5')">
<td><div class="td-name">Arlanda Terminal 5</div><div class="td-sub">REF-004 · Sigtuna</div></td>
<td> Byggnad</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>OK</span></td>
<td>
<div class="risk-bar-wrap risk-low">
<div class="risk-bar"><div class="risk-fill"></div></div>
<span class="risk-label">15</span>
</div>
</td>
<td>2026-05-16</td>
<td>2026-11-16</td>
</tr>
<tr onclick="alert('Öppnar objektdetaljer: Öresundsbron')">
<td><div class="td-name">Öresundsbron Sektion 3</div><div class="td-sub">REF-005 · Malmö</div></td>
<td> Bro</td>
<td><span class="status-badge status-warn"><span class="status-dot"></span>Kräver åtgärd</span></td>
<td>
<div class="risk-bar-wrap risk-medium">
<div class="risk-bar"><div class="risk-fill"></div></div>
<span class="risk-label">63</span>
</div>
</td>
<td>2026-04-28</td>
<td>2026-05-20</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- INSPECTIONS -->
<div class="section-header">Senaste inspektioner</div>
<div class="inspection-list">
<div class="inspection-item">
<div class="inspection-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 14H14M4 14V8L8 4L12 8V14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 14V10H10V14" stroke="#666" stroke-width="1.5"/></svg></div>
<div class="inspection-body">
<div class="inspection-title">E4 Norrtull — Ytinspektion</div>
<div class="inspection-meta">Anna Lindqvist · 2026-05-10 · <span>Godkänd</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon"></div>
<div class="inspection-body">
<div class="inspection-title">Öresundsbron — Strukturkontroll</div>
<div class="inspection-meta">Marcus Holm · 2026-04-28 · <span>Åtgärder krävs</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon"></div>
<div class="inspection-body">
<div class="inspection-title">Kungliga Slottet — Takinspektion</div>
<div class="inspection-meta">Sofia Berg · 2026-05-01 · <span>Kritisk rapport</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
<div class="inspection-item">
<div class="inspection-icon"></div>
<div class="inspection-body">
<div class="inspection-title">Arlanda T5 — Fasadinspektion</div>
<div class="inspection-meta">Erik Svensson · 2026-05-16 · <span>Godkänd</span></div>
</div>
<div class="inspection-chevron"></div>
</div>
</div>
<!-- STATUS REPORTS -->
<div class="section-header">Statusrapporter</div>
<div class="table-card">
<div class="table-scroll">
<table>
<thead>
<tr>
<th>Rapport</th>
<th>Period</th>
<th>Objekt</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><div class="td-name">Månatlig Statusrapport</div><div class="td-sub">Automatisk</div></td>
<td>Maj 2026</td>
<td>247 objekt</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>Klar</span></td>
<td><button class="table-action">Ladda ned</button></td>
</tr>
<tr>
<td><div class="td-name">Kritiska avvikelser</div><div class="td-sub">Automatisk · Alert</div></td>
<td>2026-05-17</td>
<td>3 objekt</td>
<td><span class="status-badge status-critical"><span class="status-dot"></span>Åtgärda</span></td>
<td><button class="table-action">Visa</button></td>
</tr>
<tr>
<td><div class="td-name">Kvartal Q1 2026</div><div class="td-sub">Manuell</div></td>
<td>JanMar 2026</td>
<td>231 objekt</td>
<td><span class="status-badge status-ok"><span class="status-dot"></span>Klar</span></td>
<td><button class="table-action">Ladda ned</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
<!-- FAB -->
<button class="fab" onclick="openModal()">
<span>+</span>
<span>Ny inspektion</span>
</button>
<!-- MODAL: Ny inspektion -->
<div class="modal-overlay" id="modal" onclick="closeModal(event)">
<div class="modal-sheet" onclick="event.stopPropagation()">
<div class="modal-handle"></div>
<div class="modal-title">Skapa ny inspektion</div>
<div class="form-group">
<div class="form-label">Objekt</div>
<select class="form-select">
<option>E4 Norrtull Avsnitt 12</option>
<option>Gamla Stan Bro</option>
<option>Kungliga Slottet Tak</option>
<option>Arlanda Terminal 5</option>
<option>Öresundsbron Sektion 3</option>
</select>
</div>
<div class="form-group">
<div class="form-label">Inspektionstyp</div>
<select class="form-select">
<option>Ytinspektion</option>
<option>Strukturkontroll</option>
<option>Fasadinspektion</option>
<option>Takinspektion</option>
<option>Fullständig inspektion</option>
</select>
</div>
<div class="form-group">
<div class="form-label">Ansvarig inspektion</div>
<input class="form-input" type="text" placeholder="Sök inspektör...">
</div>
<div class="form-group">
<div class="form-label">Datum</div>
<input class="form-input" type="date" value="2026-05-20">
</div>
<div class="modal-actions">
<button class="btn-secondary" onclick="closeModal()">Avbryt</button>
<button class="btn-primary" onclick="submitInspection()">Skapa inspektion</button>
</div>
</div>
</div>
<!-- TAB BAR -->
<nav class="tab-bar" aria-label="Main navigation">
<div class="tab-item active">
<div class="tab-icon"></div>
<div class="tab-label">Översikt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#objects'">
<div class="tab-icon"><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></div>
<div class="tab-label">Objekt</div>
</div>
<div class="tab-item" onclick="window.location.href='landvex-admin.html#inspections'">
<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"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>
<div class="tab-label">Inställningar</div>
</div>
</nav>
<style>
@keyframes zoomer-pulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0,122,255,0.4); }
50% { transform: scale(1.05); box-shadow: 0 0 0 6px rgba(0,122,255,0); }
}
@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>
<!-- DISPATCH MODAL -->
<div id="dispatch-modal">
<div id="dispatch-sheet">
<div></div>
<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Snabb-dispatch</div>
<div>
<div>Närmaste lediga zoomer</div>
<div>
<span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></span>
<div>
<div>LX-04 · Stockholm N</div>
<div>4.2 km bort · Laddad 100% · HD + Multispektral</div>
</div>
<button onclick="confirmDispatch('LX-04')">Skicka</button>
</div>
<div>
<span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V6M4 4H12" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M2 8H6L8 6L10 8H14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 8V12H10V8" stroke="#666" stroke-width="1.5"/></svg></span>
<div>
<div>LX-07 · Nacka</div>
<div>8.7 km bort · Laddad 87% · HD</div>
</div>
<button onclick="confirmDispatch('LX-07')">Skicka</button>
</div>
</div>
<div id="dispatch-success"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#00C853" stroke-width="3"><path d="M20 6L9 17l-5-5"/></svg> Dispatch bekräftad! Zoomer anländer inom 12 min.</div>
<button onclick="closeDispatch()">Stäng</button>
</div>
</div>
<script>
function openModal() {
document.getElementById('modal').classList.add('open');
}
function closeModal(e) {
if (!e || e.target === document.getElementById('modal')) {
document.getElementById('modal').classList.remove('open');
}
}
function submitInspection() {
document.getElementById('modal').classList.remove('open');
// Toast feedback
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-green);color:white;padding: 24px;border-radius: var(--radius-xl);font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(52,199,89,0.4);';
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> Inspektion skapad';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}
// Tab switching
document.querySelectorAll('.tab-item').forEach((tab, i) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
// Dispatch modal
function openDispatch() {
const m = document.getElementById('dispatch-modal');
const s = document.getElementById('dispatch-sheet');
document.getElementById('dispatch-success').style.display = 'none';
m.style.opacity = '1';
m.style.pointerEvents = 'all';
setTimeout(() => s.style.transform = 'translateY(0)', 10);
}
function closeDispatch() {
const m = document.getElementById('dispatch-modal');
const s = document.getElementById('dispatch-sheet');
s.style.transform = 'translateY(100%)';
setTimeout(() => { m.style.opacity = '0'; m.style.pointerEvents = 'none'; }, 300);
}
function confirmDispatch(zoomerId) {
setTimeout(() => {
document.getElementById('dispatch-success').style.display = 'block';
}, 800);
}
document.getElementById('dispatch-modal').addEventListener('click', (e) => {
if (e.target === document.getElementById('dispatch-modal')) closeDispatch();
});
// Zoomer live position animation
function cycleZoomerPos() {
const z1 = document.getElementById('zoomer-lx01');
const z2 = document.getElementById('zoomer-lx02');
const z3 = document.getElementById('zoomer-lx03');
if (z1) z1.style.left = (44 + Math.random() * 8) + '%';
if (z1) z1.style.top = (38 + Math.random() * 8) + '%';
if (z2) z2.style.left = (20 + Math.random() * 8) + '%';
if (z2) z2.style.top = (25 + Math.random() * 8) + '%';
if (z3) z3.style.left = (68 + Math.random() * 8) + '%';
if (z3) z3.style.top = (52 + Math.random() * 8) + '%';
}
// Auto-animate zoomers every 5s
setInterval(cycleZoomerPos, 5000);
</script>
</body>
</html>