2026-07-05 06:41:32 +00:00
<!DOCTYPE html>
< html lang = "sv" data-theme = "light" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1, viewport-fit=cover" >
< meta name = "color-scheme" content = "light" >
< meta name = "apple-native-compliance" content = "pass" >
< title > quiXzoom — Kunddashboard< / title >
< link rel = "stylesheet" >
< link rel = "stylesheet" >
< script defer > < / script >
< style >
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 }
html { -webkit- text-size-adjust : 100 % ; color-scheme : light }
body { font-family : - apple-system , 'SF Pro Text' , system-ui , sans-serif ; background : #F2F2F7 ; color : #000 ; -webkit- font-smoothing : antialiased ; min-height : 100 dvh ; padding-bottom : calc ( 83 px + env ( safe - area - inset - bottom , 16 px ) ) }
/* Nav */
. nav-bar { position : sticky ; top : 0 ; z-index : 200 ; padding-top : env ( safe - area - inset - top , 0 px ) ; background : rgba ( 249 , 249 , 249 , .94 ) ; backdrop-filter : blur ( 40 px ) saturate ( 180 % ) ; -webkit- backdrop-filter : blur ( 40 px ) saturate ( 180 % ) ; border-bottom : .33 px solid rgba ( 60 , 60 , 67 , .29 ) }
. nav-inner { height : 44 px ; display : flex ; align-items : center ; padding : 0 16 px ; gap : 12 px }
. nav-title { flex : 1 ; text-align : center ; font-size : 17 px ; font-weight : 600 ; letter-spacing : -.4 px }
. nav-logo { font-size : 20 px ; font-weight : 900 ; background : linear-gradient ( 135 deg , #FF6B35 , #F7C948 ) ; -webkit- background-clip : text ; -webkit- text-fill-color : transparent ; letter-spacing : -.5 px }
2026-07-07 07:11:50 +00:00
. nav-btn-icon { width : 32 px ; height : 32 px ; border-radius : var ( - - radius - lg ) ; background : #007AFF ; color : #fff ; border : none ; font-size : 14 px ; display : flex ; align-items : center ; justify-content : center ; cursor : pointer }
2026-07-05 06:41:32 +00:00
/* Customer switcher */
. customer-switcher { background : #fff ; border-bottom : .33 px solid rgba ( 60 , 60 , 67 , .12 ) ; padding : 10 px 16 px ; display : flex ; gap : 8 px ; overflow-x : auto ; scrollbar-width : none }
. customer-switcher :: -webkit-scrollbar { display : none }
2026-07-07 07:11:50 +00:00
. cust-chip { padding : 7 px 14 px ; border-radius : var ( - - radius - xl ) ; font-size : 13 px ; font-weight : 600 ; cursor : pointer ; border : none ; background : #F2F2F7 ; color : #000 ; white-space : nowrap ; transition : all .15 s ; -webkit- tap-highlight-color : transparent }
2026-07-05 06:41:32 +00:00
. cust-chip . active { background : #007AFF ; color : #fff }
/* Large title */
. large-title-area { padding : 12 px 16 px 8 px }
. large-title { font-size : 34 px ; font-weight : 700 ; letter-spacing : .37 px ; line-height : 1.1 }
. large-sub { font-size : 15 px ; color : #666666 ; margin-top : 3 px }
/* Tab bar */
. tab-bar { position : fixed ; bottom : 0 ; left : 0 ; right : 0 ; z-index : 200 ; padding-bottom : env ( safe - area - inset - bottom , 0 px ) ; background : rgba ( 249 , 249 , 249 , .94 ) ; backdrop-filter : blur ( 40 px ) saturate ( 180 % ) ; -webkit- backdrop-filter : blur ( 40 px ) saturate ( 180 % ) ; border-top : .33 px solid rgba ( 60 , 60 , 67 , .29 ) ; display : flex }
. tab-item { flex : 1 ; display : flex ; flex-direction : column ; align-items : center ; justify-content : center ; padding : 8 px 4 px 4 px ; cursor : pointer ; gap : 2 px ; -webkit- tap-highlight-color : transparent }
. tab-icon { font-size : 22 px ; line-height : 1 }
. tab-label { font-size : 10 px ; font-weight : 500 ; color : #666666 }
. tab-item . active . tab-label { color : #007AFF }
. tab-item : not ( . active ) . tab-icon { opacity : .5 }
/* Content */
. content { display : none ; padding-bottom : 8 px }
. content . active { display : block }
/* KPI strip */
. kpi-strip { display : grid ; grid-template-columns : repeat ( 3 , 1 fr ) ; gap : 10 px ; padding : 12 px 16 px 8 px }
2026-07-07 07:11:50 +00:00
. kpi-box { background : #fff ; border-radius : var ( - - radius - md ) ; padding : 14 px 12 px ; text-align : center ; box-shadow : 0 1 px 0 rgba ( 0 , 0 , 0 , .08 ) }
2026-07-05 06:41:32 +00:00
. kpi-val { font-size : 20 px ; font-weight : 700 ; letter-spacing : -.5 px }
. kpi-val-accent { color : #FF6B35 }
. kpi-lbl { font-size : 10 px ; color : #666666 ; font-weight : 500 ; margin-top : 2 px }
/* Section */
. section { margin : 8 px 0 }
. section-header { font-size : 13 px ; font-weight : 600 ; color : #555555 ; text-transform : uppercase ; padding : 0 32 px 6 px }
/* Card list */
2026-07-07 07:11:50 +00:00
. card-list { background : #fff ; border-radius : var ( - - radius - md ) ; margin : 0 16 px ; overflow : hidden ; box-shadow : 0 1 px 0 rgba ( 0 , 0 , 0 , .08 ) }
2026-07-05 06:41:32 +00:00
. card-item { display : flex ; align-items : center ; gap : 12 px ; padding : 13 px 16 px ; border-bottom : .33 px solid rgba ( 60 , 60 , 67 , .12 ) ; -webkit- tap-highlight-color : transparent ; transition : background .1 s }
. card-item : last-child { border-bottom : none }
. card-item : active { background : #F2F2F7 }
2026-07-07 07:11:50 +00:00
. card-icon { width : 40 px ; height : 40 px ; border-radius : var ( - - radius - md ) ; display : flex ; align-items : center ; justify-content : center ; font-size : 20 px ; flex-shrink : 0 }
2026-07-05 06:41:32 +00:00
. ci-blue { background : #EBF5FF } . ci-green { background : #E8F8EE } . ci-orange { background : #FFF3E0 } . ci-red { background : #FFE8E6 } . ci-purple { background : #F4EEFF } . ci-gray { background : #F2F2F7 }
. card-body { flex : 1 ; min-width : 0 }
. card-title { font-size : 15 px ; font-weight : 500 ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis }
. card-sub { font-size : 13 px ; color : #666666 ; white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis }
. card-right { display : flex ; align-items : center ; gap : 6 px ; flex-shrink : 0 }
. chevron { color : #C7C7CC ; font-size : 16 px }
/* Badges */
2026-07-07 07:11:50 +00:00
. badge { display : inline-flex ; align-items : center ; padding : 3 px 8 px ; border-radius : var ( - - radius - sm ) ; font-size : 12 px ; font-weight : 600 ; white-space : nowrap }
2026-07-05 06:41:32 +00:00
. bg-green { background : #E8F8EE ; color : #1D7A3F }
. bg-orange { background : #FFF3E0 ; color : #C07000 }
. bg-blue { background : #EBF5FF ; color : #0051A8 }
. bg-red { background : #FFE8E6 ; color : #C0392B }
. bg-gray { background : #F2F2F7 ; color : #555555 }
. bg-purple { background : #F4EEFF ; color : #6B2FA0 }
/* Star rating */
. stars { color : #FFD60A ; font-size : 13 px }
/* Favorite zoomer card */
2026-07-07 07:11:50 +00:00
. fav-card { background : #fff ; border-radius : var ( - - radius - lg ) ; margin : 0 16 px ; padding : 16 px ; display : flex ; align-items : center ; gap : 14 px ; box-shadow : 0 1 px 0 rgba ( 0 , 0 , 0 , .08 ) ; margin-bottom : 8 px }
. fav-avatar { width : 52 px ; height : 52 px ; border-radius : 26 px ; background : linear-gradient ( 135 deg , #FF6B35 , #F7C948 ) ; display : flex ; align-items : center ; justify-content : center ; font-size : 24 px ; flex-shrink : 0 }
2026-07-05 06:41:32 +00:00
. fav-name { font-size : 16 px ; font-weight : 700 }
. fav-spec { font-size : 13 px ; color : #666666 }
. fav-meta { font-size : 12 px ; color : #666666 ; margin-top : 4 px }
. fav-actions { margin-left : auto ; display : flex ; gap : 8 px }
/* Chart bar */
. bar-chart-wrap { padding : 16 px }
. bar-chart-title { font-size : 15 px ; font-weight : 600 ; margin-bottom : 14 px }
. bar-row { display : flex ; align-items : center ; gap : 10 px ; margin-bottom : 10 px }
. bar-label { font-size : 12 px ; color : #555555 ; width : 80 px ; flex-shrink : 0 }
2026-07-07 07:11:50 +00:00
. bar-track { flex : 1 ; height : 8 px ; background : #E9E9EB ; border-radius : var ( - - radius - sm ) ; overflow : hidden }
. bar-fill { height : 8 px ; border-radius : var ( - - radius - sm ) ; background : linear-gradient ( 90 deg , #FF6B35 , #F7C948 ) ; transition : width .7 s ease }
2026-07-05 06:41:32 +00:00
. bar-val { font-size : 12 px ; color : #555555 ; width : 80 px ; text-align : right ; flex-shrink : 0 }
/* Book modal */
. modal-overlay { position : fixed ; inset : 0 ; background : rgba ( 242 , 242 , 247 , .4 ) ; z-index : 300 ; display : none ; align-items : flex-end }
. modal-overlay . open { display : flex }
2026-07-07 07:11:50 +00:00
. modal { background : #fff ; border-radius : var ( - - radius - xl ) 20 px 0 0 ; padding : 20 px 20 px calc ( 20 px + env ( safe - area - inset - bottom , 0 px ) ) ; width : 100 % ; max-height : 90 dvh ; overflow-y : auto }
. modal-handle { width : 36 px ; height : 4 px ; border-radius : var ( - - radius - sm ) ; background : #C7C7CC ; margin : 0 auto 16 px }
2026-07-05 06:41:32 +00:00
. modal-title { font-size : 20 px ; font-weight : 700 ; margin-bottom : 4 px }
. modal-sub { font-size : 14 px ; color : #666666 ; margin-bottom : 20 px }
. form-group { margin-bottom : 14 px }
. form-label { font-size : 13 px ; font-weight : 600 ; color : #555555 ; margin-bottom : 6 px ; display : block }
2026-07-07 07:11:50 +00:00
. form-input , . form-select { width : 100 % ; background : #F2F2F7 ; border : none ; border-radius : var ( - - radius - md ) ; padding : 12 px 14 px ; color : #000 ; font-size : 15 px ; font-family : inherit ; outline : none }
2026-07-05 06:41:32 +00:00
. btn-row { display : flex ; flex-direction : column ; gap : 10 px ; margin-top : 20 px }
2026-07-07 07:11:50 +00:00
. btn { display : flex ; align-items : center ; justify-content : center ; gap : 8 px ; padding : 13 px ; border-radius : var ( - - radius - md ) ; font-size : 17 px ; font-weight : 600 ; cursor : pointer ; border : none ; -webkit- tap-highlight-color : transparent ; transition : opacity .15 s }
2026-07-05 06:41:32 +00:00
. btn : active { opacity : .7 }
. btn-orange { background : linear-gradient ( 135 deg , #FF6B35 , #FF9500 ) ; color : #fff }
. btn-gray { background : #F2F2F7 ; color : #000 }
/* Empty */
. empty { padding : 32 px ; text-align : center ; color : #666666 }
. empty-icon { font-size : 36 px ; margin-bottom : 8 px }
/* Toast */
2026-07-07 07:11:50 +00:00
. toast { position : fixed ; bottom : calc ( 83 px + 12 px + env ( safe - area - inset - bottom , 0 px ) ) ; left : 50 % ; transform : translateX ( -50 % ) translateY ( 60 px ) ; background : #323232 ; color : #fff ; padding : 10 px 18 px ; border-radius : var ( - - radius - md ) ; font-size : 14 px ; font-weight : 600 ; z-index : 999 ; transition : transform .3 s ; white-space : nowrap }
2026-07-05 06:41:32 +00:00
. toast . show { transform : translateX ( -50 % ) translateY ( 0 ) }
@ 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 ; }
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-05 06:41:32 +00:00
< / 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(255,255,255,0.4)" ] { color : rgba ( 255 , 255 , 255 , 0.72 ) !important ; }
[ style * = "rgba(255,255,255,0.3)" ] { color : rgba ( 255 , 255 , 255 , 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 ; }
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-05 06:41:32 +00:00
< / style >
2026-07-14 09:58:53 +00:00
< script type = "module" src = "https://auth.quixzoom.com/site-auth-snippet.js" > < / script >
2026-07-05 06:41:32 +00:00
< / head >
< body >
<!-- Nav -->
< div class = "nav-bar" >
< div class = "nav-inner" >
< div class = "nav-logo" > quiX< / div >
< div class = "nav-title" id = "nav-title" > Kunddashboard< / div >
2026-07-07 07:11:50 +00:00
< button class = "btn btn-orange" style = "padding:7px 14px;font-size:14px;font-weight:700;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick = "openBookModal()" > + Boka< / button >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
<!-- Customer switcher -->
< div class = "customer-switcher" id = "customer-switcher" > < / div >
<!-- Large title -->
< div class = "large-title-area" >
< div class = "large-title" id = "large-title" > Laddar…< / div >
< div class = "large-sub" id = "large-sub" > < / div >
< / div >
<!-- DASHBOARD -->
< div class = "content active" id = "tab-dashboard" >
< div class = "kpi-strip" id = "kpi-strip" > < / div >
< div style = "height:8px" > < / div >
< div class = "section" >
< div class = "section-header" > Aktiva uppdrag< / div >
< div class = "card-list" id = "active-missions" > < / div >
< / div >
< div style = "height:8px" > < / div >
< div class = "section" >
< div class = "section-header" > Senaste uppdrag< / div >
< div class = "card-list" id = "recent-missions" > < / div >
< / div >
< / div >
<!-- HISTORY -->
< div class = "content" id = "tab-history" >
< div style = "height:12px" > < / div >
2026-07-07 07:11:50 +00:00
< div style = "background:#fff;border-radius: var(--radius-md);margin:0 16px" id = "spending-chart" > < / div >
2026-07-05 06:41:32 +00:00
< div style = "height:12px" > < / div >
< div class = "section" >
< div class = "section-header" > Alla uppdrag< / div >
< div class = "card-list" id = "all-missions" > < / div >
< / div >
< / div >
<!-- BILLING -->
< div class = "content" id = "tab-billing" >
< div style = "height:12px" > < / div >
< div class = "section" >
< div class = "section-header" > Fakturor< / div >
< div class = "card-list" id = "invoices-list" > < / div >
< / div >
< / div >
<!-- FAVORITES -->
< div class = "content" id = "tab-favorites" >
< div style = "height:12px" > < / div >
< div style = "padding:0 16px 12px;display:flex;justify-content:flex-end" >
2026-07-07 07:11:50 +00:00
< button class = "btn btn-orange" style = "font-size:14px;padding:8px 16px;border-radius: var(--radius-md);border:none;color:#fff;background:linear-gradient(135deg,#FF6B35,#FF9500);cursor:pointer" onclick = "openAddFavModal()" > + Favorit< / button >
2026-07-05 06:41:32 +00:00
< / div >
< div id = "favs-list" > < / div >
< / div >
<!-- Tab bar -->
2026-07-14 15:27:33 +00:00
< nav class = "qz-nav" class = "tab-bar" >
2026-07-07 07:11:50 +00:00
< div class = "tab-item active" onclick = "showTab('dashboard',this,'Hem')" > < div class = "tab-icon" > < / div > < div class = "tab-label" > Hem< / div > < / div >
< div class = "tab-item" onclick = "showTab('history',this,'Historik')" > < 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" > Historik< / div > < / div >
< div class = "tab-item" onclick = "showTab('billing',this,'Fakturering')" > < div class = "tab-icon" > < / div > < div class = "tab-label" > Fakturering< / div > < / div >
< div class = "tab-item" onclick = "showTab('favorites',this,'Favoriter')" > < div class = "tab-icon" > < / div > < div class = "tab-label" > Favoriter< / div > < / div >
2026-07-05 06:41:32 +00:00
< / nav >
<!-- Book modal -->
< div class = "modal-overlay" id = "book-modal" >
< div class = "modal" >
< div class = "modal-handle" > < / div >
< div class = "modal-title" > Boka uppdrag< / div >
< div class = "modal-sub" id = "book-sub" > Beskriv ditt uppdrag< / div >
< div class = "form-group" > < label class = "form-label" > Titel< / label > < input class = "form-input" id = "bk-title" placeholder = "Vad behöver du hjälp med?" > < / div >
2026-07-07 07:11:50 +00:00
< div class = "form-group" > < label class = "form-label" > Typ< / label > < select class = "form-select" id = "bk-type" > < option value = "photography" > Fotografi < / option > < option value = "video" > Video < / option > < option value = "drone" > Drönare < 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 > < / option > < / select > < / div >
2026-07-05 06:41:32 +00:00
< div class = "form-group" > < label class = "form-label" > Startdatum & tid< / label > < input class = "form-input" type = "datetime-local" id = "bk-start" > < / div >
< div class = "form-group" > < label class = "form-label" > Duration (timmar)< / label > < input class = "form-input" type = "number" id = "bk-duration" value = "2" min = "1" > < / div >
< div class = "form-group" > < label class = "form-label" > Plats< / label > < input class = "form-input" id = "bk-location" placeholder = "Var ska uppdraget utföras?" > < / div >
< div class = "form-group" > < label class = "form-label" > Budget (SEK)< / label > < input class = "form-input" type = "number" id = "bk-budget" placeholder = "2500" > < / div >
< div class = "btn-row" >
2026-07-07 07:11:50 +00:00
< button class = "btn btn-orange" onclick = "submitBook()" > < svg width = "16" height = "16" viewBox = "0 0 16 16" fill = "none" > < path d = "M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill = "#666" / > < / svg > Boka nu< / button >
2026-07-05 06:41:32 +00:00
< button class = "btn btn-gray" onclick = "closeModal('book-modal')" > Avbryt< / button >
< / div >
< / div >
< / div >
<!-- Add favorite modal -->
< div class = "modal-overlay" id = "fav-modal" >
< div class = "modal" >
< div class = "modal-handle" > < / div >
< div class = "modal-title" > Lägg till favorit< / div >
< div class = "modal-sub" > Lägg till en zoomer som favorit< / div >
< div class = "form-group" > < label class = "form-label" > Zoomer ID< / label > < input class = "form-input" id = "fav-id" placeholder = "t.ex. zoomer-xyz" > < / div >
< div class = "form-group" > < label class = "form-label" > Namn< / label > < input class = "form-input" id = "fav-name" placeholder = "Förnamn E." > < / div >
< div class = "form-group" > < label class = "form-label" > Specialitet< / label > < select class = "form-select" id = "fav-spec" > < option > Photography< / option > < option > Video< / option > < option > Drone< / option > < option > General< / option > < / select > < / div >
< div class = "btn-row" >
2026-07-07 07:11:50 +00:00
< button class = "btn btn-orange" onclick = "submitFav()" > Lägg till< / button >
2026-07-05 06:41:32 +00:00
< button class = "btn btn-gray" onclick = "closeModal('fav-modal')" > Avbryt< / button >
< / div >
< / div >
< / div >
< div class = "toast" id = "toast" > < / div >
< script >
2026-07-07 07:11:50 +00:00
// State
2026-07-05 06:41:32 +00:00
const CUSTOMERS = [ 'cust-101' , 'cust-102' , 'cust-103' ] ;
let activeCustomer = 'cust-101' ;
let customerCache = { } ;
let missionCache = { } ;
let spendingCache = { } ;
let invoiceCache = { } ;
let favCache = { } ;
2026-07-07 07:11:50 +00:00
// Utils
2026-07-05 06:41:32 +00:00
function fmtMoney ( n ) { return new Intl . NumberFormat ( 'sv-SE' ) . format ( n ) + ' kr' }
function fmtDate ( s ) { if ( ! s ) return '—' ; return new Date ( s ) . toLocaleDateString ( 'sv-SE' , { day : '2-digit' , month : 'short' } ) }
function fmtDateLong ( s ) { if ( ! s ) return '—' ; return new Date ( s ) . toLocaleDateString ( 'sv-SE' , { day : '2-digit' , month : 'long' , year : 'numeric' } ) }
function toast ( msg ) { const el = document . getElementById ( 'toast' ) ; el . textContent = msg ; el . classList . add ( 'show' ) ; setTimeout ( ( ) => el . classList . remove ( 'show' ) , 2500 ) }
2026-07-07 07:11:50 +00:00
function statusBadge ( s ) { return { completed : '<span class="badge bg-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> Klar</span>' , active : '<span class="badge bg-blue"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Aktiv</span>' , open : '<span class="badge bg-orange"><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> Öppen</span>' , cancelled : '<span class="badge bg-red"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Avbruten</span>' } [ s ] || ` <span class="badge bg-gray"> ${ s } </span> ` }
function missionIcon ( t ) { return { photography : '' , video : '' , drone : '<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>' } [ t ] || '<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>' }
2026-07-05 06:41:32 +00:00
function missionIconClass ( s ) { return { completed : 'ci-green' , active : 'ci-blue' , open : 'ci-orange' , cancelled : 'ci-red' } [ s ] || 'ci-gray' }
2026-07-07 07:11:50 +00:00
function stars ( r ) { if ( ! r ) return '<span style="color:#666666;font-size:12px">Väntar</span>' ; return '<span class="stars">' + ( '' . repeat ( Math . round ( r ) ) + '' . repeat ( 5 - Math . round ( r ) ) ) + '</span><span style="font-size:12px;color:#666666;margin-left:4px">' + r + '</span>' }
2026-07-05 06:41:32 +00:00
function invStatusBadge ( s ) { return s === 'paid' ? '<span class="badge bg-green">Betald</span>' : '<span class="badge bg-orange">Obetald</span>' }
function closeModal ( id ) { document . getElementById ( id ) . classList . remove ( 'open' ) }
document . querySelectorAll ( '.modal-overlay' ) . forEach ( m => m . addEventListener ( 'click' , e => { if ( e . target === m ) m . classList . remove ( 'open' ) } ) ) ;
2026-07-07 07:11:50 +00:00
// Tab
2026-07-05 06:41:32 +00:00
function showTab ( id , el , title ) {
document . querySelectorAll ( '.content' ) . forEach ( c => c . classList . remove ( 'active' ) ) ;
document . querySelectorAll ( '.tab-item' ) . forEach ( t => t . classList . remove ( 'active' ) ) ;
document . getElementById ( 'tab-' + id ) . classList . add ( 'active' ) ;
el . classList . add ( 'active' ) ;
document . getElementById ( 'nav-title' ) . textContent = title ;
if ( id === 'history' ) renderHistory ( ) ;
if ( id === 'billing' ) renderBilling ( ) ;
if ( id === 'favorites' ) renderFavorites ( ) ;
}
2026-07-07 07:11:50 +00:00
// Customer switcher
2026-07-05 06:41:32 +00:00
function renderSwitcher ( ) {
const c = Object . values ( customerCache ) ;
document . getElementById ( 'customer-switcher' ) . innerHTML = c . map ( cust => `
<button class="cust-chip ${ cust . id === activeCustomer ? ' active' : '' } " onclick="switchCustomer(' ${ cust . id } ')"> ${ cust . name . split ( ' ' ) . slice ( 0 , 2 ) . join ( ' ' ) } </button>
` ) . join ( '' ) ;
}
async function switchCustomer ( id ) {
activeCustomer = id ;
renderSwitcher ( ) ;
await loadCustomerData ( id ) ;
renderDashboard ( ) ;
}
2026-07-07 07:11:50 +00:00
// Data loading
2026-07-05 06:41:32 +00:00
async function loadAll ( ) {
// Load all customers
const custR = await fetch ( 'https://api.quixzoom.com/api/qz/customers' ) . then ( r => r . json ( ) ) . catch ( ( ) => ( { ok : false } ) ) ;
if ( custR . ok ) {
custR . items . forEach ( c => customerCache [ c . id ] = c ) ;
renderSwitcher ( ) ;
}
await loadCustomerData ( activeCustomer ) ;
renderDashboard ( ) ;
}
async function loadCustomerData ( id ) {
const [ mR , sR , iR , fR ] = await Promise . all ( [
fetch ( ` /api/quixzoom/customers/ ${ id } /missions ` ) . then ( r => r . json ( ) ) . catch ( ( ) => ( { ok : false } ) ) ,
fetch ( ` /api/quixzoom/customers/ ${ id } /spending ` ) . then ( r => r . json ( ) ) . catch ( ( ) => ( { ok : false } ) ) ,
fetch ( ` /api/quixzoom/customers/ ${ id } /invoices ` ) . then ( r => r . json ( ) ) . catch ( ( ) => ( { ok : false } ) ) ,
fetch ( ` /api/quixzoom/customers/ ${ id } /favorites ` ) . then ( r => r . json ( ) ) . catch ( ( ) => ( { ok : false } ) ) ,
] ) ;
if ( mR . ok ) missionCache [ id ] = mR . items || [ ] ;
if ( sR . ok ) spendingCache [ id ] = sR . spending ;
if ( iR . ok ) invoiceCache [ id ] = iR . items || [ ] ;
if ( fR . ok ) favCache [ id ] = fR . items || [ ] ;
}
2026-07-07 07:11:50 +00:00
// Dashboard
2026-07-05 06:41:32 +00:00
function renderDashboard ( ) {
const cust = customerCache [ activeCustomer ] ;
const missions = missionCache [ activeCustomer ] || [ ] ;
const spending = spendingCache [ activeCustomer ] || { } ;
if ( cust ) {
document . getElementById ( 'large-title' ) . textContent = cust . name ;
document . getElementById ( 'large-sub' ) . textContent = ` ${ cust . plan . toUpperCase ( ) } · ${ cust . contact } ` ;
document . getElementById ( 'book-sub' ) . textContent = ` Bokar åt ${ cust . name } ` ;
}
// KPIs
const active = missions . filter ( m => m . status === 'active' || m . status === 'open' ) ;
const completed = missions . filter ( m => m . status === 'completed' ) ;
document . getElementById ( 'kpi-strip' ) . innerHTML = `
<div class="kpi-box"><div class="kpi-val kpi-val-accent"> ${ active . length } </div><div class="kpi-lbl">Aktiva</div></div>
<div class="kpi-box"><div class="kpi-val"> ${ completed . length } </div><div class="kpi-lbl">Avslutade</div></div>
<div class="kpi-box"><div class="kpi-val"> ${ fmtMoney ( spending . total _completed _sek || 0 ) . replace ( ' kr' , '' ) } </div><div class="kpi-lbl">Totalt (kr)</div></div>
` ;
// Active missions
const activeMissions = missions . filter ( m => [ 'active' , 'open' ] . includes ( m . status ) ) ;
document . getElementById ( 'active-missions' ) . innerHTML = activeMissions . length
? activeMissions . map ( renderMissionCard ) . join ( '' )
2026-07-07 07:11:50 +00:00
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga aktiva uppdrag</div><div style="font-size:13px;margin-top:4px">Boka ett nytt uppdrag!</div></div>' ;
2026-07-05 06:41:32 +00:00
// Recent (completed, max 3)
const recent = missions . filter ( m => m . status === 'completed' ) . slice ( 0 , 3 ) ;
document . getElementById ( 'recent-missions' ) . innerHTML = recent . length
? recent . map ( renderMissionCard ) . join ( '' )
2026-07-07 07:11:50 +00:00
: '<div class="empty"><div class="empty-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 style="font-weight:600">Inga avslutade uppdrag</div></div>' ;
2026-07-05 06:41:32 +00:00
}
function renderMissionCard ( m ) {
return ` <div class="card-item">
<div class="card-icon ${ missionIconClass ( m . status ) } "> ${ missionIcon ( m . type ) } </div>
<div class="card-body">
<div class="card-title"> ${ m . title } </div>
<div class="card-sub"> ${ m . location } · ${ fmtDate ( m . start _time ) } ${ m . zoomer _name ? ' · ' + m . zoomer _name : '' } </div>
</div>
<div class="card-right">
${ statusBadge ( m . status ) }
<div style="text-align:right;margin-left:6px"><div style="font-size:14px;font-weight:600"> ${ fmtMoney ( m . amount _sek ) } </div> ${ m . zoomer _rating ? stars ( m . zoomer _rating ) : '' } </div>
<span class="chevron">› </span>
</div>
</div> ` ;
}
2026-07-07 07:11:50 +00:00
// History tab
2026-07-05 06:41:32 +00:00
function renderHistory ( ) {
const missions = missionCache [ activeCustomer ] || [ ] ;
const spending = spendingCache [ activeCustomer ] || { } ;
// Spending chart
const byMonth = spending . by _month || { } ;
const months = Object . keys ( byMonth ) . sort ( ) ;
const maxVal = Math . max ( ... Object . values ( byMonth ) , 1 ) ;
document . getElementById ( 'spending-chart' ) . innerHTML = `
<div class="bar-chart-wrap">
<div class="bar-chart-title">Utgifter per månad</div>
${ months . map ( mo => `
<div class="bar-row">
<div class="bar-label"> ${ mo . slice ( 5 ) } / ${ mo . slice ( 2 , 4 ) } </div>
<div class="bar-track"><div class="bar-fill" style="width: ${ Math . round ( byMonth [ mo ] / maxVal * 100 ) } %"></div></div>
<div class="bar-val"> ${ fmtMoney ( byMonth [ mo ] ) } </div>
</div> ` ) . join ( '' ) }
${ ! months . length ? '<div style="color:#666666;font-size:14px">Ingen historik ännu</div>' : '' }
</div> ` ;
// All missions
document . getElementById ( 'all-missions' ) . innerHTML = missions . length
? missions . map ( renderMissionCard ) . join ( '' )
2026-07-07 07:11:50 +00:00
: '<div class="empty"><div class="empty-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>Inga uppdrag</div>' ;
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
// Billing tab
2026-07-05 06:41:32 +00:00
function renderBilling ( ) {
const invs = invoiceCache [ activeCustomer ] || [ ] ;
document . getElementById ( 'invoices-list' ) . innerHTML = invs . length
? invs . map ( inv => `
<div class="card-item">
2026-07-07 07:11:50 +00:00
<div class="card-icon ${ inv . status === 'paid' ? 'ci-green' : 'ci-orange' } "> ${ inv . status === 'paid' ? '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' : '' } </div>
2026-07-05 06:41:32 +00:00
<div class="card-body"><div class="card-title"> ${ inv . number } </div><div class="card-sub">Förfaller ${ fmtDate ( inv . due _date ) } </div></div>
<div class="card-right"> ${ invStatusBadge ( inv . status ) } <div style="font-size:14px;font-weight:700;margin-left:8px"> ${ fmtMoney ( inv . amount _sek ) } </div><span class="chevron">› </span></div>
</div> ` ) . join ( '' )
2026-07-07 07:11:50 +00:00
: '<div class="empty"><div class="empty-icon"></div>Inga fakturor</div>' ;
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
// Favorites tab
2026-07-05 06:41:32 +00:00
function renderFavorites ( ) {
const favs = favCache [ activeCustomer ] || [ ] ;
document . getElementById ( 'favs-list' ) . innerHTML = favs . length
? favs . map ( f => `
<div class="fav-card">
2026-07-07 07:11:50 +00:00
<div class="fav-avatar"> ${ f . avatar || '<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>
2026-07-05 06:41:32 +00:00
<div>
<div class="fav-name"> ${ f . name } </div>
<div class="fav-spec"> ${ f . specialty } </div>
2026-07-07 07:11:50 +00:00
<div class="fav-meta"> ${ f . missions _completed } uppdrag · ${ f . rating ? ' ' + f . rating : 'Ny' } </div>
2026-07-05 06:41:32 +00:00
</div>
<div class="fav-actions">
2026-07-07 07:11:50 +00:00
<button class="btn btn-gray" style="font-size:13px;padding:7px 12px;border-radius: var(--radius-md);border:none;cursor:pointer" onclick="removeFav(' ${ f . zoomer _id } ')"></button>
2026-07-05 06:41:32 +00:00
</div>
</div> ` ) . join ( '' )
2026-07-07 07:11:50 +00:00
: '<div class="empty"><div class="empty-icon"></div><div style="font-weight:600">Inga favoriter</div><div style="font-size:13px;margin-top:4px">Lägg till zoomers du gillar!</div></div>' ;
2026-07-05 06:41:32 +00:00
}
async function removeFav ( zid ) {
const r = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /favorites/ ${ zid } ` , { method : 'DELETE' } ) . then ( r => r . json ( ) ) ;
if ( r . ok ) {
toast ( 'Favorit borttagen' ) ;
const fR = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /favorites ` ) . then ( r => r . json ( ) ) ;
favCache [ activeCustomer ] = fR . items || [ ] ;
renderFavorites ( ) ;
} else toast ( 'Fel: ' + r . error ) ;
}
2026-07-07 07:11:50 +00:00
// Book modal
2026-07-05 06:41:32 +00:00
function openBookModal ( ) { document . getElementById ( 'book-modal' ) . classList . add ( 'open' ) }
async function submitBook ( ) {
const title = document . getElementById ( 'bk-title' ) . value ;
const start = document . getElementById ( 'bk-start' ) . value ;
if ( ! title || ! start ) { toast ( 'Fyll i titel och startdatum' ) ; return }
const body = {
title , type : document . getElementById ( 'bk-type' ) . value ,
start _time : new Date ( start ) . toISOString ( ) ,
duration _hours : + document . getElementById ( 'bk-duration' ) . value || 2 ,
location : document . getElementById ( 'bk-location' ) . value || 'Phuket' ,
budget _sek : + document . getElementById ( 'bk-budget' ) . value || 2500 ,
} ;
const r = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /book ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) . then ( r => r . json ( ) ) ;
if ( r . ok ) {
2026-07-07 07:11:50 +00:00
toast ( '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Uppdrag bokat!' ) ; closeModal ( 'book-modal' ) ;
2026-07-05 06:41:32 +00:00
const mR = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /missions ` ) . then ( r => r . json ( ) ) ;
missionCache [ activeCustomer ] = mR . items || [ ] ; renderDashboard ( ) ;
document . getElementById ( 'bk-title' ) . value = '' ; document . getElementById ( 'bk-location' ) . value = '' ; document . getElementById ( 'bk-budget' ) . value = '' ;
} else toast ( 'Fel: ' + r . error ) ;
}
2026-07-07 07:11:50 +00:00
// Add favorite modal
2026-07-05 06:41:32 +00:00
function openAddFavModal ( ) { document . getElementById ( 'fav-modal' ) . classList . add ( 'open' ) }
async function submitFav ( ) {
const body = { zoomer _id : document . getElementById ( 'fav-id' ) . value , name : document . getElementById ( 'fav-name' ) . value , specialty : document . getElementById ( 'fav-spec' ) . value } ;
if ( ! body . zoomer _id || ! body . name ) { toast ( 'Fyll i ID och namn' ) ; return }
const r = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /favorites ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) . then ( r => r . json ( ) ) ;
if ( r . ok ) {
2026-07-07 07:11:50 +00:00
toast ( ' Favorit tillagd!' ) ; closeModal ( 'fav-modal' ) ;
2026-07-05 06:41:32 +00:00
const fR = await fetch ( ` /api/quixzoom/customers/ ${ activeCustomer } /favorites ` ) . then ( r => r . json ( ) ) ;
favCache [ activeCustomer ] = fR . items || [ ] ; renderFavorites ( ) ;
document . getElementById ( 'fav-id' ) . value = '' ; document . getElementById ( 'fav-name' ) . value = '' ;
} else toast ( 'Fel: ' + r . error ) ;
}
2026-07-07 07:11:50 +00:00
// Init
2026-07-05 06:41:32 +00:00
loadAll ( ) ;
< / script >
2026-07-14 09:58:53 +00:00
<!-- quiXzoom Mina Sidor - Universal Header Component -->
<!-- Injicera detta i <head> på alla quiXzoom - sajter -->
< style >
. qz-auth-bar {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
height : 48 px ;
background : #0A0A0A ;
border-bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 0 20 px ;
z-index : 9999 ;
font-family : - apple-system , BlinkMacSystemFont , 'SF Pro Display' , 'Segoe UI' , Roboto , sans-serif ;
}
. qz-auth-bar . qz-logo {
font-size : 18 px ;
font-weight : 700 ;
color : #fff ;
text-decoration : none ;
letter-spacing : -0.5 px ;
}
. qz-auth-bar . qz-logo span {
color : #007AFF ;
}
. qz-auth-bar . qz-nav {
display : flex ;
gap : 8 px ;
align-items : center ;
}
. qz-auth-bar . qz-btn {
padding : 8 px 16 px ;
border-radius : 8 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
border : none ;
text-decoration : none ;
}
. qz-auth-bar . qz-btn-primary {
background : #007AFF ;
color : white ;
}
. qz-auth-bar . qz-btn-primary : hover {
background : #0056CC ;
}
. qz-auth-bar . qz-btn-ghost {
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
}
. qz-auth-bar . qz-btn-ghost : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
color : white ;
}
. qz-auth-bar . qz-user {
display : flex ;
align-items : center ;
gap : 8 px ;
color : white ;
font-size : 14 px ;
}
. qz-auth-bar . qz-avatar {
width : 32 px ;
height : 32 px ;
border-radius : 50 % ;
background : #007AFF ;
display : flex ;
align-items : center ;
justify-content : center ;
font-weight : 600 ;
font-size : 14 px ;
}
. qz-auth-bar . qz-dropdown {
position : relative ;
}
. qz-auth-bar . qz-dropdown-menu {
position : absolute ;
top : 40 px ;
right : 0 ;
background : #1C1C1E ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
border-radius : 12 px ;
padding : 8 px ;
min-width : 200 px ;
display : none ;
box-shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.4 ) ;
}
. qz-auth-bar . qz-dropdown-menu . active {
display : block ;
}
. qz-auth-bar . qz-dropdown-item {
padding : 10 px 12 px ;
border-radius : 8 px ;
color : white ;
text-decoration : none ;
display : flex ;
align-items : center ;
gap : 10 px ;
font-size : 14 px ;
transition : background 0.2 s ;
}
. qz-auth-bar . qz-dropdown-item : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
}
. qz-auth-bar . qz-dropdown-divider {
height : 1 px ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
margin : 8 px 0 ;
}
. qz-auth-bar . qz-balance {
background : rgba ( 0 , 122 , 255 , 0.15 ) ;
color : #007AFF ;
padding : 4 px 10 px ;
border-radius : 6 px ;
font-size : 13 px ;
font-weight : 600 ;
}
@ media ( max-width : 640px ) {
. qz-auth-bar . qz-nav . qz-btn-ghost : not ( . qz-user ) {
display : none ;
}
}
2026-07-14 15:27:33 +00:00
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
< / style >
<!--
quiXzoom Universal Auth Snippet v2.0
Add this to ALL quiXzoom sites for seamless cross - domain login
Features:
- Silent login (auto - refresh tokens)
- Cross - domain cookie sync
- Real - time auth state updates
- Mobile app compatible
-->
<!-- Auth bar styles -->
< style >
# qz-auth-bar {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
height : 48 px ;
background : #0A0A0A ;
border-bottom : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 0 20 px ;
z-index : 9999 ;
font-family : - apple-system , BlinkMacSystemFont , 'SF Pro Display' , 'Segoe UI' , Roboto , sans-serif ;
}
# qz-auth-bar . qz-logo {
font-size : 18 px ;
font-weight : 700 ;
color : #fff ;
text-decoration : none ;
letter-spacing : -0.5 px ;
}
# qz-auth-bar . qz-logo span { color : #007AFF ; }
# qz-auth-bar . qz-nav {
display : flex ;
gap : 8 px ;
align-items : center ;
}
# qz-auth-bar . qz-btn {
padding : 8 px 16 px ;
border-radius : 8 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
transition : all 0.2 s ;
border : none ;
text-decoration : none ;
}
# qz-auth-bar . qz-btn-primary {
background : #007AFF ;
color : white ;
}
# qz-auth-bar . qz-btn-primary : hover { background : #0056CC ; }
# qz-auth-bar . qz-btn-ghost {
background : transparent ;
color : rgba ( 255 , 255 , 255 , 0.8 ) ;
}
# qz-auth-bar . qz-btn-ghost : hover {
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
color : white ;
}
# qz-auth-bar . qz-user-menu {
position : relative ;
}
# qz-auth-bar . qz-dropdown {
position : absolute ;
top : 40 px ;
right : 0 ;
background : #1C1C1E ;
border : 1 px solid rgba ( 255 , 255 , 255 , 0.1 ) ;
border-radius : 12 px ;
padding : 8 px ;
min-width : 220 px ;
display : none ;
box-shadow : 0 8 px 32 px rgba ( 0 , 0 , 0 , 0.4 ) ;
}
# qz-auth-bar . qz-dropdown . active { display : block ; }
# qz-auth-bar . qz-dropdown-item {
padding : 10 px 12 px ;
border-radius : 8 px ;
color : white ;
text-decoration : none ;
display : flex ;
align-items : center ;
gap : 10 px ;
font-size : 14 px ;
transition : background 0.2 s ;
}
# qz-auth-bar . qz-dropdown-item : hover { background : rgba ( 255 , 255 , 255 , 0.1 ) ; }
# qz-auth-bar . qz-dropdown-divider {
height : 1 px ;
background : rgba ( 255 , 255 , 255 , 0.1 ) ;
margin : 8 px 0 ;
}
# qz-auth-bar . qz-avatar {
width : 32 px ;
height : 32 px ;
border-radius : 50 % ;
background : #007AFF ;
display : flex ;
align-items : center ;
justify-content : center ;
font-weight : 600 ;
font-size : 14 px ;
color : white ;
}
# qz-auth-bar . qz-balance {
background : rgba ( 0 , 122 , 255 , 0.15 ) ;
color : #007AFF ;
padding : 4 px 10 px ;
border-radius : 6 px ;
font-size : 13 px ;
font-weight : 600 ;
}
/* QUIXZOOM STANDARD — Dark Theme */
: root {
--qz-bg : #0a0a0a ;
--qz-surface : #111111 ;
--qz-surface-2 : #1a1a1a ;
--qz-border : rgba ( 255 , 255 , 255 , 0.08 ) ;
--qz-text : #e8e8e8 ;
--qz-text-dim : #a0a0a0 ;
--qz-text-muted : #6b6b6b ;
--qz-blue : #007AFF ;
--qz-blue-dark : #0056CC ;
--qz-radius : 10 px ;
}
* { margin : 0 ; padding : 0 ; box-sizing : border-box ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Inter' , sans-serif ;
background : var ( - - qz - bg ) ;
color : var ( - - qz - text ) ;
line-height : 1.6 ;
-webkit- font-smoothing : antialiased ;
}
/* Navigation */
. qz-nav {
position : fixed ; top : 0 ; left : 0 ; right : 0 ; z-index : 100 ;
display : flex ; align-items : center ; justify-content : space-between ;
padding : 0 24 px ; height : 56 px ;
background : rgba ( 255 , 255 , 255 , 0.92 ) ;
backdrop-filter : blur ( 16 px ) ;
border-bottom : 1 px solid var ( - - qz - border ) ;
}
. qz-nav-logo { font-size : 18 px ; font-weight : 700 ; color : #fff ; text-decoration : none ; }
. qz-nav-links { display : flex ; gap : 24 px ; list-style : none ; }
. qz-nav-links a { font-size : 14 px ; color : var ( - - qz - text - dim ) ; text-decoration : none ; transition : color 0.2 s ; }
. qz-nav-links a : hover { color : #fff ; }
/* Buttons */
. qz-btn {
display : inline-flex ; align-items : center ; gap : 6 px ;
padding : 12 px 24 px ; border-radius : var ( - - qz - radius ) ;
font-size : 15 px ; font-weight : 600 ; text-decoration : none ;
transition : all 0.2 s ; cursor : pointer ; border : none ;
}
. qz-btn-primary {
background : var ( - - qz - blue ) ; color : #fff ;
}
. qz-btn-primary : hover {
background : var ( - - qz - blue - dark ) ; transform : translateY ( -1 px ) ;
}
. qz-btn-ghost {
background : transparent ; color : var ( - - qz - text ) ;
border : 1 px solid var ( - - qz - border ) ;
}
. qz-btn-ghost : hover {
border-color : var ( - - qz - blue ) ; color : var ( - - qz - blue ) ;
}
/* Cards */
. qz-card {
background : var ( - - qz - surface ) ;
border : 1 px solid var ( - - qz - border ) ;
border-radius : var ( - - qz - radius ) ;
padding : 24 px ;
transition : transform 0.2 s , border-color 0.2 s ;
}
. qz-card : hover {
transform : translateY ( -2 px ) ;
border-color : rgba ( 0 , 122 , 255 , 0.3 ) ;
}
/* Footer */
. qz-footer {
border-top : 1 px solid var ( - - qz - border ) ;
padding : 40 px 24 px ;
text-align : center ;
color : var ( - - qz - text - dim ) ;
font-size : 14 px ;
}
/* Banner */
. qz-banner {
background : #001E50 ;
color : rgba ( 255 , 255 , 255 , 0.55 ) ;
text-align : center ;
padding : 10 px ;
font-size : 12 px ;
}
. qz-banner a { color : rgba ( 255 , 255 , 255 , 0.8 ) ; text-decoration : none ; }
@ media ( max-width : 768px ) {
. qz-nav-links { display : none ; }
}
2026-07-14 09:58:53 +00:00
< / style >
2026-07-14 15:27:33 +00:00
<!-- Auth bar HTML -->
< div id = "qz-auth-bar" >
2026-07-14 09:58:53 +00:00
< a href = "https://quixzoom.com" class = "qz-logo" > qui< span > X< / span > zoom< / a >
< div class = "qz-nav" >
2026-07-14 15:27:33 +00:00
<!-- Guest view -->
< div id = "qz-guest-view" >
2026-07-14 09:58:53 +00:00
< a href = "https://app.quixzoom.com/login" class = "qz-btn qz-btn-ghost" > Logga in< / a >
< a href = "https://app.quixzoom.com/register" class = "qz-btn qz-btn-primary" > Bli Zoomer< / a >
< / div >
2026-07-14 15:27:33 +00:00
<!-- Authenticated view -->
< div id = "qz-user-view" style = "display:none;" >
2026-07-14 09:58:53 +00:00
< span class = "qz-balance" id = "qz-balance" > $0.00< / span >
2026-07-14 15:27:33 +00:00
< div class = "qz-user-menu" >
< button class = "qz-btn qz-btn-ghost" onclick = "qzToggleMenu()" style = "display:flex;align-items:center;gap:8px;" >
< div class = "qz-avatar" id = "qz-avatar" > ?< / div >
< span id = "qz-user-name" style = "color:white;" > User< / span >
< svg width = "12" height = "12" viewBox = "0 0 12 12" fill = "none" >
< path d = "M2.5 4.5L6 8L9.5 4.5" stroke = "currentColor" stroke-width = "1.5" stroke-linecap = "round" stroke-linejoin = "round" / >
< / svg >
2026-07-14 09:58:53 +00:00
< / button >
2026-07-14 15:27:33 +00:00
< div class = "qz-dropdown" id = "qz-dropdown" >
2026-07-14 09:58:53 +00:00
< a href = "https://app.quixzoom.com/dashboard" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< rect x = "3" y = "3" width = "7" height = "7" rx = "1" / > < rect x = "14" y = "3" width = "7" height = "7" rx = "1" / >
< rect x = "14" y = "14" width = "7" height = "7" rx = "1" / > < rect x = "3" y = "14" width = "7" height = "7" rx = "1" / >
< / svg >
2026-07-14 09:58:53 +00:00
Dashboard
< / a >
< a href = "https://app.quixzoom.com/wallet" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M21 12V7H5a2 2 0 0 1 0-4h14v4" / > < path d = "M3 5v14a2 2 0 0 0 2 2h16v-5" / >
< path d = "M18 12a2 2 0 0 0 0 4h4v-4h-4z" / >
< / svg >
2026-07-14 09:58:53 +00:00
Plånbok
< / a >
< a href = "https://app.quixzoom.com/missions" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M12 2L2 7l10 5 10-5-10-5z" / > < path d = "M2 17l10 5 10-5" / > < path d = "M2 12l10 5 10-5" / >
< / svg >
2026-07-14 09:58:53 +00:00
Mina uppdrag
< / a >
< div class = "qz-dropdown-divider" > < / div >
< a href = "https://app.quixzoom.com/settings" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< circle cx = "12" cy = "12" r = "3" / >
< path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.67 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.67 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.67a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" / >
< / svg >
2026-07-14 09:58:53 +00:00
Inställningar
< / a >
< a href = "#" onclick = "qzLogout(); return false;" class = "qz-dropdown-item" >
2026-07-14 15:27:33 +00:00
< svg width = "16" height = "16" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" / > < polyline points = "16 17 21 12 16 7" / >
< line x1 = "21" y1 = "12" x2 = "9" y2 = "12" / >
< / svg >
2026-07-14 09:58:53 +00:00
Logga ut
< / a >
< / div >
< / div >
< / div >
< / div >
< / div >
2026-07-14 15:27:33 +00:00
<!-- SSO Client Script -->
2026-07-14 09:58:53 +00:00
< script >
( function ( ) {
const SSO _BASE = 'https://auth.quixzoom.com' ;
2026-07-14 15:27:33 +00:00
// Check auth status on load
2026-07-14 09:58:53 +00:00
async function qzCheckAuth ( ) {
try {
2026-07-14 15:27:33 +00:00
const res = await fetch ( SSO _BASE + '/auth/silent' , {
method : 'GET' ,
2026-07-14 09:58:53 +00:00
credentials : 'include' ,
} ) ;
2026-07-14 15:27:33 +00:00
2026-07-14 09:58:53 +00:00
if ( res . ok ) {
const data = await res . json ( ) ;
if ( data . authenticated ) {
2026-07-14 15:27:33 +00:00
qzShowUser ( data . user ) ;
return ;
2026-07-14 09:58:53 +00:00
}
}
2026-07-14 15:27:33 +00:00
// Try check endpoint
const checkRes = await fetch ( SSO _BASE + '/auth/check' , {
credentials : 'include' ,
} ) ;
if ( checkRes . ok ) {
const checkData = await checkRes . json ( ) ;
if ( checkData . authenticated && checkData . user ) {
qzShowUser ( checkData . user ) ;
return ;
}
}
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
} catch ( e ) {
2026-07-14 15:27:33 +00:00
console . log ( '[QZAuth] Check failed, showing guest' ) ;
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
}
}
2026-07-14 15:27:33 +00:00
function qzShowUser ( user ) {
document . getElementById ( 'qz-guest-view' ) . style . display = 'none' ;
document . getElementById ( 'qz-user-view' ) . style . display = 'flex' ;
document . getElementById ( 'qz-user-name' ) . textContent = user . name || user . email ;
document . getElementById ( 'qz-avatar' ) . textContent = ( user . name || user . email ) [ 0 ] . toUpperCase ( ) ;
// Dispatch event for other scripts
window . dispatchEvent ( new CustomEvent ( 'qz-auth-change' , {
detail : { authenticated : true , user }
} ) ) ;
}
function qzShowGuest ( ) {
document . getElementById ( 'qz-guest-view' ) . style . display = 'flex' ;
document . getElementById ( 'qz-user-view' ) . style . display = 'none' ;
window . dispatchEvent ( new CustomEvent ( 'qz-auth-change' , {
detail : { authenticated : false }
} ) ) ;
}
window . qzToggleMenu = function ( ) {
document . getElementById ( 'qz-dropdown' ) . classList . toggle ( 'active' ) ;
2026-07-14 09:58:53 +00:00
} ;
2026-07-14 15:27:33 +00:00
2026-07-14 09:58:53 +00:00
window . qzLogout = async function ( ) {
try {
await fetch ( SSO _BASE + '/auth/logout' , {
method : 'POST' ,
credentials : 'include' ,
} ) ;
} catch ( e ) { }
2026-07-14 15:27:33 +00:00
// Clear all cookies
2026-07-14 09:58:53 +00:00
document . cookie = 'qz_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
document . cookie = 'qz_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
2026-07-14 15:27:33 +00:00
document . cookie = 'qz_auth=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=.quixzoom.com; path=/;' ;
qzShowGuest ( ) ;
2026-07-14 09:58:53 +00:00
window . location . reload ( ) ;
} ;
2026-07-14 15:27:33 +00:00
// Close dropdown on outside click
document . addEventListener ( 'click' , function ( e ) {
if ( ! e . target . closest ( '.qz-user-menu' ) ) {
document . getElementById ( 'qz-dropdown' ) . classList . remove ( 'active' ) ;
}
} ) ;
// Check auth on page load
2026-07-14 09:58:53 +00:00
qzCheckAuth ( ) ;
2026-07-14 15:27:33 +00:00
// Re-check auth periodically (every 5 minutes)
setInterval ( qzCheckAuth , 5 * 60 * 1000 ) ;
// Listen for storage events (login from other tabs)
window . addEventListener ( 'storage' , function ( e ) {
if ( e . key === 'qz_auth_event' ) {
const event = JSON . parse ( e . newValue || '{}' ) ;
if ( event . type === 'login' ) {
qzCheckAuth ( ) ;
} else if ( event . type === 'logout' ) {
qzShowGuest ( ) ;
}
}
} ) ;
2026-07-14 09:58:53 +00:00
} ) ( ) ;
< / script >
2026-07-05 06:41:32 +00:00
< / body >
< / html >