2026-07-05 06:41:32 +00:00
<!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" >
< 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 : 390 px ; 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.5 px solid var ( - - separator ) ;
padding : 0 16 px 12 px ;
padding-top : calc ( 14 px + env ( safe - area - inset - top ) ) ;
position : fixed ; top : 0 ; left : 0 ; right : 0 ;
max-width : 390 px ; margin : 0 auto ;
z-index : 1000 ;
}
. nav-row { display : flex ; align-items : center ; justify-content : space-between ; }
. nav-title { font-size : 17 px ; font-weight : 600 ; }
. nav-date { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
. nav-avatar {
2026-07-07 07:11:50 +00:00
width : 32 px ; height : 32 px ; border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
background : var ( - - ios - blue ) ; color : white ;
display : flex ; align-items : center ; justify-content : center ;
font-size : 14 px ; font-weight : 700 ;
}
. refresh-btn {
background : none ; border : none ; font-size : 20 px ; cursor : pointer ;
padding : 4 px ; color : var ( - - ios - blue ) ;
}
/* Scroll area */
. scroll-area {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; bottom : 0 ;
max-width : 390 px ; margin : 0 auto ;
overflow-y : auto ; -webkit- overflow-scrolling : touch ;
padding-bottom : calc ( 80 px + env ( safe - area - inset - bottom ) ) ;
}
. scroll-inner { padding-top : 96 px ; }
/* Pull-to-refresh indicator */
. ptr-indicator {
text-align : center ; padding : 12 px ; font-size : 13 px ;
color : var ( - - label - secondary ) ; display : none ;
}
. ptr-indicator . active { display : block ; }
/* Screens */
. screen { display : none ; }
. screen . active { display : block ; }
/* Sections */
. section-header {
font-size : 13 px ; font-weight : 600 ; color : var ( - - label - secondary ) ;
text-transform : uppercase ; letter-spacing : 0.5 px ;
padding : 0 20 px ; margin : 20 px 0 8 px ;
}
/* Mission cards */
. mission-card {
margin : 0 16 px 10 px ;
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
cursor : pointer ;
transition : transform 0.15 s , box-shadow 0.15 s ;
-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 : 14 px 16 px ; gap : 12 px ; }
. mission-type-icon {
2026-07-07 07:11:50 +00:00
width : 44 px ; height : 44 px ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
display : flex ; align-items : center ; justify-content : center ;
font-size : 22 px ; 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 : 16 px ; font-weight : 600 ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; }
. mission-addr { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
. mission-meta { display : flex ; align-items : center ; gap : 6 px ; margin-top : 6 px ; }
. mission-priority {
2026-07-07 07:11:50 +00:00
padding : 2 px 8 px ; border-radius : var ( - - radius - sm ) ;
2026-07-05 06:41:32 +00:00
font-size : 11 px ; 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 : 12 px ; color : var ( - - label - secondary ) ; }
. mission-arrow { color : var ( - - ios - gray3 ) ; font-size : 14 px ; }
/* Map */
2026-07-07 07:11:50 +00:00
# map { height : 200 px ; margin : 0 16 px 4 px ; border-radius : var ( - - radius - md ) ; overflow : hidden ; }
2026-07-05 06:41:32 +00:00
/* Tab bar */
. tab-bar {
position : fixed ; bottom : 0 ; left : 0 ; right : 0 ;
max-width : 390 px ; margin : 0 auto ;
background : var ( - - glass - thick - bg ) ;
backdrop-filter : var ( - - glass - thick - blur ) ;
-webkit- backdrop-filter : var ( - - glass - thick - blur ) ;
border-top : 0.5 px 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 : 10 px 0 8 px ; cursor : pointer ;
color : var ( - - label - secondary ) ; font-size : 10 px ; font-weight : 500 ;
gap : 3 px ; transition : color 0.15 s ;
}
. tab-item . active { color : var ( - - ios - blue ) ; }
. tab-icon { font-size : 24 px ; line-height : 1 ; }
/* ===== INSPECTION FORM SCREEN ===== */
. form-nav {
display : flex ; align-items : center ; padding : 14 px 16 px ;
padding-top : calc ( 14 px + env ( safe - area - inset - top ) ) ;
background : var ( - - glass - bg ) ;
backdrop-filter : var ( - - glass - blur ) ;
border-bottom : 0.5 px solid var ( - - separator ) ;
gap : 8 px ;
}
. back-btn {
color : var ( - - ios - blue ) ; font-size : 16 px ; font-weight : 500 ;
background : none ; border : none ; cursor : pointer ; padding : 4 px ;
display : flex ; align-items : center ; gap : 4 px ;
}
. form-title { flex : 1 ; font-size : 17 px ; font-weight : 600 ; text-align : center ; }
. form-scroll { padding : 16 px ; }
/* Steps */
. step-indicator {
display : flex ; justify-content : center ; gap : 8 px ; margin-bottom : 20 px ;
}
. step-dot {
2026-07-07 07:11:50 +00:00
width : 8 px ; height : 8 px ; border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
background : var ( - - ios - gray4 ) ; transition : background 0.3 s ;
}
. step-dot . done { background : var ( - - ios - green ) ; }
2026-07-07 07:11:50 +00:00
. step-dot . active { background : var ( - - ios - blue ) ; width : 20 px ; border-radius : var ( - - radius - sm ) ; }
2026-07-05 06:41:32 +00:00
/* Form elements */
. form-section { margin-bottom : 24 px ; }
. form-section-title { font-size : 13 px ; font-weight : 600 ; color : var ( - - label - secondary ) ; text-transform : uppercase ; letter-spacing : 0.5 px ; margin-bottom : 10 px ; }
. form-card {
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ;
}
. form-field {
padding : 13 px 16 px ; border-bottom : 0.5 px solid var ( - - separator ) ;
}
. form-field : last-child { border-bottom : none ; }
. field-label { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-bottom : 4 px ; }
. field-input {
width : 100 % ; border : none ; outline : none ; background : transparent ;
font-size : 16 px ; font-family : var ( - - font - text ) ; color : var ( - - label - primary ) ;
-webkit- appearance : none ;
}
. field-textarea {
width : 100 % ; border : none ; outline : none ; background : transparent ;
font-size : 15 px ; font-family : var ( - - font - text ) ; color : var ( - - label - primary ) ;
resize : none ; min-height : 80 px ;
-webkit- appearance : none ;
}
select . field-input { cursor : pointer ; }
/* Checklist */
. check-item {
display : flex ; align-items : flex-start ; padding : 13 px 16 px ; gap : 12 px ;
border-bottom : 0.5 px solid var ( - - separator ) ; cursor : pointer ;
}
. check-item : last-child { border-bottom : none ; }
. check-box {
2026-07-07 07:11:50 +00:00
width : 24 px ; height : 24 px ; border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
border : 2 px solid var ( - - ios - gray3 ) ;
flex-shrink : 0 ; display : flex ; align-items : center ; justify-content : center ;
transition : all 0.15 s ;
}
. check-box . checked {
background : var ( - - ios - blue ) ; border-color : var ( - - ios - blue ) ;
color : white ; font-size : 14 px ;
}
. check-text { font-size : 15 px ; flex : 1 ; padding-top : 1 px ; }
. check-sub { font-size : 13 px ; color : var ( - - label - secondary ) ; margin-top : 2 px ; }
/* Photo grid */
. photo-grid {
display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ;
gap : 4 px ; padding : 0 ;
}
. photo-thumb {
aspect-ratio : 1 ; background : var ( - - fill - tertiary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; display : flex ; align-items : center ; justify-content : center ;
2026-07-05 06:41:32 +00:00
cursor : pointer ; overflow : hidden ; position : relative ;
transition : opacity 0.15 s ;
}
. photo-thumb : active { opacity : 0.7 ; }
. photo-add-icon { font-size : 32 px ; color : var ( - - ios - blue ) ; }
. photo-preview { width : 100 % ; height : 100 % ; object-fit : cover ; display : none ; }
. photo-delete {
position : absolute ; top : 4 px ; right : 4 px ;
2026-07-07 07:11:50 +00:00
width : 20 px ; height : 20 px ; border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
background : rgba ( 242 , 242 , 247 , 0.5 ) ; color : white ;
font-size : 12 px ; display : none ; align-items : center ; justify-content : center ;
cursor : pointer ;
}
/* Signature canvas */
. sig-wrap {
2026-07-07 07:11:50 +00:00
background : var ( - - fill - quaternary ) ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
overflow : hidden ; position : relative ; margin : 0 16 px ;
}
# 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 : 13 px ; pointer-events : none ;
transition : opacity 0.2 s ;
}
. sig-actions { display : flex ; gap : 8 px ; padding : 10 px 16 px ; }
. sig-clear-btn {
flex : 1 ; background : var ( - - fill - tertiary ) ; border : none ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; padding : 10 px ; font-size : 14 px ; font-weight : 500 ;
2026-07-05 06:41:32 +00:00
color : var ( - - ios - red ) ; cursor : pointer ; font-family : var ( - - font - text ) ;
}
/* Submit button */
. submit-btn {
display : block ; width : calc ( 100 % - 32 px ) ; margin : 16 px ;
background : var ( - - ios - blue ) ; color : white ; border : none ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; padding : 15 px ; font-size : 16 px ; font-weight : 600 ;
2026-07-05 06:41:32 +00:00
cursor : pointer ; font-family : var ( - - font - text ) ;
box-shadow : 0 2 px 12 px rgba ( 0 , 122 , 255 , 0.3 ) ;
transition : opacity 0.15 s ;
}
. submit-btn : active { opacity : 0.8 ; }
. submit-btn : disabled { opacity : 0.4 ; cursor : not-allowed ; }
/* Step nav buttons */
. step-nav {
display : flex ; gap : 12 px ; padding : 0 16 px 16 px ;
}
. step-btn {
2026-07-07 07:11:50 +00:00
flex : 1 ; padding : 13 px ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
border : none ; font-size : 15 px ; font-weight : 600 ;
cursor : pointer ; font-family : var ( - - font - text ) ;
transition : opacity 0.15 s ;
}
. 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 2 px 10 px rgba ( 0 , 122 , 255 , 0.25 ) ; }
/* ===== CONFIRMATION SCREEN ===== */
. confirm-screen {
display : flex ; flex-direction : column ; align-items : center ;
justify-content : center ; min-height : 100 vh ; padding : 40 px 24 px ;
text-align : center ;
}
. confirm-icon {
2026-07-07 07:11:50 +00:00
width : 80 px ; height : 80 px ; border-radius : var ( - - radius - full ) ;
2026-07-05 06:41:32 +00:00
background : rgba ( 52 , 199 , 89 , 0.15 ) ; border : 2 px solid var ( - - ios - green ) ;
display : flex ; align-items : center ; justify-content : center ;
font-size : 40 px ; margin-bottom : 24 px ;
animation : pop-in 0.5 s cubic-bezier ( 0.34 , 1.56 , 0.64 , 1 ) ;
}
@ keyframes pop-in { 0 % { transform : scale ( 0 ) } 100 % { transform : scale ( 1 ) } }
. confirm-title { font-size : 24 px ; font-weight : 700 ; margin-bottom : 8 px ; }
. confirm-report-num {
font-size : 13 px ; color : var ( - - label - secondary ) ;
2026-07-07 07:11:50 +00:00
background : var ( - - fill - tertiary ) ; padding : 6 px 16 px ; border-radius : var ( - - radius - xl ) ;
2026-07-05 06:41:32 +00:00
font-family : var ( - - font - mono ) ; margin-bottom : 24 px ;
}
. confirm-details {
background : var ( - - grouped - background - secondary ) ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; width : 100 % ; text-align : left ; overflow : hidden ;
2026-07-05 06:41:32 +00:00
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.08 ) ; margin-bottom : 24 px ;
}
. confirm-row {
display : flex ; justify-content : space-between ; padding : 12 px 16 px ;
border-bottom : 0.5 px solid var ( - - separator ) ; font-size : 14 px ;
}
. 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 ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; padding : 14 px ; font-size : 16 px ; font-weight : 600 ;
2026-07-05 06:41:32 +00:00
cursor : pointer ; font-family : var ( - - font - text ) ;
box-shadow : 0 2 px 12 px rgba ( 0 , 122 , 255 , 0.3 ) ;
}
@ media ( max-width : 640px ) {
. hero h1 { font-size : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !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 : 2 rem !important ; }
. hero p { font-size : 1 rem !important ; }
. card { padding : 1.5 rem !important ; }
. grid { grid-template-columns : 1 fr !important ; }
. btn { padding : 0.75 rem 1.5 rem !important ; font-size : 1 rem !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" : "Tyresö" ,
"addressRegion" : "Stockholm" ,
"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" : "Tyresö" ,
"addressRegion" : "Stockholm" ,
"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 >
2026-07-07 07:11:50 +00:00
< div class = "nav-title" > Dagens Uppdrag< / div >
2026-07-05 06:41:32 +00:00
< div class = "nav-date" id = "today-date" > —< / div >
< / div >
< div style = "display:flex;gap:10px;align-items:center" >
< 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" >
2026-07-07 07:11:50 +00:00
< div class = "mission-type-icon type-bridge" > < / div >
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< div class = "mission-type-icon type-road" > < / div >
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< div class = "mission-type-icon type-roof" > < / div >
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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 >
2026-07-07 07:11:50 +00:00
< span class = "confirm-val" style = "color:var(--ios-green)" > < 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 >
2026-07-05 06:41:32 +00:00
< / 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')" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< span > Uppdrag< / span >
< / div >
< div class = "tab-item" onclick = "switchTab('map-tab')" >
2026-07-07 07:11:50 +00:00
< div class = "tab-icon" > < / div >
2026-07-05 06:41:32 +00:00
< span > Karta< / span >
< / div >
< div class = "tab-item" onclick = "switchTab('reports')" >
2026-07-07 07:11:50 +00:00
< div class = "tab-icon" > < / div >
2026-07-05 06:41:32 +00:00
< span > Rapporter< / span >
< / div >
< div class = "tab-item" onclick = "switchTab('profile')" >
2026-07-07 07:11:50 +00:00
< div class = "tab-icon" > < / div >
2026-07-05 06:41:32 +00:00
< span > Profil< / span >
< / div >
< / div >
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" > < / script >
< script >
2026-07-07 07:11:50 +00:00
// State
2026-07-05 06:41:32 +00:00
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' } ,
] ,
} ;
2026-07-07 07:11:50 +00:00
// Init
2026-07-05 06:41:32 +00:00
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> ` ) ;
} ) ;
2026-07-07 07:11:50 +00:00
// Screens
2026-07-05 06:41:32 +00:00
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' } ) ;
}
}
2026-07-07 07:11:50 +00:00
// Missions
2026-07-05 06:41:32 +00:00
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">
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</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">
2026-07-07 07:11:50 +00:00
<option> Soligt</option>
<option> Molnigt</option>
<option> Regn</option>
<option> Snö</option>
2026-07-05 06:41:32 +00:00
</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 } ">
2026-07-07 07:11:50 +00:00
${ 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>' : '' }
2026-07-05 06:41:32 +00:00
</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">
2026-07-07 07:11:50 +00:00
<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>
2026-07-05 06:41:32 +00:00
</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" style="padding:12px">
<div class="photo-grid"> ${ photoSlots } </div>
</div>
<div style="font-size:12px;color:var(--label-secondary);padding:8px 4px">
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" style="display:none" onchange="handlePhoto(event)"> ` ;
} else if ( currentStep === 3 ) {
// Step 4: Signature
content = `
<div class="form-scroll" style="padding-bottom:0">
<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>
2026-07-07 07:11:50 +00:00
<div class="sig-label" id="sig-hint"> Rita din signatur här</div>
2026-07-05 06:41:32 +00:00
</div>
<div class="sig-actions">
2026-07-07 07:11:50 +00:00
<button class="sig-clear-btn" onclick="clearSig()"> Rensa</button>
2026-07-05 06:41:32 +00:00
</div>
</div>
</div>
<div style="padding:0 16px">
2026-07-07 07:11:50 +00:00
<button class="submit-btn" id="submit-btn" onclick="submitReport()"> Skicka in rapport</button>
2026-07-05 06:41:32 +00:00
</div>
<div style="padding:0 16px 16px">
<button class="step-btn step-prev" style="width:100%;margin-top:8px" 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 ;
}
2026-07-07 07:11:50 +00:00
// Photos
2026-07-05 06:41:32 +00:00
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' ;
}
2026-07-07 07:11:50 +00:00
// Signature
2026-07-05 06:41:32 +00:00
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' ;
}
2026-07-07 07:11:50 +00:00
// Submit
2026-07-05 06:41:32 +00:00
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 ) ;
}
2026-07-07 07:11:50 +00:00
// Offline state
2026-07-05 06:41:32 +00:00
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 >