6989a98d75
- 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
979 lines
39 KiB
HTML
979 lines
39 KiB
HTML
<!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, user-scalable=no">
|
||
<title>LandveX Inspector</title>
|
||
<link rel="stylesheet" >
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||
html, body {
|
||
font-family: var(--font-text);
|
||
background: var(--grouped-background-primary);
|
||
color: var(--label-primary);
|
||
height: 100%; overflow: hidden;
|
||
max-width: 390px; margin: 0 auto;
|
||
}
|
||
|
||
/* Safe areas */
|
||
.nav-bar {
|
||
background: var(--glass-bg);
|
||
backdrop-filter: var(--glass-blur);
|
||
-webkit-backdrop-filter: var(--glass-blur);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
padding: 0 16px 12px;
|
||
padding-top: calc(14px + env(safe-area-inset-top));
|
||
position: fixed; top: 0; left: 0; right: 0;
|
||
max-width: 390px; margin: 0 auto;
|
||
z-index: 1000;
|
||
}
|
||
.nav-row { display: flex; align-items: center; justify-content: space-between; }
|
||
.nav-title { font-size: 17px; font-weight: 600; }
|
||
.nav-date { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
.nav-avatar {
|
||
width: 32px; height: 32px; border-radius: var(--radius-full);
|
||
background: var(--ios-blue); color: white;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 14px; font-weight: 700;
|
||
}
|
||
.refresh-btn {
|
||
background: none; border: none; font-size: 20px; cursor: pointer;
|
||
padding: 4px; color: var(--ios-blue);
|
||
}
|
||
|
||
/* Scroll area */
|
||
.scroll-area {
|
||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||
max-width: 390px; margin: 0 auto;
|
||
overflow-y: auto; -webkit-overflow-scrolling: touch;
|
||
padding-bottom: calc(80px + env(safe-area-inset-bottom));
|
||
}
|
||
.scroll-inner { padding-top: 96px; }
|
||
|
||
/* Pull-to-refresh indicator */
|
||
.ptr-indicator {
|
||
text-align: center; padding: 12px; font-size: 13px;
|
||
color: var(--label-secondary); display: none;
|
||
}
|
||
.ptr-indicator.active { display: block; }
|
||
|
||
/* Screens */
|
||
.screen { display: none; }
|
||
.screen.active { display: block; }
|
||
|
||
/* Sections */
|
||
.section-header {
|
||
font-size: 13px; font-weight: 600; color: var(--label-secondary);
|
||
text-transform: uppercase; letter-spacing: 0.5px;
|
||
padding: 0 20px; margin: 20px 0 8px;
|
||
}
|
||
|
||
/* Mission cards */
|
||
.mission-card {
|
||
margin: 0 16px 10px;
|
||
background: var(--grouped-background-secondary);
|
||
border-radius: var(--radius-md); overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||
cursor: pointer;
|
||
transition: transform 0.15s, box-shadow 0.15s;
|
||
-webkit-touch-callout: none;
|
||
}
|
||
.mission-card:active { transform: scale(0.98); box-shadow: 0 0 0 rgba(0,0,0,0); }
|
||
.mission-top { display: flex; align-items: center; padding: 24px; gap: 16px; }
|
||
.mission-type-icon {
|
||
width: 44px; height: 44px; border-radius: var(--radius-md);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 22px; flex-shrink: 0;
|
||
}
|
||
.type-bridge { background: rgba(0,122,255,0.12); }
|
||
.type-road { background: rgba(255,149,0,0.12); }
|
||
.type-facade { background: rgba(88,86,214,0.12); }
|
||
.type-roof { background: rgba(52,199,89,0.12); }
|
||
.mission-info { flex: 1; min-width: 0; }
|
||
.mission-name { font-size: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.mission-addr { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
.mission-meta { display: flex; align-items: center; gap: 16px; margin-top: 6px; }
|
||
.mission-priority {
|
||
padding: 24px; border-radius: var(--radius-sm);
|
||
font-size: 11px; font-weight: 700;
|
||
}
|
||
.priority-urgent { background: rgba(255,59,48,0.12); color: var(--ios-red); }
|
||
.priority-normal { background: rgba(0,122,255,0.10); color: var(--ios-blue); }
|
||
.priority-low { background: var(--fill-tertiary); color: var(--label-secondary); }
|
||
.mission-time { font-size: 12px; color: var(--label-secondary); }
|
||
.mission-arrow { color: var(--ios-gray3); font-size: 14px; }
|
||
|
||
/* Map */
|
||
#map { height: 200px; margin: 0 16px 4px; border-radius: var(--radius-md); overflow: hidden; }
|
||
|
||
/* Tab bar */
|
||
.tab-bar {
|
||
position: fixed; bottom: 0; left: 0; right: 0;
|
||
max-width: 390px; margin: 0 auto;
|
||
background: var(--glass-thick-bg);
|
||
backdrop-filter: var(--glass-thick-blur);
|
||
-webkit-backdrop-filter: var(--glass-thick-blur);
|
||
border-top: 0.5px solid var(--separator);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
display: flex; z-index: 999;
|
||
}
|
||
.tab-item {
|
||
flex: 1; display: flex; flex-direction: column; align-items: center;
|
||
padding: 10px 0 8px; cursor: pointer;
|
||
color: var(--label-secondary); font-size: 10px; font-weight: 500;
|
||
gap: 16px; transition: color 0.15s;
|
||
}
|
||
.tab-item.active { color: var(--ios-blue); }
|
||
.tab-icon { font-size: 24px; line-height: 1; }
|
||
|
||
/* ===== INSPECTION FORM SCREEN ===== */
|
||
.form-nav {
|
||
display: flex; align-items: center; padding: 24px;
|
||
padding-top: calc(14px + env(safe-area-inset-top));
|
||
background: var(--glass-bg);
|
||
backdrop-filter: var(--glass-blur);
|
||
border-bottom: 0.5px solid var(--separator);
|
||
gap: 16px;
|
||
}
|
||
.back-btn {
|
||
color: var(--ios-blue); font-size: 16px; font-weight: 500;
|
||
background: none; border: none; cursor: pointer; padding: 4px;
|
||
display: flex; align-items: center; gap: 16px;
|
||
}
|
||
.form-title { flex: 1; font-size: 17px; font-weight: 600; text-align: center; }
|
||
|
||
.form-scroll { padding: 16px; }
|
||
|
||
/* Steps */
|
||
.step-indicator {
|
||
display: flex; justify-content: center; gap: 16px; margin-bottom: 20px;
|
||
}
|
||
.step-dot {
|
||
width: 8px; height: 8px; border-radius: var(--radius-full);
|
||
background: var(--ios-gray4); transition: background 0.3s;
|
||
}
|
||
.step-dot.done { background: var(--ios-green); }
|
||
.step-dot.active { background: var(--ios-blue); width: 20px; border-radius: var(--radius-sm); }
|
||
|
||
/* Form elements */
|
||
.form-section { margin-bottom: 24px; }
|
||
.form-section-title { font-size: 13px; font-weight: 600; color: var(--label-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
|
||
|
||
.form-card {
|
||
background: var(--grouped-background-secondary);
|
||
border-radius: var(--radius-md); overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||
}
|
||
.form-field {
|
||
padding: 24px; border-bottom: 0.5px solid var(--separator);
|
||
}
|
||
.form-field:last-child { border-bottom: none; }
|
||
.field-label { font-size: 13px; color: var(--label-secondary); margin-bottom: 4px; }
|
||
.field-input {
|
||
width: 100%; border: none; outline: none; background: transparent;
|
||
font-size: 16px; font-family: var(--font-text); color: var(--label-primary);
|
||
-webkit-appearance: none;
|
||
}
|
||
.field-textarea {
|
||
width: 100%; border: none; outline: none; background: transparent;
|
||
font-size: 15px; font-family: var(--font-text); color: var(--label-primary);
|
||
resize: none; min-height: 80px;
|
||
-webkit-appearance: none;
|
||
}
|
||
select.field-input { cursor: pointer; }
|
||
|
||
/* Checklist */
|
||
.check-item {
|
||
display: flex; align-items: flex-start; padding: 24px; gap: 16px;
|
||
border-bottom: 0.5px solid var(--separator); cursor: pointer;
|
||
}
|
||
.check-item:last-child { border-bottom: none; }
|
||
.check-box {
|
||
width: 24px; height: 24px; border-radius: var(--radius-full);
|
||
border: 2px solid var(--ios-gray3);
|
||
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
|
||
transition: all 0.15s;
|
||
}
|
||
.check-box.checked {
|
||
background: var(--ios-blue); border-color: var(--ios-blue);
|
||
color: white; font-size: 14px;
|
||
}
|
||
.check-text { font-size: 15px; flex: 1; padding-top: 1px; }
|
||
.check-sub { font-size: 13px; color: var(--label-secondary); margin-top: 2px; }
|
||
|
||
/* Photo grid */
|
||
.photo-grid {
|
||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px; padding: 0;
|
||
}
|
||
.photo-thumb {
|
||
aspect-ratio: 1; background: var(--fill-tertiary);
|
||
border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer; overflow: hidden; position: relative;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.photo-thumb:active { opacity: 0.7; }
|
||
.photo-add-icon { font-size: 32px; color: var(--ios-blue); }
|
||
.photo-preview { width: 100%; height: 100%; object-fit: cover; display: none; }
|
||
.photo-delete {
|
||
position: absolute; top: 4px; right: 4px;
|
||
width: 20px; height: 20px; border-radius: var(--radius-full);
|
||
background: rgba(242,242,247,0.5); color: white;
|
||
font-size: 12px; display: none; align-items: center; justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Signature canvas */
|
||
.sig-wrap {
|
||
background: var(--fill-quaternary); border-radius: var(--radius-md);
|
||
overflow: hidden; position: relative; margin: 0 16px;
|
||
}
|
||
#sig-canvas {
|
||
display: block; width: 100%; touch-action: none;
|
||
cursor: crosshair;
|
||
}
|
||
.sig-label {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
color: var(--label-tertiary); font-size: 13px; pointer-events: none;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.sig-actions { display: flex; gap: 16px; padding: 24px; }
|
||
.sig-clear-btn {
|
||
flex: 1; background: var(--fill-tertiary); border: none;
|
||
border-radius: var(--radius-md); padding: 10px; font-size: 14px; font-weight: 500;
|
||
color: var(--ios-red); cursor: pointer; font-family: var(--font-text);
|
||
}
|
||
|
||
/* Submit button */
|
||
.submit-btn {
|
||
display: block; width: calc(100% - 32px); margin: 16px;
|
||
background: var(--ios-blue); color: white; border: none;
|
||
border-radius: var(--radius-md); padding: 15px; font-size: 16px; font-weight: 600;
|
||
cursor: pointer; font-family: var(--font-text);
|
||
box-shadow: 0 2px 12px rgba(0,122,255,0.3);
|
||
transition: opacity 0.15s;
|
||
}
|
||
.submit-btn:active { opacity: 0.8; }
|
||
.submit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
|
||
/* Step nav buttons */
|
||
.step-nav {
|
||
display: flex; gap: 16px; padding: 0 16px 16px;
|
||
}
|
||
.step-btn {
|
||
flex: 1; padding: 13px; border-radius: var(--radius-md);
|
||
border: none; font-size: 15px; font-weight: 600;
|
||
cursor: pointer; font-family: var(--font-text);
|
||
transition: opacity 0.15s;
|
||
}
|
||
.step-btn:active { opacity: 0.75; }
|
||
.step-prev { background: var(--fill-tertiary); color: var(--label-primary); }
|
||
.step-next { background: var(--ios-blue); color: white; box-shadow: 0 2px 10px rgba(0,122,255,0.25); }
|
||
|
||
/* ===== CONFIRMATION SCREEN ===== */
|
||
.confirm-screen {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
justify-content: center; min-height: 100vh; padding: 24px;
|
||
text-align: center;
|
||
}
|
||
.confirm-icon {
|
||
width: 80px; height: 80px; border-radius: var(--radius-full);
|
||
background: rgba(52,199,89,0.15); border: 2px solid var(--ios-green);
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-size: 40px; margin-bottom: 24px;
|
||
animation: pop-in 0.5s cubic-bezier(0.34,1.56,0.64,1);
|
||
}
|
||
@keyframes pop-in { 0%{transform:scale(0)} 100%{transform:scale(1)} }
|
||
.confirm-title { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
|
||
.confirm-report-num {
|
||
font-size: 13px; color: var(--label-secondary);
|
||
background: var(--fill-tertiary); padding: 24px; border-radius: var(--radius-xl);
|
||
font-family: var(--font-mono); margin-bottom: 24px;
|
||
}
|
||
.confirm-details {
|
||
background: var(--grouped-background-secondary);
|
||
border-radius: var(--radius-md); width: 100%; text-align: left; overflow: hidden;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 24px;
|
||
}
|
||
.confirm-row {
|
||
display: flex; justify-content: space-between; padding: 24px;
|
||
border-bottom: 0.5px solid var(--separator); font-size: 14px;
|
||
}
|
||
.confirm-row:last-child { border-bottom: none; }
|
||
.confirm-key { color: var(--label-secondary); }
|
||
.confirm-val { font-weight: 500; }
|
||
.confirm-btn {
|
||
width: 100%; background: var(--ios-blue); color: white; border: none;
|
||
border-radius: var(--radius-md); padding: 14px; font-size: 16px; font-weight: 600;
|
||
cursor: pointer; font-family: var(--font-text);
|
||
box-shadow: 0 2px 12px rgba(0,122,255,0.3);
|
||
}
|
||
@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>
|
||
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": [
|
||
"Organization",
|
||
"Corporation"
|
||
],
|
||
"@id": "https://landvex.com/#organization",
|
||
"name": "Landvex",
|
||
"alternateName": [
|
||
"LandveX",
|
||
"Landvex AB",
|
||
"LandveX AB"
|
||
],
|
||
"url": "https://landvex.com",
|
||
"logo": {
|
||
"@type": "ImageObject",
|
||
"url": "https://landvex.com/apple-touch-icon.png",
|
||
"width": 180,
|
||
"height": 180
|
||
},
|
||
"foundingDate": "2024",
|
||
"foundingLocation": {
|
||
"@type": "Place",
|
||
"addressLocality": "Gothenburg",
|
||
"addressRegion": "Gothenburg",
|
||
"addressCountry": "SE"
|
||
},
|
||
"legalName": "Landvex Inc",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Houston",
|
||
"addressRegion": "TX",
|
||
"addressCountry": "US"
|
||
},
|
||
"location": [
|
||
{
|
||
"@type": "Place",
|
||
"name": "Houston, Texas, USA (US HQ)",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Houston",
|
||
"addressRegion": "TX",
|
||
"addressCountry": "US"
|
||
}
|
||
},
|
||
{
|
||
"@type": "Place",
|
||
"name": "Tyresö, Sweden (EU HQ)",
|
||
"address": {
|
||
"@type": "PostalAddress",
|
||
"addressLocality": "Gothenburg",
|
||
"addressRegion": "Gothenburg",
|
||
"addressCountry": "SE"
|
||
}
|
||
}
|
||
],
|
||
"taxID": "pending-EIN",
|
||
"description": "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally.",
|
||
"disambiguatingDescription": "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Tyresö, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform.",
|
||
"knowsAbout": [
|
||
"control intelligence",
|
||
"decision intelligence",
|
||
"infrastructure risk assessment",
|
||
"urban intelligence",
|
||
"field data collection",
|
||
"contradiction detection",
|
||
"physical world analytics",
|
||
"geospatial analytics"
|
||
],
|
||
"brand": {
|
||
"@type": "Brand",
|
||
"name": "Landvex",
|
||
"slogan": "Control intelligence for the physical world."
|
||
},
|
||
"sameAs": [
|
||
"https://landvex.com",
|
||
"https://www.linkedin.com/company/landvex",
|
||
"https://x.com/landvex",
|
||
"https://twitter.com/landvex",
|
||
"https://www.crunchbase.com/organization/landvex",
|
||
"https://github.com/landvex",
|
||
"https://www.instagram.com/landvex"
|
||
],
|
||
"contactPoint": {
|
||
"@type": "ContactPoint",
|
||
"email": "contact@landvex.com",
|
||
"contactType": "sales"
|
||
},
|
||
"slogan": "Where reported reality conflicts with observed reality."
|
||
}
|
||
</script>
|
||
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ==================== MAIN LIST SCREEN ==================== -->
|
||
<div id="screen-list" class="screen active">
|
||
<div class="nav-bar">
|
||
<div class="nav-row">
|
||
<div>
|
||
<div class="nav-title"> Dagens Uppdrag</div>
|
||
<div class="nav-date" id="today-date">—</div>
|
||
</div>
|
||
<div>
|
||
<button class="refresh-btn" onclick="pullRefresh()" title="Uppdatera">↻</button>
|
||
<div class="nav-avatar">IG</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="scroll-area">
|
||
<div class="scroll-inner">
|
||
<div class="ptr-indicator" id="ptr">⏳ Uppdaterar uppdrag...</div>
|
||
|
||
<!-- Map -->
|
||
<div class="section-header">Karta</div>
|
||
<div id="map"></div>
|
||
|
||
<!-- Missions -->
|
||
<div class="section-header">Uppdrag idag (<span id="mission-count">3</span>)</div>
|
||
|
||
<div class="mission-card" onclick="openMission('bro-e45')">
|
||
<div class="mission-top">
|
||
<div class="mission-type-icon type-bridge"></div>
|
||
<div class="mission-info">
|
||
<div class="mission-name">Bro E45 — Götaälvbron</div>
|
||
<div class="mission-addr">Göteborg, Hamnbanan</div>
|
||
<div class="mission-meta">
|
||
<span class="mission-priority priority-urgent">AKUT</span>
|
||
<span class="mission-time">08:00–10:30</span>
|
||
</div>
|
||
</div>
|
||
<div class="mission-arrow">›</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mission-card" onclick="openMission('vag-rv40')">
|
||
<div class="mission-top">
|
||
<div class="mission-type-icon type-road"></div>
|
||
<div class="mission-info">
|
||
<div class="mission-name">Väg RV40 km 142</div>
|
||
<div class="mission-addr">Borås–Göteborg, spricköversyn</div>
|
||
<div class="mission-meta">
|
||
<span class="mission-priority priority-normal">NORMAL</span>
|
||
<span class="mission-time">11:00–13:00</span>
|
||
</div>
|
||
</div>
|
||
<div class="mission-arrow">›</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mission-card" onclick="openMission('fasad-kungsbacka')">
|
||
<div class="mission-top">
|
||
<div class="mission-type-icon type-facade"><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg></div>
|
||
<div class="mission-info">
|
||
<div class="mission-name">Fasad Kungsbacka centrum</div>
|
||
<div class="mission-addr">Storgatan 12, Kungsbacka</div>
|
||
<div class="mission-meta">
|
||
<span class="mission-priority priority-low">LÅG</span>
|
||
<span class="mission-time">14:00–16:00</span>
|
||
</div>
|
||
</div>
|
||
<div class="mission-arrow">›</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mission-card" onclick="openMission('tak-lindome')">
|
||
<div class="mission-top">
|
||
<div class="mission-type-icon type-roof"></div>
|
||
<div class="mission-info">
|
||
<div class="mission-name">Tak Lindome industripark</div>
|
||
<div class="mission-addr">Lindome, Mölndal</div>
|
||
<div class="mission-meta">
|
||
<span class="mission-priority priority-normal">NORMAL</span>
|
||
<span class="mission-time">16:30–17:30</span>
|
||
</div>
|
||
</div>
|
||
<div class="mission-arrow">›</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /scroll-inner -->
|
||
</div><!-- /scroll-area -->
|
||
</div><!-- /screen-list -->
|
||
|
||
|
||
<!-- ==================== INSPECTION FORM SCREEN ==================== -->
|
||
<div id="screen-form" class="screen">
|
||
<div class="form-nav">
|
||
<button class="back-btn" onclick="goBack()">‹ Uppdrag</button>
|
||
<div class="form-title" id="form-title">Inspektion</div>
|
||
</div>
|
||
|
||
<div class="scroll-area" id="form-scroll-area">
|
||
<div id="form-content">
|
||
<!-- Steps injected by JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- ==================== CONFIRMATION SCREEN ==================== -->
|
||
<div id="screen-confirm" class="screen">
|
||
<div class="confirm-screen">
|
||
<div class="confirm-icon"><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 class="confirm-title">Rapport inskickad!</div>
|
||
<div class="confirm-report-num" id="report-num">LVX-2026-0000</div>
|
||
<div class="confirm-details">
|
||
<div class="confirm-row">
|
||
<span class="confirm-key">Objekt</span>
|
||
<span class="confirm-val" id="conf-object">—</span>
|
||
</div>
|
||
<div class="confirm-row">
|
||
<span class="confirm-key">Typ</span>
|
||
<span class="confirm-val" id="conf-type">—</span>
|
||
</div>
|
||
<div class="confirm-row">
|
||
<span class="confirm-key">Inspektör</span>
|
||
<span class="confirm-val">I. Gustafsson</span>
|
||
</div>
|
||
<div class="confirm-row">
|
||
<span class="confirm-key">Tid</span>
|
||
<span class="confirm-val" id="conf-time">—</span>
|
||
</div>
|
||
<div class="confirm-row">
|
||
<span class="confirm-key">Status</span>
|
||
<span class="confirm-val"><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> Synkad</span>
|
||
</div>
|
||
</div>
|
||
<button class="confirm-btn" onclick="goHome()">← Tillbaka till uppdrag</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- TAB BAR -->
|
||
<div class="tab-bar" id="main-tab-bar">
|
||
<div class="tab-item active" onclick="switchTab('list')">
|
||
<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>
|
||
<span>Uppdrag</span>
|
||
</div>
|
||
<div class="tab-item" onclick="switchTab('map-tab')">
|
||
<div class="tab-icon"></div>
|
||
<span>Karta</span>
|
||
</div>
|
||
<div class="tab-item" onclick="switchTab('reports')">
|
||
<div class="tab-icon"></div>
|
||
<span>Rapporter</span>
|
||
</div>
|
||
<div class="tab-item" onclick="switchTab('profile')">
|
||
<div class="tab-icon"></div>
|
||
<span>Profil</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script>
|
||
// State
|
||
const STORAGE_KEY = 'landvex_inspector_state';
|
||
let currentMission = null;
|
||
let currentStep = 0;
|
||
let checklistState = {};
|
||
let photoCount = 0;
|
||
let sigDrawing = false, sigHasData = false;
|
||
let sigCtx, sigCanvas;
|
||
|
||
const MISSIONS = {
|
||
'bro-e45': { name: 'Bro E45 — Götaälvbron', type: 'bro', lat: 57.706, lng: 11.975 },
|
||
'vag-rv40': { name: 'Väg RV40 km 142', type: 'väg', lat: 57.721, lng: 12.929 },
|
||
'fasad-kungsbacka':{ name: 'Fasad Kungsbacka centrum', type: 'fasad', lat: 57.504, lng: 12.076 },
|
||
'tak-lindome': { name: 'Tak Lindome industripark', type: 'tak', lat: 57.572, lng: 12.091 },
|
||
};
|
||
|
||
const CHECKLISTS = {
|
||
bro: [
|
||
{ id:'bro1', text:'Visuell undersökning av bärande konstruktion', sub:'Sprickor, korrosion, deformation' },
|
||
{ id:'bro2', text:'Kontroll av skyddsskikt och ytbehandling', sub:'Betongtäckning, epoxibehandling' },
|
||
{ id:'bro3', text:'Fogar och rörelsefogars skick', sub:'Täthetskontroll, gummilister' },
|
||
{ id:'bro4', text:'Dränering och avvattningssystem', sub:'Brunnar, rör, igensättning' },
|
||
{ id:'bro5', text:'Räcken och säkerhetsanordningar', sub:'Fastspänning, deformation' },
|
||
{ id:'bro6', text:'Fundament och grundläggning', sub:'Erosion, sättningar under bro' },
|
||
{ id:'bro7', text:'Belysning och signalering', sub:'Funktionskontroll, kablage' },
|
||
],
|
||
väg: [
|
||
{ id:'vag1', text:'Sprickkartering och klassificering', sub:'Längsgående, tvärgående, nätsprickor' },
|
||
{ id:'vag2', text:'Deformationsmätning (spårdjup)', sub:'Hjulspår, sättningar' },
|
||
{ id:'vag3', text:'Kantskador och slänter', sub:'Urgrävning, erosion, vegetation' },
|
||
{ id:'vag4', text:'Vägmarkeringar', sub:'Reflexivitet, synlighet' },
|
||
{ id:'vag5', text:'Avvattningssystem', sub:'Diken, trummor, brunnar' },
|
||
{ id:'vag6', text:'Skyltar och vägmärken', sub:'Synlighet, fastspänning, skador' },
|
||
],
|
||
fasad: [
|
||
{ id:'fas1', text:'Putsytans skick', sub:'Sprickor, avlossnat puts, fukt' },
|
||
{ id:'fas2', text:'Fogar och tätningslister', sub:'Kring fönster och dörrar' },
|
||
{ id:'fas3', text:'Fönsterbågar och karmar', sub:'Röta, deformation, tätning' },
|
||
{ id:'fas4', text:'Balkonger och utskjutande delar', sub:'Armering, korrosion, räcken' },
|
||
{ id:'fas5', text:'Sockel och grundmur', sub:'Fuktintrång, sprickbildning' },
|
||
{ id:'fas6', text:'Ventilationsöppningar', sub:'Igensättning, nätskydd' },
|
||
],
|
||
tak: [
|
||
{ id:'tak1', text:'Takbeläggning och ytskikt', sub:'Skador, lagningar, ålder' },
|
||
{ id:'tak2', text:'Takavvattning (rännor och stuprör)', sub:'Fastsättning, blockering' },
|
||
{ id:'tak3', text:'Genomföringar och skorstenar', sub:'Tätning, flashning' },
|
||
{ id:'tak4', text:'Takkonstruktion (om åtkomlig)', sub:'Röta, fukt, bärförmåga' },
|
||
{ id:'tak5', text:'Tillträdesanordningar', sub:'Stegar, luckor, säkerhetslina' },
|
||
{ id:'tak6', text:'Solceller/tekniska installationer', sub:'Skador, fastspänning' },
|
||
],
|
||
};
|
||
|
||
// Init
|
||
document.getElementById('today-date').textContent = new Date().toLocaleDateString('sv-SE', { weekday:'long', year:'numeric', month:'long', day:'numeric' });
|
||
|
||
// Init map
|
||
const map = L.map('map', { zoomControl:false, attributionControl:false }).setView([57.7, 12.0], 9);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
|
||
Object.entries(MISSIONS).forEach(([id, m]) => {
|
||
L.marker([m.lat, m.lng]).addTo(map).bindPopup(`<b>${m.name}</b>`);
|
||
});
|
||
|
||
// Screens
|
||
function showScreen(id) {
|
||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||
document.getElementById(id).classList.add('active');
|
||
const showTabs = id === 'screen-list';
|
||
document.getElementById('main-tab-bar').style.display = showTabs ? 'flex' : 'none';
|
||
}
|
||
|
||
function goBack() {
|
||
showScreen('screen-list');
|
||
saveOfflineState();
|
||
}
|
||
|
||
function goHome() {
|
||
showScreen('screen-list');
|
||
currentMission = null;
|
||
currentStep = 0;
|
||
checklistState = {};
|
||
photoCount = 0;
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
|
||
event.currentTarget.classList.add('active');
|
||
if (tab === 'map-tab') {
|
||
setTimeout(() => map.invalidateSize(), 100);
|
||
document.getElementById('map').scrollIntoView({ behavior:'smooth' });
|
||
}
|
||
}
|
||
|
||
// Missions
|
||
function openMission(id) {
|
||
currentMission = id;
|
||
currentStep = 0;
|
||
checklistState = {};
|
||
const m = MISSIONS[id];
|
||
document.getElementById('form-title').textContent = m.type.charAt(0).toUpperCase() + m.type.slice(1);
|
||
renderStep();
|
||
showScreen('screen-form');
|
||
}
|
||
|
||
function renderStep() {
|
||
const m = MISSIONS[currentMission];
|
||
const checklist = CHECKLISTS[m.type] || [];
|
||
const steps = ['info','checklist','photos','signature'];
|
||
const labels = ['Grundinfo','Checklista','Foton','Signatur'];
|
||
|
||
const stepDots = steps.map((s,i) => {
|
||
const cls = i < currentStep ? 'step-dot done' : i === currentStep ? 'step-dot active' : 'step-dot';
|
||
return `<div class="${cls}"></div>`;
|
||
}).join('');
|
||
|
||
let content = '';
|
||
|
||
if (currentStep === 0) {
|
||
// Step 1: Basic info
|
||
content = `
|
||
<div class="form-scroll">
|
||
<div class="step-indicator">${stepDots}</div>
|
||
<div class="form-section">
|
||
<div class="form-section-title">Objekt</div>
|
||
<div class="form-card">
|
||
<div class="form-field">
|
||
<div class="field-label">Objektnamn</div>
|
||
<input class="field-input" id="f-name" value="${m.name}" placeholder="Objektnamn">
|
||
</div>
|
||
<div class="form-field">
|
||
<div class="field-label">Objekttyp</div>
|
||
<select class="field-input" id="f-type">
|
||
<option value="bro" ${m.type==='bro'?'selected':''}> Bro</option>
|
||
<option value="väg" ${m.type==='väg'?'selected':''}> Väg</option>
|
||
<option value="fasad" ${m.type==='fasad'?'selected':''}><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 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Fasad</option>
|
||
<option value="tak" ${m.type==='tak'?'selected':''}> Tak</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field">
|
||
<div class="field-label">Adress / Plats</div>
|
||
<input class="field-input" id="f-addr" value="${m.lat.toFixed(3)}, ${m.lng.toFixed(3)}" placeholder="Adress">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="form-section">
|
||
<div class="form-section-title">Inspektör</div>
|
||
<div class="form-card">
|
||
<div class="form-field">
|
||
<div class="field-label">Namn</div>
|
||
<input class="field-input" value="I. Gustafsson">
|
||
</div>
|
||
<div class="form-field">
|
||
<div class="field-label">Väder</div>
|
||
<select class="field-input" id="f-weather">
|
||
<option> Soligt</option>
|
||
<option> Molnigt</option>
|
||
<option> Regn</option>
|
||
<option> Snö</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-field">
|
||
<div class="field-label">Anmärkningar</div>
|
||
<textarea class="field-textarea" id="f-notes" placeholder="Fri text..."></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="step-nav">
|
||
<button class="step-btn step-next" onclick="nextStep()">Checklist →</button>
|
||
</div>`;
|
||
|
||
} else if (currentStep === 1) {
|
||
// Step 2: Checklist
|
||
const items = checklist.map(c => `
|
||
<div class="check-item" onclick="toggleCheck('${c.id}')">
|
||
<div class="check-box ${checklistState[c.id] ? 'checked' : ''}" id="chk-${c.id}">
|
||
${checklistState[c.id] ? '<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="check-text">${c.text}</div>
|
||
<div class="check-sub">${c.sub}</div>
|
||
</div>
|
||
</div>`).join('');
|
||
|
||
const done = checklist.filter(c => checklistState[c.id]).length;
|
||
content = `
|
||
<div class="form-scroll">
|
||
<div class="step-indicator">${stepDots}</div>
|
||
<div class="form-section">
|
||
<div class="form-section-title">Checklista (${done}/${checklist.length})</div>
|
||
<div class="form-card">${items}</div>
|
||
</div>
|
||
</div>
|
||
<div class="step-nav">
|
||
<button class="step-btn step-prev" onclick="prevStep()">← Tillbaka</button>
|
||
<button class="step-btn step-next" onclick="nextStep()">Foton →</button>
|
||
</div>`;
|
||
|
||
} else if (currentStep === 2) {
|
||
// Step 3: Photos
|
||
const photoSlots = Array(6).fill(0).map((_, i) => `
|
||
<div class="photo-thumb" id="photo-slot-${i}" onclick="addPhoto(${i})">
|
||
<img loading="lazy" loading="lazy" loading="lazy" loading="lazy" class="photo-preview" id="photo-img-${i}" src="" alt="Inspection photo">
|
||
<div class="photo-add-icon" id="photo-add-${i}"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="4" width="12" height="9" rx="2" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="8.5" r="2.5" stroke="#666" stroke-width="1.5"/><path d="M5 4L6 2H10L11 4" stroke="#666" stroke-width="1.5"/></svg></div>
|
||
<div class="photo-delete" id="photo-del-${i}" onclick="deletePhoto(event,${i})"></div>
|
||
</div>`).join('');
|
||
|
||
content = `
|
||
<div class="form-scroll">
|
||
<div class="step-indicator">${stepDots}</div>
|
||
<div class="form-section">
|
||
<div class="form-section-title">Foto-dokumentation</div>
|
||
<div class="form-card">
|
||
<div class="photo-grid">${photoSlots}</div>
|
||
</div>
|
||
<div>
|
||
Tryck på kamera-ikonen för att ta foto eller välja från biblioteket
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="step-nav">
|
||
<button class="step-btn step-prev" onclick="prevStep()">← Checklista</button>
|
||
<button class="step-btn step-next" onclick="nextStep()">Signatur →</button>
|
||
</div>
|
||
<input type="file" id="photo-input" accept="image/*" capture="environment" onchange="handlePhoto(event)">`;
|
||
|
||
} else if (currentStep === 3) {
|
||
// Step 4: Signature
|
||
content = `
|
||
<div class="form-scroll">
|
||
<div class="step-indicator">${stepDots}</div>
|
||
<div class="form-section">
|
||
<div class="form-section-title">Digital signatur</div>
|
||
<div class="sig-wrap">
|
||
<canvas id="sig-canvas" height="180"></canvas>
|
||
<div class="sig-label" id="sig-hint"> Rita din signatur här</div>
|
||
</div>
|
||
<div class="sig-actions">
|
||
<button class="sig-clear-btn" onclick="clearSig()"> Rensa</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<button class="submit-btn" id="submit-btn" onclick="submitReport()"> Skicka in rapport</button>
|
||
</div>
|
||
<div>
|
||
<button class="step-btn step-prev" onclick="prevStep()">← Foton</button>
|
||
</div>`;
|
||
}
|
||
|
||
document.getElementById('form-content').innerHTML = content;
|
||
|
||
// Init signature canvas if on step 3
|
||
if (currentStep === 3) initSig();
|
||
}
|
||
|
||
function toggleCheck(id) {
|
||
checklistState[id] = !checklistState[id];
|
||
renderStep();
|
||
saveOfflineState();
|
||
}
|
||
|
||
function nextStep() {
|
||
currentStep = Math.min(3, currentStep + 1);
|
||
renderStep();
|
||
document.getElementById('form-scroll-area').scrollTop = 0;
|
||
}
|
||
function prevStep() {
|
||
currentStep = Math.max(0, currentStep - 1);
|
||
renderStep();
|
||
document.getElementById('form-scroll-area').scrollTop = 0;
|
||
}
|
||
|
||
// Photos
|
||
let activePhotoSlot = 0;
|
||
function addPhoto(slot) {
|
||
activePhotoSlot = slot;
|
||
document.getElementById('photo-input').click();
|
||
}
|
||
|
||
function handlePhoto(e) {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const url = URL.createObjectURL(file);
|
||
const img = document.getElementById(`photo-img-${activePhotoSlot}`);
|
||
const add = document.getElementById(`photo-add-${activePhotoSlot}`);
|
||
const del = document.getElementById(`photo-del-${activePhotoSlot}`);
|
||
img.src = url; img.style.display = 'block';
|
||
add.style.display = 'none'; del.style.display = 'flex';
|
||
e.target.value = '';
|
||
saveOfflineState();
|
||
}
|
||
|
||
function deletePhoto(e, slot) {
|
||
e.stopPropagation();
|
||
const img = document.getElementById(`photo-img-${slot}`);
|
||
const add = document.getElementById(`photo-add-${slot}`);
|
||
const del = document.getElementById(`photo-del-${slot}`);
|
||
img.src = ''; img.style.display = 'none';
|
||
add.style.display = 'flex'; del.style.display = 'none';
|
||
}
|
||
|
||
// Signature
|
||
function initSig() {
|
||
sigCanvas = document.getElementById('sig-canvas');
|
||
if (!sigCanvas) return;
|
||
sigCanvas.width = sigCanvas.offsetWidth;
|
||
sigCtx = sigCanvas.getContext('2d');
|
||
sigCtx.strokeStyle = '#007AFF';
|
||
sigCtx.lineWidth = 2.5;
|
||
sigCtx.lineCap = 'round';
|
||
sigCtx.lineJoin = 'round';
|
||
|
||
function getPos(e) {
|
||
const r = sigCanvas.getBoundingClientRect();
|
||
const t = e.touches ? e.touches[0] : e;
|
||
return { x: (t.clientX - r.left) * (sigCanvas.width / r.width), y: (t.clientY - r.top) * (sigCanvas.height / r.height) };
|
||
}
|
||
|
||
sigCanvas.addEventListener('mousedown', e => { sigDrawing = true; const p = getPos(e); sigCtx.beginPath(); sigCtx.moveTo(p.x, p.y); });
|
||
sigCanvas.addEventListener('mousemove', e => { if (!sigDrawing) return; const p = getPos(e); sigCtx.lineTo(p.x, p.y); sigCtx.stroke(); sigHasData = true; document.getElementById('sig-hint').style.opacity = '0'; });
|
||
sigCanvas.addEventListener('mouseup', () => sigDrawing = false);
|
||
sigCanvas.addEventListener('touchstart', e => { e.preventDefault(); sigDrawing = true; const p = getPos(e); sigCtx.beginPath(); sigCtx.moveTo(p.x, p.y); }, { passive:false });
|
||
sigCanvas.addEventListener('touchmove', e => { e.preventDefault(); if (!sigDrawing) return; const p = getPos(e); sigCtx.lineTo(p.x, p.y); sigCtx.stroke(); sigHasData = true; document.getElementById('sig-hint').style.opacity = '0'; }, { passive:false });
|
||
sigCanvas.addEventListener('touchend', () => sigDrawing = false);
|
||
}
|
||
|
||
function clearSig() {
|
||
if (sigCtx) sigCtx.clearRect(0, 0, sigCanvas.width, sigCanvas.height);
|
||
sigHasData = false;
|
||
const hint = document.getElementById('sig-hint');
|
||
if (hint) hint.style.opacity = '1';
|
||
}
|
||
|
||
// Submit
|
||
function submitReport() {
|
||
const btn = document.getElementById('submit-btn');
|
||
btn.disabled = true; btn.textContent = '⏳ Skickar...';
|
||
|
||
const m = MISSIONS[currentMission];
|
||
const reportNum = `LVX-${new Date().getFullYear()}-${String(Math.floor(Math.random()*9000)+1000)}`;
|
||
|
||
// Offline: save to localStorage
|
||
const reports = JSON.parse(localStorage.getItem('landvex_reports') || '[]');
|
||
reports.push({
|
||
id: reportNum, mission: currentMission, object: m.name, type: m.type,
|
||
checklist: checklistState, timestamp: new Date().toISOString(),
|
||
synced: false,
|
||
});
|
||
localStorage.setItem('landvex_reports', JSON.stringify(reports));
|
||
|
||
// Try online sync
|
||
setTimeout(() => {
|
||
document.getElementById('report-num').textContent = reportNum;
|
||
document.getElementById('conf-object').textContent = m.name;
|
||
document.getElementById('conf-type').textContent = m.type;
|
||
document.getElementById('conf-time').textContent = new Date().toLocaleString('sv-SE');
|
||
showScreen('screen-confirm');
|
||
document.getElementById('main-tab-bar').style.display = 'none';
|
||
}, 1500);
|
||
}
|
||
|
||
// Offline state
|
||
function saveOfflineState() {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ currentMission, currentStep, checklistState }));
|
||
} catch(e) {}
|
||
}
|
||
|
||
function pullRefresh() {
|
||
const ptr = document.getElementById('ptr');
|
||
ptr.classList.add('active');
|
||
setTimeout(() => ptr.classList.remove('active'), 1500);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|