<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solar Sales Suite</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/jquery.dataTables.min.css"/>
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBJPnfNCe6un1J7ck6co7zr2uvGrxhFYXY&libraries=places,drawing,geometry&callback=Function.prototype" async defer></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
/* DataTables overrides */
#customerDT_wrapper { font-family:'Inter',sans-serif; }
#customerDT_wrapper .dataTables_filter input { padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;min-width:250px; }
#customerDT_wrapper .dataTables_length select { padding:6px 10px;border:1px solid #e5e7eb;border-radius:6px;font-family:inherit; }
#customerDT_wrapper .dataTables_info { font-size:12px;color:#94a3b8; }
#customerDT_wrapper .dataTables_paginate .paginate_button { padding:4px 10px;border-radius:6px;font-size:12px;font-family:inherit; }
#customerDT_wrapper .dataTables_paginate .paginate_button.current { background:#024550!important;color:#fff!important;border:1px solid #024550!important; }
#staffDT_wrapper { font-family:'Inter',sans-serif; }
#staffDT_wrapper .dataTables_filter input { padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;min-width:250px; }
#staffDT_wrapper .dataTables_length select { padding:6px 10px;border:1px solid #e5e7eb;border-radius:6px;font-family:inherit; }
#staffDT_wrapper .dataTables_info { font-size:12px;color:#94a3b8; }
#staffDT_wrapper .dataTables_paginate .paginate_button { padding:4px 10px;border-radius:6px;font-size:12px;font-family:inherit; }
#staffDT_wrapper .dataTables_paginate .paginate_button.current { background:#024550!important;color:#fff!important;border:1px solid #024550!important; }
table.dataTable thead th { background:#f8f9fa;font-size:12px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;padding:10px 14px!important;border-bottom:2px solid #e5e7eb!important; }
table.dataTable tbody td { padding:8px 14px!important;font-size:13px;border-bottom:1px solid #f1f5f9!important; }
table.dataTable tbody tr:hover { background:#f0fdf4!important; }
table.dataTable.no-footer { border-bottom:none!important; }
body{font-family:'Inter',sans-serif;background:#f8f9fa;color:#1a1a1a;line-height:1.6;display:flex;min-height:100vh;-webkit-font-smoothing:antialiased}
/* === LOGIN === */
.login-screen{position:fixed;inset:0;z-index:9999;display:flex;background:#fff}
.login-screen.hidden{display:none}
.login-image{flex:1;background:url('assets/img/login-bg.jpg') center/cover no-repeat;position:relative}
.login-image::after{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(2,69,80,.4) 0%,rgba(0,0,0,.2) 100%)}
.login-image-text{position:absolute;bottom:50px;left:50px;z-index:1;color:#fff}
.login-image-text h2{font-size:32px;font-weight:700;margin-bottom:8px;text-shadow:0 2px 12px rgba(0,0,0,.3)}
.login-image-text p{font-size:15px;opacity:.85;text-shadow:0 1px 8px rgba(0,0,0,.3)}
.login-panel{width:480px;display:flex;flex-direction:column;justify-content:center;padding:60px 56px;background:#fff;flex-shrink:0}
.login-logo{margin-bottom:48px}
.login-logo img{width:180px;height:auto}
.login-greeting{font-size:28px;font-weight:700;color:#1a1a1a;margin-bottom:6px}
.login-subtitle{font-size:15px;color:#94a3b8;margin-bottom:36px}
.login-form-group{margin-bottom:20px}
.login-form-group label{display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px}
.login-form-group input{width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:'Inter',sans-serif;transition:all .2s;background:#fafbfc}
.login-form-group input:focus{outline:none;border-color:#024550;box-shadow:0 0 0 3px rgba(2,69,80,.1);background:#fff}
.login-form-group input::placeholder{color:#c1c7cd}
.login-remember{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px}
.login-remember label{display:flex;align-items:center;gap:8px;font-size:13px;color:#64748b;cursor:pointer}
.login-remember input[type=checkbox]{width:16px;height:16px;accent-color:#024550;cursor:pointer}
.login-forgot{font-size:13px;color:#024550;text-decoration:none;font-weight:500;transition:color .15s}
.login-forgot:hover{color:#10b981}
.login-btn{width:100%;padding:14px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:'Inter',sans-serif;transition:all .2s;letter-spacing:.2px}
.login-btn:hover{background:#035e6b;box-shadow:0 4px 14px rgba(2,69,80,.25)}
.login-btn:active{transform:scale(.985)}
.login-footer{margin-top:auto;padding-top:40px;text-align:center;font-size:12px;color:#c1c7cd}
@media(max-width:900px){.login-image{display:none}.login-panel{width:100%}}
/* === SIDEBAR === */
.sidebar{position:fixed;top:0;left:0;bottom:0;width:220px;background:#024550;display:flex;flex-direction:column;z-index:100}
.sidebar-brand{padding:20px 22px 18px;display:flex;align-items:center;justify-content:center}
.sidebar-brand-icon{width:160px;height:auto;display:flex;align-items:center;justify-content:center}
.sidebar-brand-icon img{width:100%;height:auto;object-fit:contain}
.sidebar-menu{flex:1;padding:4px 0;overflow-y:auto}
.sidebar-menu::-webkit-scrollbar{width:3px}
.sidebar-menu::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:3px}
.nav-item{display:flex;align-items:center;gap:12px;padding:10px 22px;color:rgba(255,255,255,.55);text-decoration:none;font-size:13.5px;font-weight:500;transition:all .15s;cursor:pointer;margin:1px 10px;border-radius:8px}
.nav-item:hover{color:rgba(255,255,255,.85);background:rgba(255,255,255,.05)}
.nav-item.active{background:rgba(46,204,113,.18);color:#fff;font-weight:600}
.nav-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.nav-icon svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.sidebar-bottom{padding:16px 22px;border-top:1px solid rgba(255,255,255,.08)}
.sidebar-user{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.sidebar-user-info{font-size:11px;color:rgba(255,255,255,.4);line-height:1.4}
.sidebar-user-info strong{display:block;color:rgba(255,255,255,.7);font-size:12px;font-weight:500}
.sidebar-logout{display:flex;align-items:center;gap:8px;color:rgba(255,255,255,.4);font-size:12px;cursor:pointer;transition:color .15s;background:none;border:none;font-family:inherit;padding:0}
.sidebar-logout:hover{color:rgba(255,255,255,.7)}
.sidebar-logout svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
/* === MAIN === */
.main-wrapper{margin-left:220px;padding:32px 40px;flex:1;min-height:100vh}
.page-title{font-size:28px;font-weight:700;color:#1a1a1a;letter-spacing:-.5px;margin-bottom:4px}
.page-subtitle{font-size:14px;color:#94a3b8;margin-bottom:28px;font-weight:400}
.page-content{display:none}
.page-content.active{display:block}
/* === DASHBOARD STAT CARDS === */
.stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:20px}
.stat-card{background:#fff;border-radius:10px;padding:14px 16px;border:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:flex-start;transition:box-shadow .2s}
.stat-card:hover{box-shadow:0 4px 12px rgba(0,0,0,.06)}
.stat-card-info h4{font-size:11px;color:#94a3b8;font-weight:500;margin-bottom:4px;text-transform:uppercase;letter-spacing:.3px}
.stat-card-info .stat-value{font-size:20px;font-weight:700;color:#1a1a1a;letter-spacing:-.5px}
.stat-card-icon{width:34px;height:34px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.stat-card-icon svg{width:16px;height:16px;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;fill:none}
.stat-card-icon.green{background:#ecfdf5}
.stat-card-icon.green svg{stroke:#10b981}
.stat-card-icon.blue{background:#eff6ff}
.stat-card-icon.blue svg{stroke:#3b82f6}
.stat-card-icon.purple{background:#faf5ff}
.stat-card-icon.purple svg{stroke:#a855f7}
.stat-card-icon.teal{background:#f0fdfa}
.stat-card-icon.teal svg{stroke:#14b8a6}
/* === DASHBOARD 2-COL === */
.dash-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.dash-panel{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden}
.dash-panel-header{padding:12px 16px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #f1f5f9}
.dash-panel-header svg{width:18px;height:18px;stroke:#64748b;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.dash-panel-header h3{font-size:14px;font-weight:600;color:#1a1a1a}
.dash-panel-body{padding:0}
.dash-panel-empty{padding:40px 22px;text-align:center;color:#94a3b8;font-size:14px}
.offert-item{display:flex;justify-content:space-between;align-items:center;padding:14px 22px;border-bottom:1px solid #f1f5f9;transition:background .15s}
.offert-item:last-child{border-bottom:none}
.offert-item:hover{background:#fafbfc}
.offert-item-left h4{font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px}
.offert-item-left p{font-size:12px;color:#94a3b8}
.offert-item-right{text-align:right}
.offert-item-right .offert-price{font-size:15px;font-weight:700;color:#10b981}
.offert-item-right .offert-status{font-size:11px;color:#94a3b8;margin-top:2px}
/* === TABLES (other pages) === */
.dummy-table{background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb}
.dummy-table-header{padding:16px 22px;font-weight:600;font-size:15px;color:#1a1a1a;border-bottom:1px solid #f1f5f9}
.dummy-table table{width:100%;border-collapse:collapse}
.dummy-table th,.dummy-table td{padding:12px 22px;text-align:left;border-bottom:1px solid #f1f5f9}
.dummy-table th{background:#fafbfc;font-weight:600;color:#64748b;font-size:12px;text-transform:uppercase;letter-spacing:.5px}
.dummy-table td{font-size:13.5px;color:#334155}
.dummy-table tr:last-child td{border-bottom:none}
.dummy-table tr:hover td{background:#fafbfc}
.status-badge{padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
.status-badge.green{background:#dcfce7;color:#166534}
.status-badge.yellow{background:#fef9c3;color:#854d0e}
.status-badge.blue{background:#dbeafe;color:#1e40af}
.status-badge.gray{background:#f1f5f9;color:#64748b}
.status-badge.purple{background:#ede9fe;color:#6d28d9}
.progress-cell{min-width:120px}
.progress-bar{height:20px;background:#f1f5f9;border-radius:10px;overflow:hidden;position:relative}
.progress-fill{height:100%;background:linear-gradient(90deg,#2ecc71,#27ae60);border-radius:10px;transition:width .4s}
.progress-text{position:absolute;top:0;left:0;right:0;height:100%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#333}
.dummy-btn{padding:10px 20px;background:#024550;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;display:inline-flex;align-items:center;gap:8px;transition:all .15s;font-family:'Inter',sans-serif}
.dummy-btn:hover{background:#244a36}
.dummy-btn.secondary{background:#f1f5f9;color:#334155}
.dummy-btn.secondary:hover{background:#e2e8f0}
.dummy-actions{display:flex;gap:10px;margin-bottom:20px}
.dummy-form{background:#fff;border-radius:12px;padding:28px;border:1px solid #e5e7eb;max-width:500px}
.dummy-form h3{margin-bottom:20px;color:#1a1a1a;font-size:18px}
.dummy-form-group{margin-bottom:15px}
.dummy-form-group label{display:block;font-weight:500;margin-bottom:6px;font-size:13px;color:#64748b}
.dummy-form-group input,.dummy-form-group select{width:100%;padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:'Inter',sans-serif;transition:all .15s}
.dummy-form-group input:focus,.dummy-form-group select:focus{outline:none;border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
/* === SETTINGS TABS === */
.settings-tab{padding:12px 24px;background:none;border:none;border-bottom:2px solid transparent;font-size:14px;font-weight:600;color:#94a3b8;cursor:pointer;font-family:'Inter',sans-serif;transition:all .2s;margin-bottom:-2px}
.settings-tab:hover{color:#1a1a1a}
.settings-tab.active{color:#024550;border-bottom-color:#024550}
.settings-panel{display:none}
.settings-panel.active{display:block}
/* === KONFIGURATOR === */
.config-header{display:flex;align-items:center;gap:15px;margin-bottom:20px;flex-wrap:wrap}
.config-label{font-size:14px;font-weight:600;color:#64748b}
.product-select{padding:12px 40px 12px 15px;font-size:15px;font-weight:600;border:1px solid #e5e7eb;border-radius:10px;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23334155'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E") no-repeat right 10px center/20px;appearance:none;cursor:pointer;min-width:320px;color:#1a1a1a;transition:all .15s;font-family:'Inter',sans-serif}
.product-select:hover{border-color:#94a3b8}
.product-select:focus{outline:none;border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.config-layout{display:grid;grid-template-columns:1fr 350px;gap:20px;align-items:start}
.config-main{display:flex;flex-direction:column;gap:15px}
.config-sidebar{position:sticky;top:20px}
.product-section{background:#fff;border-radius:12px;padding:25px;display:grid;grid-template-columns:1fr 1fr;gap:30px;border:1px solid #e5e7eb}
.image-container{position:relative}
.main-image{width:100%;display:flex;justify-content:center;align-items:center;min-height:250px}
.main-image img{max-width:100%;max-height:300px;object-fit:contain}
.thumbnail-strip{display:flex;gap:8px;margin-top:12px}
.thumbnail{width:50px;height:50px;border:2px solid #e5e7eb;border-radius:6px;cursor:pointer;overflow:hidden;transition:border-color .15s}
.thumbnail img{width:100%;height:100%;object-fit:cover}
.thumbnail:hover{border-color:#94a3b8}
.thumbnail.active{border-color:#024550}
.image-note{font-size:11px;color:#94a3b8;margin-top:10px}
.measure-section{padding-top:10px;display:flex;flex-direction:column;align-items:center}
.measure-title{font-size:20px;font-weight:700;margin-bottom:5px;text-align:center;color:#1a1a1a}
.measure-link{color:#3b82f6;font-size:13px;text-decoration:underline;cursor:pointer;display:block;text-align:center;margin-bottom:15px}
.tab-switch{display:inline-flex;border:1px solid #e5e7eb;border-radius:20px;overflow:hidden;margin:0 auto 20px}
.tab-btn{padding:10px 18px;background:#fff;border:none;cursor:pointer;font-size:13px;transition:all .15s;font-family:'Inter',sans-serif;font-weight:500;color:#64748b}
.tab-btn:hover{background:#f8f9fa}
.tab-btn.active{background:#024550;color:#fff}
.dim-inputs{display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:280px;margin:0 auto}
.dim-group label{display:block;font-weight:600;margin-bottom:6px;font-size:14px;color:#334155}
.dim-group .req{color:#3b82f6}
.input-wrap{display:flex;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;transition:all .15s}
.input-wrap:focus-within{border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.input-wrap input{flex:1;padding:10px 12px;border:none;font-size:15px;width:100%;font-family:'Inter',sans-serif}
.input-wrap input:focus{outline:none}
.input-wrap .unit{padding:10px 12px;background:#f8f9fa;border-left:1px solid #e5e7eb;font-weight:600;color:#64748b;font-size:13px}
.dim-result{text-align:center;margin-top:5px;color:#94a3b8;font-size:13px}
.options-section{background:#fff;border-radius:12px;padding:20px;border:1px solid #e5e7eb}
.options-header{display:flex;align-items:center;gap:20px;margin-bottom:15px}
.options-title{font-size:16px;font-weight:700;margin:0;color:#1a1a1a}
.cheapest-btn{padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;font-family:'Inter',sans-serif}
.cheapest-btn:hover{background:#244a36}
.option-card{background:#fff;border:1px solid #e5e7eb;border-radius:10px;margin-bottom:8px;overflow:hidden;transition:all .2s}
.option-card:last-child{margin-bottom:0}
.option-card.expanded{border:2px solid #2ecc71;background:#fff}
.option-card.required-error{border:2px solid #ef4444!important;animation:shake .3s}
.option-card.required-error .option-header{background:#fef2f2}
.option-card.is-valid .option-header::after{content:'';width:20px;height:20px;background:#dcfce7;border-radius:50%;margin-left:8px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2316a34a' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");background-size:12px;background-position:center;background-repeat:no-repeat;flex-shrink:0}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-4px)}75%{transform:translateX(4px)}}
@keyframes spin{to{transform:rotate(360deg)}}
.option-header{display:flex;align-items:center;padding:12px 15px;cursor:pointer;background:#fff;transition:background .15s}
.option-header:hover{background:#fafbfc}
.option-image{width:42px;height:42px;background:#f8f9fa;border-radius:8px;margin-right:12px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;overflow:hidden}
.option-image img{width:100%;height:100%;object-fit:cover}
.option-info{flex:1;min-width:0}
.option-name{font-weight:600;font-size:14px;margin-bottom:1px;color:#1a1a1a}
.option-value{color:#94a3b8;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.option-price{text-align:right;margin-right:10px;font-size:13px;color:#94a3b8;flex-shrink:0}
.option-price.extra{color:#ef4444}
.option-price.discount{color:#10b981}
.option-edit{width:30px;height:30px;background:#f1f5f9;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;transition:all .15s}
.option-edit:hover{background:#e2e8f0}
.required-badge{display:none;background:#ef4444;color:#fff;font-size:10px;padding:2px 6px;border-radius:8px;margin-left:8px;font-weight:400}
.option-card.required-error .required-badge{display:inline}
.option-body{display:none;padding:15px 20px 20px;border-top:1px solid #f1f5f9;background:#fff}
.option-card.expanded .option-body{display:block}
.option-body-title{font-size:14px;color:#64748b;margin-bottom:15px}
.option-choices{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:15px}
.option-choice{position:relative;background:#fff;border:2px solid #e5e7eb;border-radius:10px;cursor:pointer;transition:all .15s;text-align:center;padding:12px 8px}
.option-choice:hover{border-color:#94a3b8}
.option-choice.selected{border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.option-choice-info{position:absolute;top:6px;right:6px;width:18px;height:18px;background:#f1f5f9;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#94a3b8}
.option-choice-placeholder{width:100%;aspect-ratio:1/1;background:#f8f9fa;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:28px;margin-bottom:8px;overflow:hidden}
.option-choice-placeholder img{width:100%;height:100%;object-fit:cover}
.option-choice-radio{width:22px;height:22px;border:2px solid #d1d5db;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 6px;transition:all .15s}
.option-choice.selected .option-choice-radio{border-color:#2ecc71;background:#2ecc71}
.option-choice.selected .option-choice-radio::after{content:'';width:10px;height:10px;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E") center/contain no-repeat}
.option-choice-name{font-size:12px;line-height:1.3;color:#64748b}
.option-choice-price{font-size:11px;color:#94a3b8;margin-top:3px}
.option-save-btn{display:block;margin:0 auto;padding:10px 30px;background:#024550;border:none;border-radius:8px;font-size:13px;color:#fff;cursor:pointer;transition:all .15s;font-family:'Inter',sans-serif;font-weight:600}
.option-save-btn:hover{background:#244a36}
.price-card{background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb}
.price-header{background:#024550;color:#fff;padding:14px;font-weight:600;font-size:15px;text-align:center}
.price-rows{padding:15px}
.price-line{display:flex;justify-content:space-between;padding:8px 0;font-size:14px}
.price-line-label{color:#94a3b8}
.price-line-value{font-weight:600;color:#334155}
.price-line.subtotal{border-top:1px solid #f1f5f9;margin-top:8px;padding-top:12px;font-weight:600}
.rot-section{background:#ecfdf5;padding:12px 15px;border-top:1px solid #bbf7d0}
.rot-line{display:flex;justify-content:space-between;color:#10b981;font-weight:600;font-size:14px}
.total-section{background:#024550;color:#fff;padding:15px}
.total-line{display:flex;justify-content:space-between;align-items:center}
.total-label{font-size:15px}
.total-value{font-size:24px;font-weight:700}
.total-vat{font-size:11px;color:rgba(255,255,255,.5);text-align:right;margin-top:3px}
.rot-info{background:#fffbeb;border-top:1px solid #fde68a;padding:12px 15px;font-size:12px;color:#92400e}
.rot-info strong{display:block;margin-bottom:2px}
.cart-section{padding:15px;border-top:1px solid #f1f5f9}
.qty-row{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.qty-label{font-weight:600;font-size:13px;color:#334155}
.qty-controls{display:flex;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden}
.qty-btn{width:36px;height:36px;border:none;background:#f8f9fa;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s;color:#334155}
.qty-btn:hover{background:#e2e8f0}
.qty-input{width:45px;height:36px;border:none;border-left:1px solid #e5e7eb;border-right:1px solid #e5e7eb;text-align:center;font-size:15px;font-weight:600;font-family:'Inter',sans-serif}
.qty-input:focus{outline:none}
.cart-btn{width:100%;background:#024550;color:#fff;border:none;border-radius:10px;padding:14px;font-size:14px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .15s;font-family:'Inter',sans-serif}
.cart-btn:hover{background:#244a36}
.validation-msg{background:#ef4444;color:#fff;padding:10px 15px;border-radius:8px;margin-bottom:12px;display:none;align-items:center;gap:8px;font-size:13px}
.validation-msg.show{display:flex}
/* Modal */
.modal-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:200;justify-content:center;align-items:center;padding:20px}
.modal-overlay.active{display:flex}
.modal{background:#fff;border-radius:16px;width:900px;max-width:95%;height:800px;max-height:90vh;overflow-y:auto;box-shadow:0 24px 80px rgba(0,0,0,.2);display:flex;flex-direction:column}
.modal-header{background:#024550;color:#fff;padding:18px 22px;display:flex;justify-content:space-between;align-items:center}
.modal-header h2{font-size:17px;font-weight:600}
.modal-close{background:none;border:none;color:#fff;font-size:26px;cursor:pointer;opacity:.8}
.modal-close:hover{opacity:1}
.modal-body{padding:20px;flex:1;overflow-y:auto}
.modal-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.modal-section{margin-bottom:20px}
.modal-section:last-child{margin-bottom:0}
.modal-section-title{font-size:12px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid #f1f5f9}
.modal-product{display:flex;gap:12px;align-items:center}
.modal-product-image{width:70px;height:70px;border-radius:8px;object-fit:cover;border:1px solid #e5e7eb}
.modal-product-info h3{font-size:15px;font-weight:600;margin-bottom:3px}
.modal-product-info p{color:#94a3b8;font-size:13px}
.modal-cart{background:#f8f9fa;border-radius:8px;overflow:hidden}
.modal-cart-item{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid #e5e7eb;font-size:13px}
.modal-cart-item:last-child{border-bottom:none}
.modal-cart-item-name{color:#334155;font-weight:500;flex:0 0 auto}
.modal-cart-item-value{color:#94a3b8;flex:1;text-align:center;padding:0 8px}
.modal-cart-item-price{font-weight:600;min-width:70px;text-align:right}
.modal-cart-item-price.has-price{color:#10b981}
.modal-price-summary{background:#f8f9fa;border-radius:8px;padding:12px}
.modal-price-row{display:flex;justify-content:space-between;padding:5px 0;font-size:13px}
.modal-price-row.rot{color:#10b981;font-weight:500}
.modal-price-row.total{border-top:2px solid #e5e7eb;margin-top:8px;padding-top:10px;font-size:16px;font-weight:700}
.modal-form{display:grid;gap:12px}
.modal-form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.modal-form-group{display:flex;flex-direction:column}
.modal-form-group.full{grid-column:1/-1}
.modal-form-group label{font-size:12px;font-weight:500;color:#64748b;margin-bottom:4px}
.modal-form-group input,.modal-form-group textarea{padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:'Inter',sans-serif;transition:all .15s}
.modal-form-group input:focus,.modal-form-group textarea:focus{outline:none;border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.modal-form-group textarea{resize:vertical;min-height:60px}
.modal-submit{width:100%;padding:14px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;margin-top:8px;transition:all .15s;font-family:'Inter',sans-serif}
.modal-submit:hover{background:#244a36}
/* === KUNDER MAP === */
.kunder-map-wrap{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;margin-bottom:16px}
.kunder-map{height:380px;width:100%}
.kunder-map .leaflet-control-attribution{font-size:10px}
.kunder-filters{display:flex;align-items:center;gap:10px;padding:14px 20px;background:#fff;border-radius:12px;border:1px solid #e5e7eb;margin-bottom:16px;flex-wrap:wrap}
.kunder-filters-label{font-size:13px;font-weight:600;color:#64748b;margin-right:4px}
.filter-chip{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;border:2px solid #e5e7eb;background:#fff;transition:all .15s;font-family:'Inter',sans-serif;color:#334155}
.filter-chip:hover{background:#f8f9fa}
.filter-chip.active{border-color:currentColor;background:rgba(0,0,0,.03)}
.filter-chip .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.filter-chip.f-order .dot{background:#16a34a}
.filter-chip.f-order.active{border-color:#16a34a;color:#16a34a}
.filter-chip.f-offert .dot{background:#eab308}
.filter-chip.f-offert.active{border-color:#eab308;color:#854d0e}
.filter-chip.f-lost .dot{background:#ef4444}
.filter-chip.f-lost.active{border-color:#ef4444;color:#ef4444}
.filter-chip.f-lead .dot{background:#94a3b8}
.filter-chip.f-lead.active{border-color:#94a3b8;color:#64748b}
.filter-chip .count{background:rgba(0,0,0,.06);padding:1px 7px;border-radius:10px;font-size:11px;margin-left:2px}
.kunder-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
.kunder-stat{background:#fff;border-radius:10px;padding:14px 16px;border:1px solid #e5e7eb;display:flex;align-items:center;gap:12px}
.kunder-stat .ks-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0}
.kunder-stat .ks-info{flex:1}
.kunder-stat .ks-info h4{font-size:11px;color:#94a3b8;font-weight:500;text-transform:uppercase;letter-spacing:.3px}
.kunder-stat .ks-info .ks-val{font-size:22px;font-weight:700;color:#1a1a1a}
.kunder-stat .ks-info .ks-sum{font-size:12px;color:#64748b;margin-top:1px}
.kunder-search{display:flex;gap:10px;margin-bottom:0}
.kunder-search input{flex:1;padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:'Inter',sans-serif}
.kunder-search input:focus{outline:none;border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.map-marker-order{background:#16a34a;border:2px solid #fff;border-radius:50%;width:14px;height:14px;box-shadow:0 1px 4px rgba(0,0,0,.3)}
.sidebar-section-label{padding:20px 22px 6px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,.25)}
.sidebar-divider{height:1px;background:rgba(255,255,255,.08);margin:8px 18px 4px}
/* === KALENDER === */
.cal-wrap{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;margin-bottom:20px}
.cal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #f1f5f9}
.cal-header h3{font-size:16px;font-weight:600;color:#1a1a1a}
.cal-nav{display:flex;gap:6px}
.cal-nav button{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;color:#64748b;transition:all .15s}
.cal-nav button:hover{background:#f8f9fa;border-color:#94a3b8}
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;padding:12px 16px 16px}
.cal-day-header{font-size:11px;font-weight:600;color:#94a3b8;padding:4px 0 8px;text-transform:uppercase}
.cal-day{padding:8px 4px;font-size:13px;color:#334155;border-radius:8px;cursor:pointer;transition:all .15s;position:relative}
.cal-day:hover{background:#f1f5f9}
.cal-day.today{background:#024550;color:#fff;font-weight:600}
.cal-day.has-event::after{content:'';position:absolute;bottom:3px;left:50%;transform:translateX(-50%);width:5px;height:5px;background:#eab308;border-radius:50%}
.cal-day.other-month{color:#d1d5db}
.cal-events{margin-top:0}
.cal-event-item{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:8px;transition:all .15s}
.cal-event-item:hover{box-shadow:0 2px 8px rgba(0,0,0,.05)}
.cal-event-time{font-size:12px;font-weight:600;color:#64748b;min-width:50px}
.cal-event-info h4{font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:1px}
.cal-event-info p{font-size:12px;color:#94a3b8}
.cal-event-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
/* === DAGRAPPORT === */
.dr-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px}
.dr-card{background:#fff;border-radius:12px;padding:18px 20px;border:1px solid #e5e7eb}
.dr-card h4{font-size:12px;color:#94a3b8;font-weight:500;margin-bottom:6px;text-transform:uppercase;letter-spacing:.3px}
.dr-card .dr-val{font-size:24px;font-weight:700;color:#1a1a1a}
.dr-card .dr-sub{font-size:12px;color:#64748b;margin-top:2px}
.dr-form{background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:24px;margin-bottom:20px}
.dr-form h3{font-size:16px;font-weight:600;color:#1a1a1a;margin-bottom:16px}
.dr-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.dr-form-group{display:flex;flex-direction:column}
.dr-form-group.full{grid-column:1/-1}
.dr-form-group label{font-size:12px;font-weight:500;color:#64748b;margin-bottom:5px}
.dr-form-group input,.dr-form-group textarea,.dr-form-group select{padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:'Inter',sans-serif;transition:all .15s}
.dr-form-group input:focus,.dr-form-group textarea:focus,.dr-form-group select:focus{outline:none;border-color:#2ecc71;box-shadow:0 0 0 3px rgba(46,204,113,.1)}
.dr-form-group textarea{resize:vertical;min-height:80px}
.dr-log-item{display:flex;gap:14px;padding:16px 20px;background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:8px}
.dr-log-date{font-size:12px;font-weight:600;color:#64748b;min-width:80px}
.dr-log-info{flex:1}
.dr-log-info h4{font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:3px}
.dr-log-info p{font-size:13px;color:#64748b;line-height:1.5}
.dr-log-tag{font-size:11px;padding:3px 10px;border-radius:12px;font-weight:600}
/* === MIN LÖN === */
.lon-overview{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:20px}
.lon-card{background:#fff;border-radius:12px;padding:20px;border:1px solid #e5e7eb}
.lon-card.highlight{background:#024550;border-color:#024550}
.lon-card.highlight h4{color:rgba(255,255,255,.6)}
.lon-card.highlight .lon-val{color:#fff}
.lon-card.highlight .lon-sub{color:rgba(255,255,255,.4)}
.lon-card h4{font-size:12px;color:#94a3b8;font-weight:500;margin-bottom:6px;text-transform:uppercase;letter-spacing:.3px}
.lon-card .lon-val{font-size:26px;font-weight:700;color:#1a1a1a}
.lon-card .lon-sub{font-size:12px;color:#64748b;margin-top:3px}
.lon-breakdown{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;margin-bottom:20px}
.lon-breakdown-header{padding:16px 20px;font-weight:600;font-size:15px;color:#1a1a1a;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;gap:10px}
.lon-row{display:flex;justify-content:space-between;align-items:center;padding:12px 20px;border-bottom:1px solid #f1f5f9;font-size:14px}
.lon-row:last-child{border-bottom:none}
.lon-row-label{color:#64748b}
.lon-row-val{font-weight:600;color:#1a1a1a}
.lon-row.total{background:#f8f9fa;font-weight:700}
.lon-row.total .lon-row-label{color:#1a1a1a}
.lon-row.total .lon-row-val{color:#10b981;font-size:16px}
.lon-row.deduction .lon-row-val{color:#ef4444}
/* === EVA AI CHAT (compact) === */
.eko-layout{display:grid;grid-template-columns:1fr 340px;gap:20px;align-items:start}
.eko-main{display:flex;flex-direction:column;gap:16px}
.eko-tabs{display:flex;gap:4px;background:#fff;border-radius:10px;padding:4px;border:1px solid #e5e7eb;margin-bottom:4px}
.eko-tab{padding:9px 16px;border-radius:8px;font-size:13px;font-weight:500;color:#64748b;cursor:pointer;border:none;background:none;font-family:'Inter',sans-serif;transition:all .15s}
.eko-tab:hover{color:#334155;background:#f8f9fa}
.eko-tab.active{background:#024550;color:#fff;font-weight:600}
.eko-panel{display:none}
.eko-panel.active{display:block}
.eva-chat-wrap{background:#fff;border-radius:12px;border:1px solid #e5e7eb;display:flex;flex-direction:column;overflow:hidden;height:520px;position:sticky;top:20px}
.eva-chat-header{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid #f1f5f9;background:#fff}
.eva-avatar{width:34px;height:34px;border-radius:50%;object-fit:cover;border:2px solid #e5e7eb;flex-shrink:0}
.eva-header-info h3{font-size:13px;font-weight:600;color:#1a1a1a;margin-bottom:0}
.eva-header-info p{font-size:11px;color:#64748b}
.eva-online{display:inline-block;width:7px;height:7px;background:#10b981;border-radius:50%;margin-left:4px;vertical-align:middle}
.eva-messages{flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:10px}
.eva-messages::-webkit-scrollbar{width:3px}
.eva-messages::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:3px}
.eva-msg{display:flex;gap:8px;max-width:92%;animation:msgIn .3s ease}
.eva-msg.eva{align-self:flex-start}
.eva-msg.user{align-self:flex-end;flex-direction:row-reverse}
.eva-msg-avatar{width:26px;height:26px;border-radius:50%;object-fit:cover;flex-shrink:0;margin-top:2px}
.eva-msg-bubble{padding:8px 12px;border-radius:14px;font-size:12.5px;line-height:1.5;color:#1a1a1a}
.eva-msg.eva .eva-msg-bubble{background:#f1f5f9;border-bottom-left-radius:4px}
.eva-msg.user .eva-msg-bubble{background:#024550;color:#fff;border-bottom-right-radius:4px}
.eva-msg-time{font-size:9px;color:#94a3b8;margin-top:2px;padding:0 4px}
.eva-msg.user .eva-msg-time{text-align:right}
.eva-input-wrap{display:flex;align-items:center;gap:8px;padding:10px 14px;border-top:1px solid #f1f5f9;background:#fff}
.eva-input{flex:1;padding:8px 12px;border:1px solid #e5e7eb;border-radius:20px;font-size:12.5px;font-family:'Inter',sans-serif;resize:none;max-height:60px;line-height:1.4}
.eva-input:focus{outline:none;border-color:#024550;box-shadow:0 0 0 2px rgba(2,69,80,.1)}
.eva-send{width:34px;height:34px;border-radius:50%;background:#024550;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
.eva-send:hover{background:#035a6b}
.eva-send:disabled{background:#94a3b8;cursor:not-allowed}
.eva-send svg{width:15px;height:15px;stroke:#fff;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.eva-typing{display:flex;gap:4px;padding:8px 12px;background:#f1f5f9;border-radius:14px;border-bottom-left-radius:4px;align-self:flex-start}
.eva-typing span{width:6px;height:6px;background:#94a3b8;border-radius:50%;animation:typingDot 1.4s infinite}
.eva-typing span:nth-child(2){animation-delay:.2s}
.eva-typing span:nth-child(3){animation-delay:.4s}
@keyframes typingDot{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-5px);opacity:1}}
@keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.eva-suggestions{display:flex;flex-wrap:wrap;gap:5px;padding:0 14px 10px}
.eva-suggestion{padding:5px 10px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:14px;font-size:10.5px;color:#64748b;cursor:pointer;transition:all .15s;font-family:'Inter',sans-serif}
.eva-suggestion:hover{background:#e2e8f0;color:#334155;border-color:#94a3b8}
.lon-month-select{padding:8px 30px 8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-weight:500;font-family:'Inter',sans-serif;appearance:none;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23334155'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E") no-repeat right 8px center/16px;cursor:pointer}
.map-marker-offert{background:#eab308;border:2px solid #fff;border-radius:50%;width:14px;height:14px;box-shadow:0 1px 4px rgba(0,0,0,.3)}
.map-marker-lost{background:#ef4444;border:2px solid #fff;border-radius:50%;width:14px;height:14px;box-shadow:0 1px 4px rgba(0,0,0,.3)}
.map-marker-lead{background:#94a3b8;border:2px solid #fff;border-radius:50%;width:14px;height:14px;box-shadow:0 1px 4px rgba(0,0,0,.3)}
@media(max-width:1100px){.config-layout{grid-template-columns:1fr}.config-sidebar{position:static}.stat-grid{grid-template-columns:repeat(2,1fr)}}
@media(max-width:900px){.sidebar{display:none}.main-wrapper{margin-left:0}}
/* === TOP SELLERS === */
.top-sellers-list{padding:0}
.top-seller-item{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid #f1f5f9;transition:background .15s}
.top-seller-item:last-child{border-bottom:none}
.top-seller-item:hover{background:#fafbfc}
.top-seller-medal{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;flex-shrink:0}
.medal-gold{background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#78350f}
.medal-silver{background:linear-gradient(135deg,#d1d5db,#9ca3af);color:#374151}
.medal-bronze{background:linear-gradient(135deg,#d97706,#b45309);color:#fff}
.top-seller-info{flex:1;min-width:0}
.top-seller-name{font-size:14px;font-weight:600;color:#1a1a1a}
.top-seller-detail{font-size:12px;color:#94a3b8}
.top-seller-amount{text-align:right;font-size:15px;font-weight:700;color:#10b981}
/* === DASHBOARD CHARTS === */
.dash-charts{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px}
.dash-chart-panel{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden}
.dash-chart-panel .dash-panel-header{padding:12px 16px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #f1f5f9}
.dash-chart-panel .dash-panel-header svg{width:18px;height:18px;stroke:#64748b;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.dash-chart-panel .dash-panel-header h3{font-size:14px;font-weight:600;color:#1a1a1a}
.dash-chart-body{padding:14px}
.dash-chart-body canvas{max-height:200px}
/* === FÄLTSÄLJ === */
.falt-layout{display:grid;grid-template-columns:1fr 360px;gap:20px;align-items:start}
.falt-left{display:flex;flex-direction:column;gap:20px;min-width:0}
.falt-map{height:500px;width:100%;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden}
.falt-stats{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:16px}
.falt-stat{background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:12px 14px;text-align:center}
.falt-stat h4{font-size:10px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.falt-stat .fval{font-size:22px;font-weight:700;color:#1a1a1a}
.falt-stat .fsub{font-size:11px;color:#94a3b8;margin-top:2px}
.falt-sidebar{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden}
.falt-sidebar-header{padding:16px 18px;border-bottom:1px solid #f1f5f9;font-size:15px;font-weight:600;color:#1a1a1a}
.falt-list{max-height:450px;overflow-y:auto}
.falt-list::-webkit-scrollbar{width:4px}
.falt-list::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:4px}
.falt-item{display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid #f8f9fa;cursor:pointer;transition:background .15s}
.falt-item:hover{background:#fafbfc}
.falt-item-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.falt-item-info{flex:1;min-width:0}
.falt-item-addr{font-size:13px;font-weight:500;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.falt-item-detail{font-size:11px;color:#94a3b8}
.falt-item-status{font-size:11px;font-weight:600;padding:3px 8px;border-radius:10px;flex-shrink:0}
.falt-popup-btns{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
.falt-popup-btn{padding:4px 10px;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:'Inter',sans-serif;transition:all .15s}
.falt-popup-btn:hover{opacity:.85}
.falt-kalkyler{overflow-x:auto}
.falt-kalkyler-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.falt-kalkyler-header h3{font-size:16px;font-weight:700;color:#1a1a1a}
.falt-kalkyler-header .fk-count{font-size:13px;color:#94a3b8;font-weight:400}
.falt-kalkyler table{width:100%;border-collapse:collapse;background:#fff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb}
.falt-kalkyler thead th{background:#f8f9fa;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;padding:8px 10px;text-align:left;border-bottom:1px solid #e5e7eb;white-space:nowrap}
.falt-kalkyler tbody td{padding:8px 10px;font-size:12px;color:#1a1a1a;border-bottom:1px solid #f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.falt-kalkyler tbody td.addr-cell{max-width:180px}
.falt-kalkyler tbody tr:hover{background:#fafbfc}
.fk-badge{font-size:11px;font-weight:600;padding:3px 10px;border-radius:10px;display:inline-block}
.fk-badge.ny{background:#e0f2fe;color:#0284c7}
.fk-badge.skickad{background:#fef3c7;color:#b45309}
.fk-badge.godkand{background:#dcfce7;color:#15803d}
.fk-badge.forlorad{background:#fee2e2;color:#dc2626}
.fk-action{padding:4px 12px;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;font-weight:500;cursor:pointer;background:#fff;font-family:'Inter',sans-serif;transition:all .15s;color:#1a1a1a}
.fk-action:hover{background:#f1f5f9;border-color:#cbd5e1}
/* === PRODUCT CATALOG === */
.catalog-header{display:flex;align-items:center;gap:15px;margin-bottom:20px;flex-wrap:wrap}
.catalog-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.catalog-card{background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;transition:all .2s;cursor:pointer}
.catalog-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}
.catalog-card-img{width:100%;aspect-ratio:4/3;background:#f8f9fa;display:flex;align-items:center;justify-content:center;overflow:hidden}
.catalog-card-img img{width:100%;height:100%;object-fit:cover}
.catalog-card-img svg{width:48px;height:48px;stroke:#cbd5e1;fill:none;stroke-width:1.2}
.catalog-card-body{padding:14px 16px}
.catalog-card-cat{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:#64748b;margin-bottom:4px}
.catalog-card-name{font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:6px;line-height:1.3}
.catalog-card-desc{font-size:12px;color:#94a3b8;margin-bottom:8px;line-height:1.4}
.catalog-card-footer{display:flex;justify-content:space-between;align-items:center}
.catalog-card-price{font-size:16px;font-weight:700;color:#024550}
.catalog-card-badge{padding:3px 8px;border-radius:12px;font-size:10px;font-weight:600}
@media(max-width:768px){
.product-section{grid-template-columns:1fr;gap:20px}.option-choices{grid-template-columns:repeat(2,1fr)}.modal-grid{grid-template-columns:1fr}.stat-grid{grid-template-columns:1fr}.dash-grid{grid-template-columns:1fr}.dash-charts{grid-template-columns:1fr}.catalog-grid{grid-template-columns:repeat(2,1fr)}.kunder-stats{grid-template-columns:repeat(2,1fr)}.kunder-map{height:250px}
.main-wrapper{padding:16px 10px;margin-left:0 !important}
/* FältSälj mobil */
.falt-layout{grid-template-columns:1fr !important;gap:12px}
.falt-map{height:50vh !important;border-radius:10px}
.falt-stats{grid-template-columns:repeat(3,1fr) !important;gap:6px}
.falt-stat{padding:8px 6px}
.falt-stat .fval{font-size:18px}
.falt-stat h4{font-size:9px}
.falt-sidebar{border-radius:10px}
.falt-list{max-height:300px !important}
.falt-kalkyler{overflow-x:auto}
.falt-left{gap:12px}
.falt-kalkyler table{min-width:500px;font-size:11px}
.page-title{font-size:20px}
.page-subtitle{font-size:12px;margin-bottom:12px}
/* Mobil bottom nav */
.mobile-nav{display:flex !important}
body{padding-bottom:60px}
}
.mobile-nav{display:none;position:fixed;bottom:0;left:0;right:0;background:#024550;z-index:200;justify-content:space-around;padding:6px 0 env(safe-area-inset-bottom,6px);box-shadow:0 -2px 12px rgba(0,0,0,.15)}
.mobile-nav a{display:flex;flex-direction:column;align-items:center;gap:2px;color:rgba(255,255,255,.5);text-decoration:none;font-size:10px;font-weight:500;padding:4px 8px;transition:color .15s}
.mobile-nav a.active{color:#fff}
.mobile-nav a svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
@keyframes blink-red{0%,100%{opacity:1}50%{opacity:.3}}</style>
</head>
<body>
<!-- LOGIN SCREEN -->
<div class="login-screen" id="loginScreen">
<div class="login-image">
<div class="login-image-text">
<h2 id="loginGreetingBig" style="font-size:44px;font-weight:700;margin-bottom:16px">God morgon</h2>
<p id="loginQuote" style="font-size:18px;opacity:.9;font-style:italic;max-width:480px;line-height:1.6"></p>
</div>
</div>
<div class="login-panel">
<div class="login-logo">
<img src="https://www.solargroup.se/wp-content/uploads/2024/02/Solar_Energy_Group_26_-removebg-preview.png" alt="Solar Energy Group">
</div>
<h1 class="login-greeting" id="loginGreeting" style="font-size:22px">Välkommen tillbaka</h1>
<p class="login-subtitle">Logga in för att fortsätta</p>
<form onsubmit="doLogin(event)">
<div class="login-form-group">
<label>E-postadress</label>
<input type="text" id="loginEmail" placeholder="namn@solargroup.se eller admin" autocomplete="email">
</div>
<div class="login-form-group">
<label>Lösenord</label>
<div style="position:relative">
<input type="password" id="loginPassword" placeholder="Ange lösenord" autocomplete="current-password" style="padding-right:44px">
<button type="button" onclick="var p=document.getElementById('loginPassword');var t=p.type==='password'?'text':'password';p.type=t;this.innerHTML=t==='password'?'<svg width=20 height=20 viewBox="0 0 24 24" fill=none stroke="#94a3b8" stroke-width=2><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx=12 cy=12 r=3/></svg>':'<svg width=20 height=20 viewBox="0 0 24 24" fill=none stroke="#024550" stroke-width=2><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1=1 y1=1 x2=23 y2=23/></svg>'" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:4px;display:flex;align-items:center"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<div class="login-remember">
<label><input type="checkbox" checked> Kom ihåg mig</label>
<a href="#" class="login-forgot" onclick="alert('En återställningslänk har skickats till din e-post.\n\n(DEMO)');return false">Glömt lösenord?</a>
</div>
<button type="submit" class="login-btn">Logga in</button>
</form>
<div style="display:flex;align-items:center;gap:12px;margin:20px 0">
<div style="flex:1;height:1px;background:#e5e7eb"></div>
<span style="font-size:12px;color:#94a3b8;font-weight:500">eller</span>
<div style="flex:1;height:1px;background:#e5e7eb"></div>
</div>
<button onclick="googleLoginFromScreen()" style="width:100%;padding:12px 20px;background:#fff;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;color:#1a1a1a;display:flex;align-items:center;justify-content:center;gap:10px;transition:all .2s" onmouseover="this.style.background='#f8fafc';this.style.borderColor='#024550'" onmouseout="this.style.background='#fff';this.style.borderColor='#e5e7eb'">
<svg width="20" height="20" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
Logga in med Google
</button>
<div id="loginError" style="display:none;background:#fef2f2;border:1px solid #fecaca;color:#dc2626;padding:10px 14px;border-radius:8px;font-size:13px;margin-top:12px;text-align:center"></div>
<div id="loginPending" style="display:none;background:#fffbeb;border:1px solid #fde68a;color:#92400e;padding:10px 14px;border-radius:8px;font-size:13px;margin-top:12px;text-align:center"></div>
<p style="text-align:center;margin-top:16px;font-size:13px;color:#64748b">Inget konto? <a href="#" onclick="showRegisterForm();return false" style="color:#024550;font-weight:600;text-decoration:none">Skapa konto</a></p>
<!-- Registreringsformulär (dolt) -->
<div id="registerForm" style="display:none;margin-top:16px;padding:16px;background:#f8fafc;border-radius:12px;border:1px solid #e5e7eb">
<h3 style="margin:0 0 12px;font-size:16px;color:#1e293b">Skapa konto</h3>
<div class="login-form-group"><label>Namn</label><input type="text" id="regName" placeholder="Ditt namn"></div>
<div class="login-form-group"><label>E-postadress</label><input type="email" id="regEmail" placeholder="namn@solargroup.se"></div>
<div class="login-form-group"><label>Lösenord</label><input type="password" id="regPassword" placeholder="Minst 4 tecken"></div>
<button onclick="doRegister()" class="login-btn" style="margin-top:8px">Skapa konto</button>
<div id="regError" style="display:none;background:#fef2f2;border:1px solid #fecaca;color:#dc2626;padding:8px 12px;border-radius:8px;font-size:12px;margin-top:8px;text-align:center"></div>
<div id="regSuccess" style="display:none;background:#f0fdf4;border:1px solid #bbf7d0;color:#166534;padding:8px 12px;border-radius:8px;font-size:12px;margin-top:8px;text-align:center"></div>
<p style="text-align:center;margin-top:10px;font-size:12px;color:#64748b"><a href="#" onclick="hideRegisterForm();return false" style="color:#024550;text-decoration:none">← Tillbaka till inloggning</a></p>
</div>
<div class="login-footer">Solar Energy Group AB • Solar Sales Suite v1.0</div>
</div>
</div>
<!-- SIDEBAR -->
<nav class="sidebar" style="display:none" id="appSidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-icon"><img src="https://www.solargroup.se/wp-content/uploads/2024/02/Solar_Energy_Group_26_-removebg-preview.png" alt="Solar Energy Group"></div>
</div>
<div class="sidebar-menu">
<div class="sidebar-section-label">Försäljning</div>
<a class="nav-item active" data-page="oversikt"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></span>Dashboard</a>
<a class="nav-item" data-page="faltsalj"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>FältSälj</a>
<a class="nav-item" data-page="leads"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="11" x2="19" y2="17"/><line x1="16" y1="14" x2="22" y2="14"/></svg></span>Leads</a>
<a class="nav-item" data-page="kunder"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>Kunder</a>
<a class="nav-item" data-page="produkter"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg></span>Produktkatalog</a>
<a class="nav-item" data-page="konfigurator"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></span>Kalkyler</a>
<a class="nav-item" data-page="projekt"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>Affärer</a>
<a class="nav-item" data-page="projektering"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg></span>Projektflöde</a>
<a class="nav-item" data-page="ue"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>Entreprenörer</a>
<a class="nav-item" data-page="inkop"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg></span>Inköp</a>
<a class="nav-item" data-page="leverantorer"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg></span>Leverantörer</a>
<a class="nav-item" data-page="ekonomi"><span class="nav-icon"><svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></span>Ekonomi</a>
<a class="nav-item" data-page="bildgen"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></span>Bildgenerering</a>
<a class="nav-item" data-page="personal"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg></span>Personal</a>
<div class="sidebar-divider"></div>
<div class="sidebar-section-label">Mina Sidor</div>
<a class="nav-item" data-page="kalender"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg></span>Kalender</a>
<a class="nav-item" data-page="inkorg"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></span>Inkorg</a>
<a class="nav-item" data-page="dagrapport"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg></span>Dagrapport</a>
<a class="nav-item" data-page="minlon"><span class="nav-icon"><svg viewBox="0 0 24 24"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg></span>Min Lön</a>
<div class="sidebar-divider"></div>
<a class="nav-item" data-page="admin" id="navAdmin" style="display:none"><span class="nav-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>Admin <span id="adminPendingBadge" style="display:none;background:#ef4444;color:#fff;border-radius:10px;padding:1px 7px;font-size:10px;font-weight:700;margin-left:4px"></span></a>
<a class="nav-item" data-page="settings"><span class="nav-icon"><svg viewBox="0 0 24 24"><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.68 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.6 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.68a1.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></span>Inställningar</a>
</div>
<div class="sidebar-bottom">
<div class="sidebar-user">
<div class="sidebar-user-info"><span>Inloggad som</span><strong>admin@solargroup.se</strong></div>
</div>
<button class="sidebar-logout"><svg viewBox="0 0 24 24"><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>Logga ut</button>
</div>
</nav>
<!-- MAIN -->
<main class="main-wrapper" style="display:none" id="appMain">
<!-- DASHBOARD -->
<div class="page-content active" id="page-oversikt">
<h1 class="page-title">Dashboard</h1>
<p class="page-subtitle">Översikt över din försäljning och aktiviteter</p>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-card-info"><h4>Totalt Värde</h4><div class="stat-value" id="dashTotalValue">-</div></div>
<div class="stat-card-icon green"><svg viewBox="0 0 24 24"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></div>
</div>
<div class="stat-card">
<div class="stat-card-info"><h4>Pågående Offerter</h4><div class="stat-value" id="dashOffertar">-</div></div>
<div class="stat-card-icon blue"><svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div>
</div>
<div class="stat-card">
<div class="stat-card-info"><h4>Kunder</h4><div class="stat-value" id="dashKunder">-</div></div>
<div class="stat-card-icon purple"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
</div>
<div class="stat-card">
<div class="stat-card-info"><h4>Accepterade</h4><div class="stat-value" id="dashAccepted">-</div></div>
<div class="stat-card-icon teal"><svg viewBox="0 0 24 24"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></div>
</div>
</div>
<div class="dash-grid" style="grid-template-columns:1fr 1fr 1fr">
<div class="dash-panel">
<div class="dash-panel-header">
<svg viewBox="0 0 24 24"><path d="M6 9l6-6 6 6"/><path d="M12 3v14"/><circle cx="6" cy="20" r="1"/><circle cx="12" cy="20" r="1"/><circle cx="18" cy="20" r="1"/></svg>
<h3>Top Säljare</h3>
</div>
<div class="dash-panel-body top-sellers-list" id="dashTopSellers"><div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Laddar...</div></div>
</div>
<div class="dash-panel">
<div class="dash-panel-header">
<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<h3>Senaste Affärer</h3>
</div>
<div class="dash-panel-body" id="dashRecentDeals"><div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Laddar...</div></div>
</div>
<div class="dash-panel">
<div class="dash-panel-header">
<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
<h3>Kommande Möten</h3>
</div>
<div class="dash-panel-body" id="dashMeetings"><div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Inga möten inbokade</div></div>
</div>
</div>
<div class="dash-charts">
<div class="dash-chart-panel">
<div class="dash-panel-header">
<svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
<h3>Försäljning Februari 2026</h3>
</div>
<div class="dash-chart-body"><canvas id="chartMonthly"></canvas></div>
</div>
<div class="dash-chart-panel">
<div class="dash-panel-header">
<svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
<h3>Helårsöversikt 2026</h3>
</div>
<div class="dash-chart-body"><canvas id="chartYearly"></canvas></div>
</div>
</div>
</div>
<!-- FÄLTSÄLJ -->
<div class="page-content" id="page-faltsalj">
<h1 class="page-title">FältSälj</h1>
<p class="page-subtitle">Sök adress, se solpotential och prospektera</p>
<!-- Adresssökning -->
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center">
<div style="flex:1;position:relative" id="faltSearchWrap">
<input type="text" id="faltAddressInput" placeholder="Skriv adress..." autocomplete="off" onfocus="showAddressHistory()" style="width:100%;padding:12px 16px 12px 40px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:'Inter',sans-serif">
<svg style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:18px;height:18px;stroke:#94a3b8;fill:none;stroke-width:2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<div id="addressHistoryDropdown" style="display:none;position:absolute;top:100%;left:0;right:0;background:#fff;border:1px solid #e5e7eb;border-radius:0 0 10px 10px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:500;max-height:260px;overflow-y:auto"></div>
</div>
<button class="dummy-btn" onclick="faltUseMyLocation()"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4m0 12v4m10-10h-4M6 12H2"/><circle cx="12" cy="12" r="3"/></svg> Min plats</button>
<button class="dummy-btn" id="btnDrawArea" onclick="startDrawArea()" style="background:#024550;color:#fff"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg> Markera område</button>
</div>
<!-- Stats -->
<div class="falt-stats">
<div class="falt-stat"><h4>Planerade</h4><div class="fval" id="faltTotal">0</div><div class="fsub">att besöka</div></div>
<div class="falt-stat"><h4>Besökta</h4><div class="fval" id="faltVisited">0</div><div class="fsub" id="faltVisitedPct">0%</div></div>
<div class="falt-stat"><h4>Leads</h4><div class="fval" id="faltInterested">0</div><div class="fsub" style="color:#10b981">intresserade</div></div>
<div class="falt-stat"><h4>Avböjt</h4><div class="fval" id="faltNotInt">0</div><div class="fsub" style="color:#ef4444">ej intresserad</div></div>
<div class="falt-stat"><h4>Återbesök</h4><div class="fval" id="faltNotHome">0</div><div class="fsub" style="color:#eab308">ej hemma</div></div>
</div>
<div class="falt-layout">
<div class="falt-left">
<div class="falt-map" id="faltMap"></div>
<!-- Leads / Kalkyler (fast under kartan) -->
<div class="falt-kalkyler">
<div class="falt-kalkyler-header">
<h3>Leads & Kalkyler <span class="fk-count" id="fkCount"></span></h3>
</div>
<table>
<thead><tr><th>Status</th><th>Adress</th><th>Kund</th><th>Tel</th><th>Datum</th><th></th></tr></thead>
<tbody id="faltKalkylBody"></tbody>
</table>
</div>
</div>
<div class="falt-sidebar">
<!-- Fastighetsinformation vid klick -->
<div class="falt-sidebar-header">Fastighet</div>
<div id="solarDataPanel" style="padding:18px">
<div style="text-align:center;padding:40px 20px;color:#94a3b8;font-size:14px">
Klicka på ett hus på kartan eller sök adress
</div>
</div>
<!-- Prospektlista -->
<div class="falt-sidebar-header" style="border-top:1px solid #e5e7eb;display:flex;align-items:center;justify-content:space-between">
<span>Planerad rutt <span id="faltListCount" style="color:#94a3b8;font-weight:400;font-size:13px;margin-left:4px"></span></span>
<div style="display:flex;gap:4px;align-items:center">
<button onclick="faltListFilter='all';renderFaltList()" id="fltAll" style="padding:2px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;cursor:pointer;background:#024550;color:#fff;font-family:inherit">Alla</button>
<button onclick="faltListFilter='unvisited';renderFaltList()" id="fltPlan" style="padding:2px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;cursor:pointer;background:#fff;color:#64748b;font-family:inherit">Planerade</button>
<button onclick="faltListFilter='interested';renderFaltList()" id="fltLead" style="padding:2px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;cursor:pointer;background:#fff;color:#64748b;font-family:inherit">Leads</button>
<button onclick="clearAllProspects()" style="padding:2px 8px;border:1px solid #fee2e2;border-radius:6px;font-size:11px;cursor:pointer;background:#fff;color:#ef4444;font-family:inherit" title="Rensa alla">Rensa</button>
</div>
</div>
<div class="falt-list" id="faltList" style="max-height:400px;overflow-y:auto"></div>
<!-- Ruttknappar -->
<div style="padding:12px;display:flex;gap:6px;border-top:1px solid #e5e7eb;flex-wrap:wrap">
<button onclick="showSaveRouteModal()" style="flex:1;padding:8px 10px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:5px"><svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Spara</button>
<button onclick="showLoadRouteModal()" style="flex:1;padding:8px 10px;background:#f0f9ff;color:#1e40af;border:1px solid #bfdbfe;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:5px"><svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>Ladda</button>
<button onclick="startStreetViewRoute()" style="flex:1;padding:8px 10px;background:#f5f3ff;color:#6d28d9;border:1px solid #ddd6fe;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:5px"><svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><circle cx="12" cy="5" r="3"/><path d="M12 8v4"/><path d="M8 20l4-8 4 8"/></svg>SV-rutt</button>
</div>
<div id="currentRouteInfo" style="display:none;padding:8px 12px;background:#ecfdf5;border-top:1px solid #bbf7d0;font-size:11px;color:#166534"></div>
</div>
</div>
<!-- Check-in modal -->
<div id="checkinModal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:16px;padding:28px;width:440px;max-width:95vw;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.2)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px">
<h3 style="font-size:18px;font-weight:700" id="checkinTitle">Incheckning</h3>
<button onclick="closeCheckin()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">×</button>
</div>
<div id="checkinAddr" style="font-size:14px;color:#64748b;margin-bottom:16px"></div>
<!-- Snabbknapp: Ny Kalkyl -->
<div id="checkinStep0">
<button onclick="checkinDirectToKalkyl()" style="width:100%;padding:16px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:16px;box-shadow:0 4px 12px rgba(2,69,80,.25)"><svg viewBox="0 0 24 24" style="width:20px;height:20px;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> Ny Kalkyl</button>
</div>
<!-- Steg 1: Resultat -->
<div id="checkinStep1">
<p style="font-size:14px;font-weight:600;margin-bottom:12px">Hur gick besöket?</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<button onclick="checkinResult('interested')" style="padding:16px;border:2px solid #dcfce7;border-radius:10px;background:#f0fdf4;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;color:#166534">Intresserad</button>
<button onclick="checkinResult('not_interested')" style="padding:16px;border:2px solid #fee2e2;border-radius:10px;background:#fef2f2;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;color:#991b1b">Ej intresserad</button>
<button onclick="checkinResult('not_home')" style="padding:16px;border:2px solid #fef9c3;border-radius:10px;background:#fffbeb;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;color:#854d0e">Ej hemma</button>
<button onclick="checkinResult('callback')" style="padding:16px;border:2px solid #dbeafe;border-radius:10px;background:#eff6ff;cursor:pointer;font-family:inherit;font-size:14px;font-weight:600;color:#1e40af">Boka återbesök</button>
</div>
</div>
<!-- Steg 2: Lead-info (visas vid intresserad) -->
<div id="checkinStep2" style="display:none">
<p style="font-size:14px;font-weight:600;margin-bottom:12px;color:#10b981">Kunden är intresserad! Fyll i uppgifter:</p>
<div style="display:grid;gap:10px">
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Kundens namn</label><input type="text" id="ciName" placeholder="Förnamn Efternamn" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Telefon</label><input type="tel" id="ciPhone" placeholder="070-123 45 67" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">E-post (valfritt)</label><input type="email" id="ciEmail" placeholder="kund@email.se" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Intresserad av</label>
<select id="ciProduct" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">
<option value="">Välj produkt...</option>
<option value="Solceller">Solceller</option>
<option value="Takbyte">Takbyte</option>
<option value="Fönster">Fönster</option>
<option value="Fasad">Fasadrenovering</option>
<option value="Laddbox">Laddbox</option>
<option value="Batteri">Batteri</option>
<option value="Värmepump">Värmepump</option>
<option value="Kombination">Kombination</option>
</select>
</div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Anteckning</label><textarea id="ciNote" rows="2" placeholder="T.ex. 'Vill ha offert inom 2 veckor, gammalt tak'" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical"></textarea></div>
</div>
<button onclick="saveCheckinLead()" style="width:100%;margin-top:14px;padding:12px;background:#10b981;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Spara lead</button>
</div>
<!-- Steg 2b: Notering (vid ej intresserad / ej hemma) -->
<div id="checkinStep2b" style="display:none">
<div style="margin-top:12px">
<label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Anteckning (valfritt)</label>
<textarea id="ciNoteSimple" rows="2" placeholder="T.ex. 'Hade redan solceller' eller 'Prova igen torsdag'" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical"></textarea>
</div>
<button onclick="saveCheckinSimple()" style="width:100%;margin-top:12px;padding:12px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Spara & nästa</button>
</div>
<!-- Steg 2c: Boka återbesök med datum/tid -->
<div id="checkinStep2c" style="display:none">
<p style="font-size:14px;font-weight:600;margin-bottom:12px;color:#1e40af">Boka återbesök</p>
<div style="display:grid;gap:10px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Datum</label><input type="date" id="ciCallbackDate" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Tid</label><input type="time" id="ciCallbackTime" value="10:00" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
</div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Anteckning</label><textarea id="ciCallbackNote" rows="2" placeholder="T.ex. 'Ring innan', 'Prata med frun'" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical"></textarea></div>
</div>
<button onclick="saveCheckinCallback()" style="width:100%;margin-top:12px;padding:12px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Spara & lägg i kalender</button>
</div>
</div>
</div>
</div>
<!-- LEADS -->
<div class="page-content" id="page-leads">
<h1 class="page-title">Leads</h1>
<p class="page-subtitle">Leads från fältsälj och inkommande förfrågningar</p>
<div style="padding:40px;text-align:center;color:#94a3b8;font-size:14px">Kommer snart</div>
</div>
<!-- KUNDER -->
<div class="page-content" id="page-kunder">
<h1 class="page-title">Kunder</h1>
<p class="page-subtitle">Kundregister <span id="customerCount" style="color:#94a3b8;font-weight:400;font-size:13px"></span></p>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="dummy-btn" onclick="showNewCustomerModal()">+ Ny kund</button>
<input type="text" id="custCityFilter" list="custCityList" placeholder="Filtrera stad..." oninput="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;width:160px">
<datalist id="custCityList"></datalist>
<select id="custStatusFilter" onchange="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:140px">
<option value="">Alla statusar</option>
<option value="offert">Ny order</option>
<option value="order">Godkänd</option>
<option value="projektering">Projektering</option>
<option value="bestallning">Beställning</option>
<option value="leverans">Leverans</option>
<option value="montering">Montering</option>
<option value="fardigstall">Färdigställt</option>
<option value="anger">Ånger</option>
<option value="ej_godkand">Ej godkänd</option>
<option value="avbrott">Avbrott</option>
</select>
<select id="custSaljStatusFilter" onchange="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:140px">
<option value="">Alla säljstatus</option>
<option value="godkand">Godkänd</option>
<option value="anger">ÅNGER</option>
<option value="inte_godkand">Inte godkänd</option>
<option value="ej_hanterad">Ej hanterad</option>
<option value="avvakta">Avvakta</option>
<option value="underpris">Underpris</option>
<option value="halv_provis">Halv provis</option>
<option value="ingen_provis">Ingen provis</option>
<option value="raddad_anger">Räddad ånger</option>
<option value="senare_loneunderlag">Senare löneunderlag</option>
</select>
<select id="custRegionFilter" onchange="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:140px">
<option value="">Alla regioner</option>
</select>
<input type="date" id="custDateFrom" onchange="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit" title="Affärer från datum">
<input type="date" id="custDateTo" onchange="saveCustFilters();applyCustomerFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit" title="Affärer till datum">
<button onclick="clearCustFilters()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;background:#f8fafc;color:#64748b;cursor:pointer" title="Rensa filter">✕ Rensa</button>
</div>
<table id="customerDT" class="display" style="width:100%">
<thead><tr><th>ID</th><th>Namn</th><th>Stad</th><th>Region</th><th>Säljare</th><th>Datum</th><th>Status</th><th>Status sälj</th><th>Status order</th><th>Antal</th><th>Totalvärde</th></tr></thead>
<tbody id="customerTableBody"></tbody>
</table>
</div>
<!-- KUNDDETALJ (full page) -->
<div class="page-content" id="page-customer-detail" style="max-width:1400px">
</div>
<!-- PROJEKTERING -->
<div class="page-content" id="page-projektering">
<div style="margin-bottom:12px">
<div style="display:flex;align-items:center;gap:16px;margin-bottom:4px">
<div>
<h1 class="page-title" style="margin-bottom:2px">Projektflöde</h1>
<p class="page-subtitle" style="margin:0">Pipeline - från order till färdigställt</p>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px">
<div id="pipelineAkut" style="background:#fef9c3;border:1.5px solid #f59e0b;border-radius:10px;padding:5px 12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px" onclick="toggleAkutExpand()">
<span style="font-size:13px">🔥</span>
<span style="font-size:11px;font-weight:700;color:#b45309">Akut</span>
<span id="akutBadge" style="font-size:10px;font-weight:600;background:#ef4444;color:#fff;padding:1px 6px;border-radius:8px">0</span>
<span id="akutSum" style="font-size:10px;color:#b45309"></span>
</div>
<div id="pipelineAvbrott" style="background:#fef2f2;border:1.5px solid #fca5a5;border-radius:10px;padding:5px 12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px" onclick="toggleAvbrottExpand()">
<span style="font-size:11px;font-weight:700;color:#ef4444">Avbrott</span>
<span id="avbrottBadge" style="font-size:10px;font-weight:600;background:#ef4444;color:#fff;padding:1px 6px;border-radius:8px">0</span>
<span id="avbrottSum" style="font-size:10px;color:#ef4444"></span>
</div>
</div>
<div id="avbrottList" style="display:none;margin-bottom:8px;max-height:300px;overflow-y:auto;background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px"></div>
<div id="akutList" style="display:none"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px">
<input type="date" id="pipelineDateFrom" onchange="loadDealPipeline()" style="padding:5px 8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit" title="Från datum">
<span style="font-size:12px;color:#94a3b8">–</span>
<input type="date" id="pipelineDateTo" onchange="loadDealPipeline()" style="padding:5px 8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit" title="Till datum">
<select id="pipelineRegion" onchange="loadDealPipeline()" style="padding:5px 8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">
<option value="">Alla regioner</option>
</select>
<select id="pipelineUE" onchange="loadDealPipeline()" style="padding:5px 8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;max-width:200px">
<option value="">Alla entreprenörer</option>
</select>
<select id="pipelineProduct" onchange="loadDealPipeline()" style="padding:5px 8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">
<option value="">Alla produkter</option>
</select>
<button onclick="clearPipelineFilters()" style="padding:5px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;cursor:pointer;background:#fff;font-family:inherit;color:#64748b">Rensa</button>
</div>
</div>
<div style="overflow-x:auto;padding-bottom:16px">
<div style="display:flex;gap:6px;min-width:max-content" id="pipelineColumns"></div>
</div>
</div>
<!-- UNDERENTREPRENÖRER -->
<div class="page-content" id="page-ue">
<h1 class="page-title">Underentreprenörer</h1>
<p class="page-subtitle">Alla UE-företag och kontaktpersoner <span id="ueCount" style="color:#94a3b8;font-weight:400;font-size:13px"></span></p>
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
<input type="text" id="ueSearch" placeholder="Sök företag, kontakt, telefon..." oninput="filterUE()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:250px">
<select id="ueSpecFilter" onchange="filterUE()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla specialiteter</option>
</select>
</div>
<div id="ueContainer"></div>
</div>
<!-- INKÖP -->
<div class="page-content" id="page-inkop">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<div>
<h1 class="page-title" style="margin-bottom:2px">Inköp</h1>
<p class="page-subtitle" style="margin:0">Inköpsordrar och materialbeställningar</p>
</div>
</div>
<!-- Tabs -->
<div style="display:flex;gap:0;border-bottom:2px solid #e5e7eb;margin-bottom:16px">
<button id="inkopTab1" onclick="switchInkopTab('deals')" style="padding:10px 24px;font-size:14px;font-weight:700;border:none;border-bottom:3px solid #024550;background:none;color:#024550;cursor:pointer;margin-bottom:-2px;font-family:inherit">Att beställa</button>
<button id="inkopTab2" onclick="switchInkopTab('orders')" style="padding:10px 24px;font-size:14px;font-weight:600;border:none;border-bottom:3px solid transparent;background:none;color:#94a3b8;cursor:pointer;margin-bottom:-2px;font-family:inherit">Inköpsordrar</button>
</div>
<!-- Tab 1: Deals needing purchase -->
<div id="inkopDealsTab">
<div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
<select id="inkopProductType" onchange="loadInkopDeals()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:180px">
<option value="">Alla typer</option>
<option value="fonster">Fönster</option>
<option value="tak">Tak</option>
<option value="solceller">Solceller</option>
<option value="sol_batteri">Sol & Batteri</option>
<option value="batteri">Batteri</option>
<option value="luftvarmepump">Luftvärmepump</option>
<option value="laddbox">Laddbox</option>
<option value="isolering">Isolering</option>
<option value="taktvatt">Taktvätt</option>
<option value="utvandig_malning">Utvändig målning</option>
<option value="fagelband">Fågelband</option>
</select>
<select id="inkopDealStatus" onchange="loadInkopDeals()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla statusar</option>
<option value="projektering">Projektering</option>
<option value="bestallning">Beställning</option>
<option value="leverans">Leverans</option>
<option value="montering">Montering</option>
<option value="besiktning">Besiktning</option>
<option value="order">Order</option>
</select>
<select id="inkopPoFilter" onchange="loadInkopDeals()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla</option>
<option value="without_po" selected>Utan inköpsorder</option>
<option value="with_po">Med inköpsorder</option>
</select>
</div>
<div id="inkopDealsStats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px"></div>
<table id="inkopDealsDT" class="display" style="width:100%">
<thead>
<tr>
<th style="width:30px"><input type="checkbox" id="inkopSelectAll" onclick="toggleAllDealSelect(this.checked)" style="width:16px;height:16px;cursor:pointer"></th>
<th>Affär</th>
<th>Kund</th>
<th>Adress</th>
<th>Produkter</th>
<th>Säljdatum</th>
<th>Status</th>
<th>Ordervärde</th>
<th>Inköp</th>
</tr>
</thead>
<tbody id="inkopDealsBody"></tbody>
</table>
</div>
<!-- Tab 2: Existing POs -->
<div id="inkopOrdersTab" style="display:none">
<div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
<select id="inkopOrderType" onchange="filterInkopOrders()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:180px">
<option value="">Alla typer</option>
<option value="fonster">Fönster</option>
<option value="tak">Tak</option>
<option value="solceller">Solceller</option>
<option value="sol_batteri">Sol & Batteri</option>
<option value="batteri">Batteri</option>
<option value="luftvarmepump">Luftvärmepump</option>
</select>
<select id="inkopOrderStatus" onchange="filterInkopOrders()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla statusar</option>
<option value="draft">Utkast</option>
<option value="ordered">Beställd</option>
<option value="confirmed">Bekräftad</option>
<option value="shipped">Skickad</option>
<option value="delivered">Levererad</option>
<option value="cancelled">Avbruten</option>
</select>
<select id="inkopOrderSupplier" onchange="filterInkopOrders()" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla leverantörer</option>
</select>
</div>
<div id="inkopOrderStats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;margin-bottom:16px"></div>
<table id="inkopOrdersDT" class="display" style="width:100%">
<thead>
<tr>
<th>Ordernr</th>
<th>Leverantör</th>
<th>Kund / Affär</th>
<th>Orderdatum</th>
<th>Förv. leverans</th>
<th>Status</th>
<th>Belopp</th>
<th>Rader</th>
</tr>
</thead>
<tbody id="inkopOrdersBody"></tbody>
</table>
</div>
</div>
<!-- LEVERANTÖRER -->
<div class="page-content" id="page-leverantorer">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<div>
<h1 class="page-title" style="margin-bottom:2px">Leverantörer</h1>
<p class="page-subtitle" style="margin:0">Materialleverantörer och artikelkatalog</p>
</div>
<button onclick="showNewSupplierModal()" style="padding:10px 20px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:linear-gradient(135deg,#024550,#036b78);color:#fff;cursor:pointer">+ Ny leverantör</button>
</div>
<div id="leverantorContainer"></div>
</div>
<!-- PROJEKT/AFFÄRER -->
<div class="page-content" id="page-projekt">
<h1 class="page-title">Affärer</h1>
<p class="page-subtitle">Pipeline och affärsöversikt</p>
<!-- Stats row -->
<div id="dealStatsRow" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px"> <div style="display:flex;align-items:center;gap:5px;padding:5px 12px;background:#fefce8;border:1px solid #fde68a;border-radius:8px"><span style="width:8px;height:8px;border-radius:50%;background:#eab308"></span><span style="font-size:12px;font-weight:600;color:#92400e">Offert</span><span style="font-size:12px;font-weight:700;color:#92400e" id="dStatOffert">0</span><span style="font-size:10px;color:#a16207" id="dSumOffert"></span></div> <div style="display:flex;align-items:center;gap:5px;padding:5px 12px;background:#f0fdf4;border:1px solid #86efac;border-radius:8px"><span style="width:8px;height:8px;border-radius:50%;background:#16a34a"></span><span style="font-size:12px;font-weight:600;color:#166534">Order</span><span style="font-size:12px;font-weight:700;color:#166534" id="dStatOrder">0</span><span style="font-size:10px;color:#15803d" id="dSumOrder"></span></div> <div style="display:flex;align-items:center;gap:5px;padding:5px 12px;background:#eff6ff;border:1px solid #93c5fd;border-radius:8px"><span style="width:8px;height:8px;border-radius:50%;background:#3b82f6"></span><span style="font-size:12px;font-weight:600;color:#1e40af">Pågående</span><span style="font-size:12px;font-weight:700;color:#1e40af" id="dStatPagaende">0</span><span style="font-size:10px;color:#1d4ed8" id="dSumPagaende"></span></div> <div style="display:flex;align-items:center;gap:5px;padding:5px 12px;background:#f0fdf4;border:1px solid #6ee7b7;border-radius:8px"><span style="width:8px;height:8px;border-radius:50%;background:#10b981"></span><span style="font-size:12px;font-weight:600;color:#065f46">Klart</span><span style="font-size:12px;font-weight:700;color:#065f46" id="dStatFardig">0</span><span style="font-size:10px;color:#047857" id="dSumFardig"></span></div> <div style="display:flex;align-items:center;gap:5px;padding:5px 12px;background:#fef2f2;border:1px solid #fca5a5;border-radius:8px"><span style="width:8px;height:8px;border-radius:50%;background:#ef4444"></span><span style="font-size:12px;font-weight:600;color:#991b1b">Ånger</span><span style="font-size:12px;font-weight:700;color:#991b1b" id="dStatLost">0</span><span style="font-size:10px;color:#b91c1c" id="dSumLost"></span></div> </div>
<!-- Toolbar -->
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="dummy-btn" onclick="showNewDealModal()">+ Ny affär</button>
<select id="dealFilterStatus" onchange="reloadDeals()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla statusar</option>
<option value="offert">Offert</option>
<option value="order">Order</option>
<option value="projektering">Projektering</option>
<option value="bestallning">Beställning</option>
<option value="leverans">Leverans</option>
<option value="montering">Montering</option>
<option value="besiktning">Besiktning</option>
<option value="fardigstall">Färdigställt</option>
<option value="anger">Ånger</option>
<option value="ej_godkand">Ej godkänd</option>
<option value="avbrott">Avbrott</option>
</select>
<select id="dealFilterProduct" onchange="reloadDeals()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla produkter</option>
<option value="solceller">Solceller</option>
<option value="sol_batteri">Sol & Batteri</option>
<option value="batteri">Batteri</option>
<option value="tak">Tak</option>
<option value="fonster">Fönster</option>
<option value="luftvarmepump">Luftvärmepump</option>
<option value="laddbox">Laddbox</option>
<option value="isolering">Isolering</option>
<option value="taktvatt">Taktvätt</option>
<option value="utvandig_malning">Utvändig målning</option>
<option value="fagelband">Fågelband</option>
</select>
<select id="dealFilterMonth" onchange="reloadDeals()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla månader</option>
</select>
<select id="dealFilterSaljStatus" onchange="reloadDeals()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla säljstatus</option>
<option value="godkand">Godkänd</option>
<option value="anger">ÅNGER</option>
<option value="inte_godkand">Inte godkänd</option>
<option value="ej_hanterad">Ej hanterad</option>
<option value="avvakta">Avvakta</option>
<option value="underpris">Underpris</option>
<option value="halv_provis">Halv provis</option>
<option value="ingen_provis">Ingen provis</option>
<option value="raddad_anger">Räddad ånger</option>
<option value="anger_fraga">Ånger?</option>
<option value="senare_loneunderlag">Senare löneunderlag</option>
</select>
<select id="dealFilterRegion" onchange="reloadDeals()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla regioner</option>
</select>
</div>
<!-- Table view (DataTable) -->
<div id="dealTableView">
<table id="dealsDT" class="display" style="width:100%">
<thead><tr>
<th>Affärsnr</th><th>Kund</th><th>Produkter</th><th style="text-align:right">Ordervärde</th><th>Säljare</th><th>Datum</th><th>Status</th><th>Status sälj</th><th>Status order</th><th>Region</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- PRODUKTKATALOG -->
<div class="page-content" id="page-produkter">
<h1 class="page-title">Produktkatalog</h1>
<p class="page-subtitle">Utforska vårt sortiment av produkter</p>
<div class="catalog-header">
<label class="config-label">Kategori:</label>
<select class="product-select" id="catalogCatSelect" onchange="filterCatalog()" style="min-width:200px">
<option value="">Alla produkter</option>
<option value="solceller">Solceller</option>
<optgroup label="Batteri">
<option value="g:batteri">Batteri (alla)</option>
<option value="batteri">Batterier</option>
<option value="batteri_utbyggnad">Utbyggnad</option>
</optgroup>
<option value="laddbox">Laddbox</option>
<option value="fonster">Fönster</option>
<option value="dorrar">Dörrar</option>
<option value="tak">Tak</option>
<option value="varmepump">Värmepump</option>
<option value="taktvatt">Taktvätt</option>
<option value="isolering">Isolering</option>
<option value="tjanster">Tjänster</option>
<option value="tillbehor">Tillbehör</option>
</select>
<div style="flex:1"></div>
<div style="position:relative;margin-right:12px"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#94a3b8;fill:none;stroke-width:2;position:absolute;left:10px;top:50%;transform:translateY(-50%)"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg><input id="catalogSearch" type="text" placeholder="Sök produkt..." oninput="filterCatalog()" autocomplete="off" style="padding:8px 12px 8px 32px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;width:200px;outline:none;transition:border-color .15s" onfocus="this.style.borderColor='#024550'" onblur="this.style.borderColor='#e5e7eb'"></div>
<span id="catalogCount" style="font-size:13px;color:#94a3b8;margin-right:12px"></span>
<div style="display:flex;gap:4px;margin-right:8px">
<button id="catViewGrid" onclick="setCatalogView('grid')" style="padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;background:#024550;color:#fff;cursor:pointer" title="Rutnät"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg></button>
<button id="catViewList" onclick="setCatalogView('list')" style="padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;color:#64748b;cursor:pointer" title="Lista"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg></button>
</div>
<button onclick="showAddProductModal()" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Ny produkt</button>
<button onclick="showCategoryEditor()" style="padding:8px 16px;background:#f59e0b;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;margin-left:6px">Kategorier</button>
</div>
<div class="catalog-grid" id="catalogGrid"></div>
</div>
<!-- KONFIGURATOR -->
<div class="page-content" id="page-konfigurator">
<!-- KALKYL-LISTA (visas först) -->
<div id="kalkylListView">
<h1 class="page-title">Kalkyler</h1>
<p class="page-subtitle">Dina sparade kalkyler och offerter</p>
<div style="display:flex;gap:10px;margin-bottom:20px;align-items:center;flex-wrap:wrap">
<button class="dummy-btn" onclick="showKalkylConfig()">+ Ny Kalkyl</button>
<select id="quotesFilterStatus" onchange="loadQuotesList()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="">Alla statusar</option>
<option value="utkast">Utkast</option>
<option value="skickad">Skickad</option>
<option value="godkänd">Godkänd</option>
<option value="förlorad">Förlorad</option>
</select>
<input id="quotesSearch" type="text" placeholder="Sök kund/adress..." oninput="loadQuotesList()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:200px">
</div>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden">
<table style="width:100%;border-collapse:collapse">
<thead><tr style="background:#f8f9fa">
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Kalkyl</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Kund</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Produkt</th>
<th style="padding:10px 14px;text-align:right;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Belopp</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Bilder</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Säljare</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Datum</th>
<th style="padding:10px 14px;text-align:left;font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Status</th>
<th style="padding:10px 14px;font-size:11px"></th>
</tr></thead>
<tbody id="quotesListBody">
<tr><td colspan="9" style="padding:40px;text-align:center;color:#94a3b8;font-size:14px">Laddar kalkyler...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- KONFIGURATOR (dold tills man klickar Ny Kalkyl) -->
<div id="kalkylConfigView" style="display:none">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<button class="dummy-btn secondary" onclick="kalkylGoBack()" style="padding:8px 14px"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> Tillbaka</button>
<div><h1 class="page-title" style="margin-bottom:0">Ny Kalkyl</h1></div>
</div>
<!-- Kundbanner (fylls av populateKalkylFromCustomer) -->
<div id="kalkylCustomerBanner"></div>
<!-- Prospektbilder (expanderbar) -->
<div id="kalkylPhotosSection" style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:20px;display:none">
<div style="display:flex;align-items:center;justify-content:space-between;cursor:pointer" onclick="toggleKalkylPhotos()">
<div style="display:flex;align-items:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#f59e0b;stroke-width:2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<span style="font-size:14px;font-weight:700;color:#1a1a1a">Prospektbilder</span>
<span id="kpCount" style="font-size:12px;color:#64748b;font-weight:400;margin-left:4px"></span>
</div>
<svg id="kpChevron" viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#94a3b8;stroke-width:2;transition:transform .2s"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<!-- Collapsed: thumbnails of selected images -->
<div id="kpThumbsCollapsed" style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;overflow:hidden;max-height:72px"></div>
<!-- Expanded: full bildgenerering -->
<div id="kpExpanded" style="display:none;margin-top:14px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<!-- Vänster: Prospektbilder + Bildgenerering -->
<div>
<!-- Prospektbilder från fältsälj -->
<div id="kpProspectPhotos" style="margin-bottom:14px"></div>
<!-- Steg 1: Foto av huset -->
<div style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">1. Foto av huset</label>
<div style="display:flex;gap:8px">
<input type="file" id="kpBgFileInput" accept="image/*" style="display:none" onchange="handleKpBgUpload(this)">
<input type="file" id="kpBgCameraInput" accept="image/*" capture="environment" style="display:none" onchange="handleKpBgUpload(this)">
<button onclick="document.getElementById('kpBgFileInput').click()" style="flex:1;padding:8px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:12px">Ladda upp</button>
<button onclick="document.getElementById('kpBgCameraInput').click()" style="flex:1;padding:8px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:12px">Ta foto</button>
</div>
<div id="kpBgPreview" style="margin-top:8px;display:none;position:relative">
<img id="kpBgPreviewImg" style="width:100%;max-height:300px;object-fit:contain;border-radius:8px;background:#f1f5f9">
<button onclick="clearKpBgUpload()" style="position:absolute;top:4px;right:4px;background:#ef4444;color:#fff;border:none;border-radius:6px;width:24px;height:24px;cursor:pointer;font-size:12px">✕</button>
</div>
</div>
<!-- Steg 2: Vad vill du visa? -->
<div style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">2. Vad vill du visa?</label>
<input type="hidden" id="kpProjectType" value="">
<input type="hidden" id="kpPrompt" value="">
<div id="kpOptionGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:6px"></div>
</div>
<!-- Steg 3: Stil -->
<div id="kpDetailsPanel" style="display:none;margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">3. Välj stil</label>
<div id="kpSubOptions" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px"></div>
<div id="kpCountPanel" style="display:none;margin-top:10px">
<label id="kpCountLabel" style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">Antal paneler</label>
<div id="kpCountOptions" style="display:flex;flex-wrap:wrap;gap:5px"></div>
</div>
</div>
<!-- Extra detaljer -->
<div style="margin-bottom:10px">
<label style="display:block;font-size:12px;font-weight:600;color:#334155;margin-bottom:4px">Extra detaljer (valfritt)</label>
<input type="text" id="kpExtraPrompt" placeholder="T.ex. 'sommardag, blå himmel'" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box">
</div>
<!-- Motor + Generera -->
<div style="display:flex;gap:8px;align-items:center;margin-bottom:10px">
<select id="kpProvider" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:11px;font-family:inherit;color:#64748b">
<option value="openai">OpenAI GPT Image (~0.65 kr/bild)</option>
<option value="openai-mini">OpenAI Mini (~0.20 kr/bild)</option>
<option value="gemini">Google Gemini (~0.30 kr/bild)</option>
</select>
<button id="kpGenerateBtn" onclick="generateKpImage()" style="flex:1;padding:10px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Generera bild</button>
</div>
</div>
<!-- Höger: Resultat + Galleri -->
<div>
<div id="kpResultArea">
<div id="kpPlaceholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:280px;border:2px dashed #e5e7eb;border-radius:12px;color:#94a3b8">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<p style="margin-top:10px;font-size:13px">Genererad bild visas här</p>
<p style="margin-top:2px;font-size:11px">Välj typ och klicka "Generera bild"</p>
</div>
<div id="kpLoading" style="display:none;flex-direction:column;align-items:center;justify-content:center;height:280px;border:2px dashed #e5e7eb;border-radius:12px;color:#024550">
<div style="width:36px;height:36px;border:3px solid #e5e7eb;border-top:3px solid #024550;border-radius:50%;animation:spin 1s linear infinite"></div>
<p style="margin-top:12px;font-size:13px;font-weight:500">Genererar bild...</p>
<p style="margin-top:2px;font-size:11px;color:#94a3b8">Kan ta upp till 30 sekunder</p>
</div>
<div id="kpResult" style="display:none">
<img id="kpResultImg" style="width:100%;border-radius:10px;border:1px solid #e5e7eb;cursor:pointer" onclick="openLightbox(this.src)">
<div style="display:flex;gap:6px;margin-top:8px">
<button onclick="saveKpResultToGallery()" style="flex:1;padding:8px;border:1.5px solid #059669;border-radius:8px;background:#f0fdf4;cursor:pointer;font-family:inherit;font-size:12px;font-weight:600;color:#059669">✓ Spara i kalkyl</button>
<button onclick="downloadKpResult()" style="flex:1;padding:8px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:12px">Ladda ner</button>
<button onclick="resetKpResult()" style="flex:1;padding:8px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:12px">Ny bild</button>
</div>
</div>
</div>
<!-- Sparade bilder i kalkylen -->
<div id="kpSavedGallery" style="margin-top:14px">
<h4 style="font-size:13px;font-weight:600;margin-bottom:8px;color:#334155">Bilder i kalkylen</h4>
<div id="kpGallery" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px"></div>
<div id="kpEmpty" style="padding:16px;text-align:center;color:#94a3b8;font-size:12px;border:2px dashed #e5e7eb;border-radius:8px">
Inga bilder sparade ännu.
</div>
</div>
</div>
</div>
</div>
</div>
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:20px;margin-bottom:20px;display:grid;grid-template-columns:1fr 1fr;gap:16px"><div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Kalkylnamn</label><input type="text" id="cfgQuoteName" placeholder="T.ex. Villa Andersson" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box"></div><div style="position:relative"><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Kund</label><input type="text" id="cfgCustomerSearch" placeholder="Sök kund eller lämna tomt..." style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box"><div id="cfgCustomerDropdown" style="display:none;position:absolute;left:0;right:0;background:#fff;border:1px solid #e5e7eb;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.12);max-height:250px;overflow-y:auto;z-index:100;margin-top:4px"></div></div></div>
<div class="config-header" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:20px">
<label class="config-label" style="font-size:12px;font-weight:600;color:#64748b">Kategori:</label>
<select class="product-select" id="categorySelect" onchange="changeCategory()" style="min-width:200px;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">
<option value="">Välj kategori...</option>
<option value="solceller">Solceller</option>
<option value="batteri">Batteri</option>
<option value="laddbox">Laddbox</option>
<option value="taktvatt">Taktvätt</option>
<option value="varmepump">Värmepump</option>
<option value="fonster">Fönster</option>
<option value="tak">Tak</option>
<option value="isolering">Isolering</option>
</select>
</div>
<div id="cfgCategoryGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px"></div>
<!-- SOLCELLS-KONFIGURATOR -->
<div id="solarConfigView">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Välj solpanel -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj solpanel</h3>
</div>
<div id="solarPanelCards" style="display:grid;gap:10px"></div>
</div>
<!-- Steg 2: Antal paneler -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#3b82f6">2</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Antal paneler</h3>
</div>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px">
<input type="range" id="solPanelCount" min="8" max="80" step="2" value="14" oninput="updateSolarCalc()" style="flex:1;accent-color:#024550">
<div style="background:#f0f9ff;border:2px solid #3b82f6;border-radius:10px;padding:8px 16px;text-align:center;min-width:80px">
<div id="solPanelCountVal" style="font-size:24px;font-weight:700;color:#1e40af">14</div>
<div style="font-size:10px;color:#64748b;font-weight:600">PANELER</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<div style="background:#f8fafc;padding:10px;border-radius:8px;text-align:center">
<div style="font-size:10px;color:#64748b;font-weight:600">EFFEKT</div>
<div id="solKwpVal" style="font-size:16px;font-weight:700;color:#1a1a1a">6.0 kWp</div>
</div>
<div style="background:#f8fafc;padding:10px;border-radius:8px;text-align:center">
<div style="font-size:10px;color:#64748b;font-weight:600">TAKYTA</div>
<div id="solRoofVal" style="font-size:16px;font-weight:700;color:#1a1a1a">24 m²</div>
</div>
<div style="background:#f8fafc;padding:10px;border-radius:8px;text-align:center">
<div style="font-size:10px;color:#64748b;font-weight:600">PRODUKTION</div>
<div id="solProdVal" style="font-size:16px;font-weight:700;color:#1a1a1a">~5 400 kWh</div>
</div>
</div>
</div>
<!-- Steg 3: Batteri -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#faf5ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#a855f7">3</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Batteri <span style="font-size:12px;color:#94a3b8;font-weight:400">(valfritt)</span></h3>
</div>
<div id="solarBatteryCards" style="display:grid;gap:10px">
<div class="sol-addon-card" data-battery="0" onclick="selectBattery(this)" style="padding:14px 16px;border:2px solid #3b82f6;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:#f0f9ff">
<div style="width:20px;height:20px;border:2px solid #3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"><div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div></div>
<div style="flex:1"><div style="font-weight:600;font-size:14px">Inget batteri</div></div>
<div style="font-weight:700;font-size:14px;color:#1a1a1a">0 kr</div>
</div>
</div>
</div>
<!-- Steg 4: Elbilsladdare -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#fef9c3;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#ca8a04">4</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Elbilsladdare <span style="font-size:12px;color:#94a3b8;font-weight:400">(valfritt)</span></h3>
</div>
<div id="solarChargerCards" style="display:grid;gap:10px">
<div class="sol-addon-card" data-charger="0" onclick="selectCharger(this)" style="padding:14px 16px;border:2px solid #3b82f6;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:#f0f9ff">
<div style="width:20px;height:20px;border:2px solid #3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"><div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div></div>
<div style="flex:1"><div style="font-weight:600;font-size:14px">Ingen laddare</div></div>
<div style="font-weight:700;font-size:14px;color:#1a1a1a">0 kr</div>
</div>
</div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Material <span style="font-size:10px;color:#94a3b8">(paneler, inverter, montering)</span></span><span class="price-line-value" id="solPrMaterial">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Installation <span style="font-size:10px;color:#94a3b8">(arbete, el, projekt)</span></span><span class="price-line-value" id="solPrLabor">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Batteri</span><span class="price-line-value" id="solPrBattery">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Elbilsladdare</span><span class="price-line-value" id="solPrCharger">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="solPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<!-- Välj avdragstyp -->
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setDeductionType('green')" class="deduct-btn" data-dt="green" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">Grönt teknik<br><span style="font-size:9px;font-weight:400;opacity:.8">20% av allt</span></button>
<button onclick="setDeductionType('rot')" class="deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setDeductionType('both')" class="deduct-btn" data-dt="both" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Båda<br><span style="font-size:9px;font-weight:400;opacity:.8">max av båda</span></button>
<button onclick="setDeductionType('none')" class="deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<!-- Avdragssumma -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span id="solDeductLabel" style="font-size:12px;font-weight:700;color:#059669">GRÖNT TEKNIK-AVDRAG</span>
<span id="solPrGreenTech" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="solDeductDetail" style="font-size:11px;color:#6b7280;margin-bottom:8px"></div>
<!-- Antal ägare -->
<div id="solOwnerSection" style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setOwnerCount(1)" class="owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setOwnerCount(2)" class="owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
<!-- Slider -->
<div id="solSliderSection">
<div style="display:flex;justify-content:space-between;font-size:11px;color:#6b7280;margin-bottom:4px">
<span>Kvarvarande avdrag</span>
<span id="solGreenSliderVal" style="font-weight:600;color:#059669">50 000 kr</span>
</div>
<input type="range" id="solGreenSlider" min="0" max="50000" step="5000" value="50000" oninput="updateGreenSlider()" style="width:100%;accent-color:#059669">
<div style="display:flex;justify-content:space-between;font-size:10px;color:#94a3b8"><span>0 kr</span><span id="solGreenSliderMax">50 000 kr</span></div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="solPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<!-- Finansiering -->
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setFinanceYears(5)" class="fin-yr-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setFinanceYears(10)" class="fin-yr-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setFinanceYears(15)" class="fin-yr-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
<button onclick="setFinanceYears(20)" class="fin-yr-btn" data-yr="20" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">20 år</button>
<button onclick="setFinanceYears(25)" class="fin-yr-btn" data-yr="25" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">25 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="solFinanceInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="solPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<!-- Uppskattad besparing -->
<div style="background:#ecfdf5;border-radius:10px;padding:14px;margin-bottom:16px">
<div style="font-size:11px;color:#059669;font-weight:600;margin-bottom:4px">UPPSKATTAD BESPARING</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Per år</span>
<span id="solPrSaving" style="font-size:20px;font-weight:700;color:#166534">0 kr/år</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-top:4px">
<span style="font-size:13px;color:#334155">Återbetalningstid</span>
<span id="solPrPayback" style="font-size:16px;font-weight:700;color:#166534">- år</span>
</div>
</div>
<button id="saveKalkylBtn" onclick="saveKalkylDraft()" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Spara utkast
</button>
<button class="cart-btn" onclick="goToAffar()" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6"/><path d="M9 12h6M9 16h6"/></svg>
Lägg till order
</button>
</div>
</div>
</div>
</div>
</div>
<!-- FÖNSTER/ÖVRIG KONFIGURATOR (dold vid solceller) -->
<div id="genericConfigView" style="display:none">
<div style="padding:40px;text-align:center;color:#94a3b8">
<svg viewBox="0 0 24 24" style="width:48px;height:48px;stroke:currentColor;fill:none;stroke-width:1.5;margin-bottom:12px"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
<p style="font-size:16px;font-weight:600">Konfigurator för denna kategori kommer snart</p>
<p style="font-size:13px">Solceller-konfiguratorn är aktiv — välj Solceller ovan.</p>
</div>
</div>
<!-- BATTERI (FRISTÅENDE) KONFIGURATOR -->
<div id="batteriConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<div style="background:#dbeafe;border:1px solid #93c5fd;border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:13px;color:#1e40af">
<strong>Observera:</strong> Detta är för enbart batteriinstallation utan solceller.
</div>
<!-- Steg 1: Batterimärke -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj batterimärke</h3>
</div>
<div id="batBrandCards" style="display:grid;gap:10px"></div>
</div>
<!-- Steg 2: Batteristorlek -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#3b82f6">2</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj batteristorlek</h3>
</div>
<div id="batSizeCards" style="display:grid;gap:10px"></div>
</div>
<!-- Steg 3: Tillägg -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#faf5ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#a855f7">3</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Tillägg</h3>
</div>
<div id="batAddonCards" style="display:grid;gap:10px"></div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Batteri</span><span class="price-line-value" id="batPrBattery">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Tillägg</span><span class="price-line-value" id="batPrAddons">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="batPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setBatDeduction('green')" class="bat-deduct-btn" data-dt="green" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">Grönt teknik<br><span style="font-size:9px;font-weight:400;opacity:.8">20% av allt</span></button>
<button onclick="setBatDeduction('none')" class="bat-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="batDeductLabel" style="font-size:12px;font-weight:700;color:#059669">GRÖNT TEKNIK-AVDRAG</span>
<span id="batDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="batOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setBatOwners(1)" class="bat-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setBatOwners(2)" class="bat-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="batPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setBatFinYears(5)" class="bat-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setBatFinYears(10)" class="bat-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setBatFinYears(15)" class="bat-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="batFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="batPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('batteri')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('batteri')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- LADDBOX KONFIGURATOR -->
<div id="laddboxConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Välj laddbox -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj laddbox</h3>
</div>
<div id="lbChargerCards" style="display:grid;gap:10px"></div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Laddbox</span><span class="price-line-value" id="lbPrCharger">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="lbPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setLbDeduction('green')" class="lb-deduct-btn" data-dt="green" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">Grönt teknik<br><span style="font-size:9px;font-weight:400;opacity:.8">20% av allt</span></button>
<button onclick="setLbDeduction('none')" class="lb-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="lbDeductLabel" style="font-size:12px;font-weight:700;color:#059669">GRÖNT TEKNIK-AVDRAG</span>
<span id="lbDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="lbOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setLbOwners(1)" class="lb-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setLbOwners(2)" class="lb-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="lbPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setLbFinYears(5)" class="lb-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setLbFinYears(10)" class="lb-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setLbFinYears(15)" class="lb-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="lbFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="lbPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('laddbox')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('laddbox')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- TAKTVÄTT KONFIGURATOR -->
<div id="taktvatConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Typ + yta -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Typ av taktvätt</h3>
</div>
<div id="ttTypeCards" style="display:grid;gap:10px;margin-bottom:16px"></div>
<div style="margin-top:16px">
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Takyta (m²)</label>
<input type="number" id="ttArea" value="" placeholder="150" min="0" oninput="updateTtCalc()" style="width:200px;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">
</div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Taktvätt <span style="font-size:10px;color:#94a3b8" id="ttPrDesc"></span></span><span class="price-line-value" id="ttPrWash">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="ttPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setTtDeduction('rot')" class="tt-deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setTtDeduction('none')" class="tt-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="ttDeductLabel" style="font-size:12px;font-weight:700;color:#059669">ROT-AVDRAG</span>
<span id="ttDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="ttOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setTtOwners(1)" class="tt-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setTtOwners(2)" class="tt-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="ttPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setTtFinYears(5)" class="tt-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setTtFinYears(10)" class="tt-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setTtFinYears(15)" class="tt-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="ttFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="ttPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('taktvatt')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('taktvatt')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- VÄRMEPUMP KONFIGURATOR -->
<div id="varmepumpConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Välj värmepump -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj värmepump</h3>
</div>
<div id="vpPumpCards" style="display:grid;gap:10px"></div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Värmepump</span><span class="price-line-value" id="vpPrPump">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Installation</span><span class="price-line-value" id="vpPrInstall">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="vpPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setVpDeduction('green')" class="vp-deduct-btn" data-dt="green" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">Grönt teknik<br><span style="font-size:9px;font-weight:400;opacity:.8">20% av allt</span></button>
<button onclick="setVpDeduction('rot')" class="vp-deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setVpDeduction('none')" class="vp-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="vpDeductLabel" style="font-size:12px;font-weight:700;color:#059669">GRÖNT TEKNIK-AVDRAG</span>
<span id="vpDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="vpOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setVpOwners(1)" class="vp-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setVpOwners(2)" class="vp-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="vpPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setVpFinYears(5)" class="vp-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setVpFinYears(10)" class="vp-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setVpFinYears(15)" class="vp-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="vpFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="vpPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('varmepump')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('varmepump')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- TAK KONFIGURATOR -->
<div id="takConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Takytor -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Takytor</h3>
<button onclick="addTakYta()" style="margin-left:auto;padding:6px 14px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till takyta</button>
</div>
<div id="takYtorContainer" style="display:grid;gap:12px">
<div style="text-align:center;padding:30px;border:2px dashed #e5e7eb;border-radius:10px;color:#94a3b8">
<p style="font-size:14px;font-weight:600;margin:0 0 8px">Inga takytor tillagda ännu</p>
<button onclick="addTakYta()" style="padding:8px 16px;background:#f0f9ff;border:1.5px solid #3b82f6;border-radius:8px;color:#3b82f6;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till första takytan</button>
</div>
</div>
</div>
<!-- Steg 2: Tillbehör -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#3b82f6">2</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Tillbehör</h3>
</div>
<div id="takTillbehorCards" style="display:grid;gap:10px"></div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Takytor</span><span class="price-line-value" id="takPrYtor">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Tillbehör</span><span class="price-line-value" id="takPrTillb">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="takPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setTakDeduction('rot')" class="tak-deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setTakDeduction('none')" class="tak-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="takDeductLabel" style="font-size:12px;font-weight:700;color:#059669">ROT-AVDRAG</span>
<span id="takDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="takOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setTakOwners(1)" class="tak-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setTakOwners(2)" class="tak-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="takPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setTakFinYears(5)" class="tak-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setTakFinYears(10)" class="tak-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setTakFinYears(15)" class="tak-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="takFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="takPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('tak')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('tak')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- ISOLERING KONFIGURATOR -->
<div id="isoleringConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Isolering -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Isolering</h3>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Yta (m²)</label>
<input type="number" id="isoArea" value="" placeholder="120" min="0" oninput="updateIsoCalc()" style="width:100%;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Isoleringstyp</label>
<input type="text" id="isoType" value="" placeholder="Mineralull" oninput="updateIsoCalc()" style="width:100%;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Tjocklek (mm)</label>
<input type="number" id="isoThickness" value="" placeholder="200" min="0" oninput="updateIsoCalc()" style="width:100%;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Totalt pris (kr)</label>
<input type="number" id="isoPrice" value="" placeholder="35000" min="0" oninput="updateIsoCalc()" style="width:100%;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">
</div>
</div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Isolering <span style="font-size:10px;color:#94a3b8" id="isoPrDesc"></span></span><span class="price-line-value" id="isoPrMaterial">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="isoPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setIsoDeduction('rot')" class="iso-deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setIsoDeduction('none')" class="iso-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="isoDeductLabel" style="font-size:12px;font-weight:700;color:#059669">ROT-AVDRAG</span>
<span id="isoDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="isoOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setIsoOwners(1)" class="iso-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setIsoOwners(2)" class="iso-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="isoPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setIsoFinYears(5)" class="iso-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setIsoFinYears(10)" class="iso-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setIsoFinYears(15)" class="iso-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="isoFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="isoPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button onclick="saveCfgQuote('isolering')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">💾 Spara kalkyl</button>
<button onclick="goToCfgAffar('isolering')" class="cart-btn" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">📋 Lägg till order</button>
</div>
</div>
</div>
</div>
</div>
<!-- FÖNSTER-KONFIGURATOR -->
<div id="fonsterConfigView" style="display:none">
<div style="display:grid;grid-template-columns:1fr 380px;gap:24px;align-items:start">
<div>
<!-- Steg 1: Välj fönstertyp -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#ecfdf5;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#059669">1</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Välj fönstertyp</h3>
</div>
<div id="fkProductCards" style="display:grid;gap:10px"></div>
</div>
<!-- Steg 2: Konfigurera -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#eff6ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#3b82f6">2</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Fönsterpositioner</h3>
<div style="margin-left:auto;display:flex;gap:12px;font-size:12px;color:#64748b">
<span>Totalt: <strong id="fkTotalCount" style="color:#1a1a1a">0</strong> st</span>
</div>
</div>
<!-- Grundval (profil + fönsterfärg) -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px;padding:14px;background:#f8fafc;border-radius:10px">
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Profil (alla fönster)</label>
<select id="fkProfil" onchange="updateFkCalc()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;background:#fff">
<option value="Optima Trä">Optima Trä</option>
<option value="Optima Trä/Alu">Optima Trä/Alu</option>
<option value="PVC Ara">PVC Ara</option>
</select>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Färg insida (fönster)</label>
<select id="fkFargIn" onchange="updateFkCalc()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;background:#fff">
<option value="Vit (RAL9010)" selected>Vit (RAL9010)</option>
<option value="Ljusgrå">Ljusgrå</option>
<option value="Svart">Svart</option>
<option value="Oljad ek">Oljad ek</option>
</select>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Färg utsida (fönster)</label>
<select id="fkFargUt" onchange="updateFkCalc()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;background:#fff">
<option value="Vit (RAL9010)" selected>Vit (RAL9010)</option>
<option value="Ljusgrå">Ljusgrå</option>
<option value="Svart">Svart</option>
<option value="Faluröd">Faluröd</option>
</select>
</div>
</div>
<!-- Positionstabell -->
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr style="background:#f1f5f9">
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:35px;text-align:center">Pos</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:100px">Rum</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:140px">Produkt</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:70px">Bredd</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:70px">Höjd</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:50px">Antal</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:120px">Glas</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:120px">Notering</th>
<th style="padding:8px 6px;border:1px solid #e2e8f0;width:30px"></th>
</tr></thead>
<tbody id="fkPosBody"></tbody>
</table>
</div>
<button type="button" onclick="addFkPos()" style="margin-top:8px;padding:8px 20px;border-radius:8px;border:1.5px solid #3b82f6;background:#f0f9ff;font-size:12px;font-weight:600;cursor:pointer;color:#3b82f6;font-family:inherit">+ Lägg till position</button>
</div>
<!-- Steg 3: Foder & Smyg -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#faf5ff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#a855f7">3</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Foder, Smyg & Färg</h3>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<!-- Foder -->
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Foder</label>
<div id="fkFoderCards" style="display:grid;gap:6px">
<label onclick="setFkFoder('gerad')" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border:2px solid #a855f7;border-radius:8px;cursor:pointer;background:#faf5ff">
<input type="radio" name="fkFoder" value="gerad" checked style="accent-color:#a855f7">
<div><div style="font-weight:600;font-size:13px">Gerad (kant i kant)</div><div style="font-size:11px;color:#64748b">45° gering, list möter list</div></div>
</label>
<label onclick="setFkFoder('kloss')" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff">
<input type="radio" name="fkFoder" value="kloss" style="accent-color:#a855f7">
<div><div style="font-weight:600;font-size:13px">Med kloss</div><div style="font-size:11px;color:#64748b">Rak kap med hörnkloss</div></div>
</label>
<label onclick="setFkFoder('ingen')" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff">
<input type="radio" name="fkFoder" value="ingen" style="accent-color:#a855f7">
<div><div style="font-weight:600;font-size:13px">Utan foder</div><div style="font-size:11px;color:#64748b">Inget foder</div></div>
</label>
</div>
</div>
<!-- Smyg -->
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Smyg</label>
<div id="fkSmygCards" style="display:grid;gap:6px">
<label onclick="setFkSmyg('ja')" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border:2px solid #a855f7;border-radius:8px;cursor:pointer;background:#faf5ff">
<input type="radio" name="fkSmyg" value="ja" checked style="accent-color:#a855f7">
<div><div style="font-weight:600;font-size:13px">Med smyg</div><div style="font-size:11px;color:#64748b">Smygbräda runt fönstret</div></div>
</label>
<label onclick="setFkSmyg('nej')" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff">
<input type="radio" name="fkSmyg" value="nej" style="accent-color:#a855f7">
<div><div style="font-weight:600;font-size:13px">Utan smyg</div><div style="font-size:11px;color:#64748b">Ingen smygbräda</div></div>
</label>
</div>
</div>
<!-- Färg insida (foder/smyg) -->
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Färg foder/smyg insida</label>
<select id="fkFoderFargIn" onchange="updateFkCalc()" style="width:100%;padding:9px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="Vit (RAL9010)" selected>Vit (RAL9010)</option>
<option value="Ljusgrå">Ljusgrå</option>
<option value="Svart">Svart</option>
<option value="Oljad ek">Oljad ek</option>
<option value="Laserad furu">Laserad furu</option>
<option value="Faluröd">Faluröd</option>
</select>
</div>
<!-- Färg utsida (foder/smyg) -->
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Färg foder/smyg utsida</label>
<select id="fkFoderFargUt" onchange="updateFkCalc()" style="width:100%;padding:9px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="Vit (RAL9010)" selected>Vit (RAL9010)</option>
<option value="Ljusgrå">Ljusgrå</option>
<option value="Svart">Svart</option>
<option value="Faluröd">Faluröd</option>
<option value="Laserad furu">Laserad furu</option>
</select>
</div>
<!-- Fönsterbänk -->
<div style="grid-column:1/-1">
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Fönsterbänk</label>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #a855f7;border-radius:8px;cursor:pointer;background:#faf5ff;font-size:13px">
<input type="radio" name="fkBank" value="tra" checked onchange="updateFkCalc()" style="accent-color:#a855f7"> Trä
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff;font-size:13px">
<input type="radio" name="fkBank" value="granit" onchange="updateFkCalc()" style="accent-color:#a855f7"> Granit
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff;font-size:13px">
<input type="radio" name="fkBank" value="marmor" onchange="updateFkCalc()" style="accent-color:#a855f7"> Marmor
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;background:#fff;font-size:13px">
<input type="radio" name="fkBank" value="ingen" onchange="updateFkCalc()" style="accent-color:#a855f7"> Utan
</label>
</div>
</div>
</div>
</div>
<!-- Steg 4: Tillbehör -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#fef3c7;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#d97706">4</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Tillbehör</h3>
</div>
<div id="fkTillbCatTabs" style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:16px"></div>
<div style="display:grid;grid-template-columns:180px 1fr;gap:16px;min-height:200px">
<div id="fkTillbCatMenu" style="display:flex;flex-direction:column;gap:2px"></div>
<div id="fkTillbProducts" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;align-content:start"></div>
</div>
<div id="fkTillbSelected" style="margin-top:16px;display:none">
<div style="font-size:12px;font-weight:600;color:#64748b;margin-bottom:8px">Valda tillbehör:</div>
<div id="fkTillbSelectedList" style="display:grid;gap:6px"></div>
</div>
</div>
<!-- Steg 5: Montering & Frakt -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
<div style="width:32px;height:32px;background:#0ea5e9;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff">5</div>
<h3 style="font-size:16px;font-weight:700;color:#1a1a1a;margin:0">Montering & Frakt</h3>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Montering (timmar)</label>
<input type="number" id="fkMontTimmar" value="0" min="0" oninput="updateFkCalc()" style="width:100%;padding:7px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box">
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Timpris (kr)</label>
<input type="number" id="fkMontPris" value="650" oninput="updateFkCalc()" style="width:100%;padding:7px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box">
</div>
<div>
<label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Frakt (kr)</label>
<input type="number" id="fkFrakt" value="0" min="0" oninput="updateFkCalc()" style="width:100%;padding:7px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box">
</div>
</div>
</div>
</div>
<!-- PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header">Prissammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Fönster <span style="font-size:10px;color:#94a3b8" id="fkPrDesc"></span></span><span class="price-line-value" id="fkPrFonster">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Tillbehör</span><span class="price-line-value" id="fkPrTillbehor">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Montering</span><span class="price-line-value" id="fkPrMontering">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Frakt</span><span class="price-line-value" id="fkPrFrakt">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="fkPrSubtotal">0 kr</span></div>
</div>
<div class="rot-section" style="background:#ecfdf5;padding:14px 16px">
<div style="margin-bottom:10px">
<div style="font-size:11px;color:#6b7280;margin-bottom:6px;font-weight:600">SKATTEAVDRAG</div>
<div style="display:flex;gap:4px">
<button onclick="setFkDeduction('rot')" class="fk-deduct-btn" data-dt="rot" style="flex:1;padding:7px 4px;border:1.5px solid #059669;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit;line-height:1.2">ROT-avdrag<br><span style="font-size:9px;font-weight:400;opacity:.8">30% av arbete</span></button>
<button onclick="setFkDeduction('green')" class="fk-deduct-btn" data-dt="green" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Grönt teknik<br><span style="font-size:9px;font-weight:400;opacity:.8">20% av allt</span></button>
<button onclick="setFkDeduction('none')" class="fk-deduct-btn" data-dt="none" style="flex:1;padding:7px 4px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit;line-height:1.2">Inget<br><span style="font-size:9px;font-weight:400;opacity:.8"> </span></button>
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="fkDeductLabel" style="font-size:12px;font-weight:700;color:#059669">ROT-AVDRAG</span>
<span id="fkDeductKr" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
<div id="fkOwnerSection" style="margin-top:8px">
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">Antal ägare</div>
<div style="display:flex;gap:6px">
<button onclick="setFkOwners(1)" class="fk-owner-btn" data-oc="1" style="flex:1;padding:6px;border:1.5px solid #059669;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#059669;color:#fff;font-family:inherit">1 person</button>
<button onclick="setFkOwners(2)" class="fk-owner-btn" data-oc="2" style="flex:1;padding:6px;border:1.5px solid #bbf7d0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;color:#059669;font-family:inherit">2 personer</button>
</div>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Att betala</span><span class="total-value" id="fkPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#faf5ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#a855f7;font-weight:600;margin-bottom:8px">MARGINAL</div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<input type="range" id="fkMarginSlider" min="0" max="40" step="1" value="20" oninput="updateFkCalc()" style="flex:1;accent-color:#a855f7">
<span id="fkMarginPct" style="font-size:18px;font-weight:700;color:#7c3aed;min-width:40px;text-align:right">20%</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:12px">
<span style="color:#6b7280">Marginal</span>
<span id="fkMarginKr" style="font-weight:700;color:#7c3aed">0 kr</span>
</div>
</div>
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:8px">FINANSIERING</div>
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
<button onclick="setFkFinYears(5)" class="fk-fin-btn" data-yr="5" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">5 år</button>
<button onclick="setFkFinYears(10)" class="fk-fin-btn" data-yr="10" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">10 år</button>
<button onclick="setFkFinYears(15)" class="fk-fin-btn" data-yr="15" style="padding:4px 10px;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#3b82f6;color:#fff;font-family:inherit">15 år</button>
<button onclick="setFkFinYears(20)" class="fk-fin-btn" data-yr="20" style="padding:4px 10px;border:1.5px solid #bfdbfe;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;background:#fff;color:#3b82f6;font-family:inherit">20 år</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad <span id="fkFinInfo" style="font-size:10px;color:#64748b">(15 år, 4.9%)</span></span>
<span id="fkPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<button id="fkSaveBtn" onclick="saveFkQuote('utkast')" style="width:100%;padding:12px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:8px">
<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Spara utkast
</button>
<button class="cart-btn" onclick="goToFkAffar()" style="width:100%;padding:14px;background:linear-gradient(135deg,#024550,#035e6b);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2M9 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2M9 5h6"/><path d="M9 12h6M9 16h6"/></svg>
Lägg till i affär
</button>
</div>
</div>
</div>
</div>
</div>
<!-- AFFÄR-VY (efter kalkyl) -->
<div id="affarView" style="display:none">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<button class="dummy-btn secondary" onclick="backToKalkylFromAffar()" style="padding:8px 14px"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> Tillbaka till kalkyl</button>
<div><h1 class="page-title" style="margin-bottom:0">Affär</h1></div>
<div id="affarStatus" style="margin-left:auto;padding:4px 12px;border-radius:20px;font-size:12px;font-weight:600;background:#fef3c7;color:#92400e">Utkast</div>
</div>
<!-- Kundbanner i affär -->
<div id="affarCustomerBanner" style="margin-bottom:16px"></div>
<div style="display:grid;grid-template-columns:1fr 400px;gap:24px;align-items:start">
<div>
<!-- Sammanfattning från kalkyl -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:700;margin:0 0 16px;display:flex;align-items:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:none;stroke:#059669;stroke-width:2"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg>
Konfiguration
</h3>
<div id="affarConfigSummary" style="display:grid;gap:8px"></div>
</div>
<!-- Extrakostnader -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:700;margin:0;display:flex;align-items:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:none;stroke:#3b82f6;stroke-width:2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
Extrakostnader
</h3>
<button onclick="addExtraCost()" style="padding:6px 14px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:4px">
<svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Lägg till
</button>
</div>
<div id="affarExtraCosts"></div>
<div id="affarExtraCostsEmpty" style="padding:16px;text-align:center;color:#94a3b8;font-size:13px">
Inga extrakostnader tillagda. Klicka "Lägg till" för takunderhåll, extra kabelarbete etc.
</div>
</div>
<!-- Marginal -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:700;margin:0 0 16px;display:flex;align-items:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:none;stroke:#a855f7;stroke-width:2"><path d="M16 8v8M12 11v5M8 14v2M3 3v18h18"/></svg>
Marginal
</h3>
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px">
<input type="range" id="affarMarginSlider" min="0" max="40" step="1" value="0" oninput="updateAffarCalc()" style="flex:1;accent-color:#a855f7">
<div style="background:#faf5ff;border:2px solid #a855f7;border-radius:10px;padding:8px 16px;text-align:center;min-width:80px">
<div id="affarMarginVal" style="font-size:24px;font-weight:700;color:#7c3aed">0%</div>
<div style="font-size:10px;color:#64748b;font-weight:600">MARGINAL</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div style="background:#f8fafc;padding:10px;border-radius:8px;text-align:center">
<div style="font-size:10px;color:#64748b;font-weight:600">MARGINALKOSTNAD</div>
<div id="affarMarginKr" style="font-size:16px;font-weight:700;color:#1a1a1a">0 kr</div>
</div>
<div style="background:#f8fafc;padding:10px;border-radius:8px;text-align:center">
<div style="font-size:10px;color:#64748b;font-weight:600">VINST</div>
<div id="affarProfit" style="font-size:16px;font-weight:700;color:#059669">0 kr</div>
</div>
</div>
</div>
<!-- Bilder för offert -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:700;margin:0;display:flex;align-items:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:20px;height:20px;fill:none;stroke:#f59e0b;stroke-width:2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
Bilder för offert
</h3>
<div style="display:flex;gap:6px">
<button onclick="addProspectPhotosToAffar()" id="affarAddProspectBtn" style="padding:6px 14px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:4px">
<svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
Hämta prospektbilder
</button>
</div>
</div>
<div id="affarImages" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px"></div>
<div id="affarImagesEmpty" style="padding:20px;text-align:center;color:#94a3b8;font-size:13px;border:2px dashed #e5e7eb;border-radius:10px">
Inga bilder ännu. Hämta prospektbilder eller skapa en prospektbild för offerten.
</div>
</div>
<!-- Anteckningar -->
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:14px;padding:24px;margin-bottom:16px">
<h3 style="font-size:16px;font-weight:700;margin:0 0 12px">Anteckningar</h3>
<textarea id="affarNotes" rows="3" placeholder="Interna anteckningar om affären..." style="width:100%;padding:12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical"></textarea>
</div>
</div>
<!-- AFFÄR PRISSIDEBAR -->
<div style="position:sticky;top:20px">
<div class="price-card">
<div class="price-header" style="background:linear-gradient(135deg,#024550,#035e6b)">Affärssammanställning</div>
<div class="price-rows">
<div class="price-line"><span class="price-line-label">Material</span><span class="price-line-value" id="affarPrMaterial">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Installation</span><span class="price-line-value" id="affarPrLabor">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Batteri</span><span class="price-line-value" id="affarPrBattery">0 kr</span></div>
<div class="price-line"><span class="price-line-label">Laddare</span><span class="price-line-value" id="affarPrCharger">0 kr</span></div>
<div class="price-line" id="affarPrExtraRow" style="display:none"><span class="price-line-label">Extrakostnader</span><span class="price-line-value" id="affarPrExtra">0 kr</span></div>
<div class="price-line" id="affarPrMarginRow" style="display:none"><span class="price-line-label">Marginal <span id="affarPrMarginPct" style="font-size:10px;color:#94a3b8"></span></span><span class="price-line-value" id="affarPrMargin">0 kr</span></div>
<div class="price-line subtotal"><span class="price-line-label">Totalt före avdrag</span><span class="price-line-value" id="affarPrSubtotal">0 kr</span></div>
</div>
<div id="affarDeductionRow" style="background:#ecfdf5;padding:14px 16px">
<div style="display:flex;justify-content:space-between;align-items:center">
<span id="affarDeductLabel" style="font-size:12px;font-weight:700;color:#059669">AVDRAG</span>
<span id="affarPrGreenTech" style="font-size:16px;font-weight:700;color:#059669">0 kr</span>
</div>
</div>
<div class="total-section">
<div class="total-line"><span class="total-label">Kunden betalar</span><span class="total-value" id="affarPrTotal">0 kr</span></div>
<div class="total-vat">Inkl. moms</div>
</div>
<div style="padding:0 16px 16px">
<div style="background:#f0f9ff;border-radius:10px;padding:14px;margin-bottom:12px">
<div style="font-size:11px;color:#3b82f6;font-weight:600;margin-bottom:4px">FINANSIERING</div>
<div style="display:flex;justify-content:space-between;align-items:baseline">
<span style="font-size:13px;color:#334155">Månadskostnad</span>
<span id="affarPrMonthly" style="font-size:20px;font-weight:700;color:#1e40af">0 kr/mån</span>
</div>
</div>
<div style="display:grid;gap:8px">
<button onclick="saveQuoteAsDraft()" style="width:100%;padding:14px;background:#f59e0b;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Spara som utkast
</button>
<button onclick="createFinalQuote()" style="width:100%;padding:14px;background:#166534;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Skapa offert
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- KALENDER -->
<div class="page-content" id="page-kalender">
<h1 class="page-title">Kalender</h1>
<p class="page-subtitle">Din Google Kalender — bädda in genom att ange din e-post</p>
<div id="calSetup" style="max-width:500px">
<div style="display:flex;gap:8px;margin-bottom:16px">
<input type="email" id="calEmail" placeholder="din.email@gmail.com" value="" style="flex:1;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">
<button onclick="loadGoogleCalendar()" style="padding:10px 20px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;white-space:nowrap">Ladda kalender</button>
</div>
<p style="font-size:12px;color:#94a3b8">Obs: Din Google Kalender måste vara publik eller så måste du vara inloggad i Google i samma webbläsare.</p>
</div>
<div id="calEmbed" style="display:none;margin-top:16px;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb">
<iframe id="calIframe" src="" style="width:100%;height:700px;border:none"></iframe>
</div>
<div style="margin-top:20px">
<h3 style="font-size:16px;font-weight:600;color:#1a1a1a;margin-bottom:14px">Återbesök att boka</h3>
<div id="callbackList" style="display:flex;flex-direction:column;gap:8px">
<div style="padding:16px;background:#f8fafc;border-radius:10px;color:#94a3b8;text-align:center;font-size:13px">Inga planerade återbesök</div>
</div>
</div>
</div>
<!-- INKORG -->
<div class="page-content" id="page-inkorg">
<h1 class="page-title">Inkorg</h1>
<p class="page-subtitle">Din e-post direkt i CRM:et</p>
<!-- Mail Login -->
<div id="mailLogin" style="max-width:500px">
<div style="padding:28px;background:#fff;border-radius:12px;border:1px solid #e5e7eb">
<h3 style="font-size:16px;font-weight:600;margin-bottom:16px">Anslut din e-post</h3>
<div style="display:grid;gap:10px">
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">E-postadress</label><input type="email" id="mailEmail" placeholder="dittnamn@solargroup.se" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Lösenord</label><input type="password" id="mailPass" placeholder="E-postlösenord" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">IMAP-server</label><input type="text" id="mailImapHost" value="localhost" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Port</label><input type="number" id="mailImapPort" value="993" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">SMTP-server</label><input type="text" id="mailSmtpHost" value="localhost" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Port</label><input type="number" id="mailSmtpPort" value="587" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
</div>
<div id="mailLoginError" style="display:none;padding:8px 12px;background:#fef2f2;border-radius:6px;color:#991b1b;font-size:13px"></div>
<button onclick="mailConnect()" id="mailConnectBtn" style="width:100%;padding:12px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:4px">Anslut</button>
</div>
<p style="font-size:11px;color:#94a3b8;margin-top:12px">Dina uppgifter sparas lokalt i webbläsaren och skickas säkert till servern vid varje anrop.</p>
</div>
</div>
<!-- Mail Client -->
<div id="mailClient" style="display:none">
<!-- Toolbar -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">
<div style="display:flex;gap:8px;align-items:center">
<select id="mailFolderSelect" onchange="mailSwitchFolder()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit"></select>
<button onclick="mailRefresh()" style="padding:8px 14px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">Uppdatera</button>
<span id="mailCount" style="font-size:12px;color:#94a3b8"></span>
</div>
<div style="display:flex;gap:8px;align-items:center">
<input type="text" id="mailSearch" placeholder="Sök..." onkeydown="if(event.key==='Enter')mailRefresh()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;width:200px">
<button onclick="mailCompose()" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Skriv nytt</button>
<button onclick="mailDisconnect()" style="padding:8px 12px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit;color:#ef4444" title="Logga ut">Logga ut</button>
</div>
</div>
<!-- Mail layout: list + reader -->
<div style="display:grid;grid-template-columns:380px 1fr;gap:0;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;background:#fff;min-height:600px">
<!-- Message list -->
<div style="border-right:1px solid #e5e7eb;overflow-y:auto;max-height:700px" id="mailListContainer">
<div id="mailList" style="display:flex;flex-direction:column"></div>
<div id="mailListLoading" style="display:none;padding:20px;text-align:center;color:#94a3b8;font-size:13px">Laddar...</div>
<div id="mailPagination" style="display:flex;justify-content:center;gap:8px;padding:12px;border-top:1px solid #f1f5f9"></div>
</div>
<!-- Reader pane -->
<div style="overflow-y:auto;max-height:700px" id="mailReaderContainer">
<div id="mailReader" style="padding:24px">
<div id="mailReaderEmpty" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:400px;color:#94a3b8">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
<p style="margin-top:12px;font-size:14px">Välj ett meddelande att läsa</p>
</div>
<div id="mailReaderContent" style="display:none"></div>
</div>
</div>
</div>
</div>
<!-- Compose modal -->
<div id="mailComposeModal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.5);align-items:center;justify-content:center">
<div style="background:#fff;border-radius:16px;padding:0;width:640px;max-width:95vw;max-height:90vh;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2);display:flex;flex-direction:column">
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e5e7eb">
<h3 style="font-size:16px;font-weight:600" id="composeTitle">Nytt meddelande</h3>
<button onclick="mailCloseCompose()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">×</button>
</div>
<div style="padding:16px 20px;overflow-y:auto;flex:1;display:grid;gap:8px">
<div style="display:flex;align-items:center;gap:8px"><label style="font-size:12px;font-weight:600;color:#334155;width:40px">Till</label><input type="text" id="composeTo" style="flex:1;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div style="display:flex;align-items:center;gap:8px"><label style="font-size:12px;font-weight:600;color:#334155;width:40px">Cc</label><input type="text" id="composeCc" style="flex:1;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<div style="display:flex;align-items:center;gap:8px"><label style="font-size:12px;font-weight:600;color:#334155;width:40px">Ämne</label><input type="text" id="composeSubject" style="flex:1;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>
<input type="hidden" id="composeReplyUid" value="0">
<input type="hidden" id="composeReplyFolder" value="">
<input type="hidden" id="composeReplyTo" value="">
<textarea id="composeBody" rows="14" placeholder="Skriv ditt meddelande..." style="width:100%;padding:12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical;min-height:200px"></textarea>
<div style="padding:8px 0">
<label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Signatur</label>
<textarea id="composeSignature" rows="4" placeholder="-- Med vänliga hälsningar Ditt namn Solargroup AB" style="width:100%;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;background:#f8fafc;color:#64748b"></textarea>
</div>
</div>
<div style="padding:12px 20px;border-top:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center">
<button onclick="mailCloseCompose()" style="padding:10px 20px;background:#f1f5f9;color:#334155;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;cursor:pointer;font-family:inherit">Avbryt</button>
<button onclick="mailSend()" id="composeSendBtn" style="padding:10px 24px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Skicka</button>
</div>
</div>
</div>
</div>
<!-- DAGRAPPORT -->
<div class="page-content" id="page-dagrapport">
<h1 class="page-title">Dagrapport</h1>
<p class="page-subtitle">Rapportera dina dagliga aktiviteter</p>
<div class="dr-summary">
<div class="dr-card"><h4>Kundbesök idag</h4><div class="dr-val">3</div><div class="dr-sub">Av 4 planerade</div></div>
<div class="dr-card"><h4>Offerter skapade</h4><div class="dr-val">2</div><div class="dr-sub">Totalt 87 500 kr</div></div>
<div class="dr-card"><h4>Körsträcka</h4><div class="dr-val">145 km</div><div class="dr-sub">18,50 kr/km</div></div>
</div>
<div class="dr-form">
<h3>Ny dagrapport</h3>
<div class="dr-form-grid">
<div class="dr-form-group"><label>Datum</label><input type="date" value="2026-02-18"></div>
<div class="dr-form-group"><label>Typ av aktivitet</label><select><option>Kundbesök</option><option>Inmätning</option><option>Montering</option><option>Offertarbete</option><option>Intern</option><option>Utbildning</option></select></div>
<div class="dr-form-group"><label>Kund</label><select><option value="">Välj kund...</option><option>Andersson Bygg AB</option><option>Svensson Fastigheter</option><option>Johansson & Co</option><option>Nilsson Renovering</option><option>Karlsson Entreprenad</option><option>Berg Fastigheter AB</option></select></div>
<div class="dr-form-group"><label>Körsträcka (km)</label><input type="number" placeholder="0"></div>
<div class="dr-form-group"><label>Starttid</label><input type="time" value="08:00"></div>
<div class="dr-form-group"><label>Sluttid</label><input type="time" value="17:00"></div>
<div class="dr-form-group full"><label>Kommentar</label><textarea placeholder="Beskriv dagens aktiviteter..."></textarea></div>
</div>
<button class="dummy-btn" style="margin-top:16px">Spara rapport</button>
</div>
<div>
<h3 style="font-size:16px;font-weight:600;color:#1a1a1a;margin-bottom:14px">Senaste rapporter</h3>
<div class="dr-log-item">
<div class="dr-log-date">17 feb</div>
<div class="dr-log-info"><h4>Kundbesök - Svensson Fastigheter</h4><p>Offertpresentation villa Kungälv. Kunden nöjd, vill ha komplettering med balkongdörr. 87 km.</p></div>
<span class="dr-log-tag" style="background:#dcfce7;color:#166534">Godkänd</span>
</div>
<div class="dr-log-item">
<div class="dr-log-date">14 feb</div>
<div class="dr-log-info"><h4>Inmätning - Andersson Bygg AB</h4><p>Inmätning av 12 fönster + 2 balkongdörrar. Kungsholmen 14. Allt stämde med offert. 23 km.</p></div>
<span class="dr-log-tag" style="background:#dcfce7;color:#166534">Godkänd</span>
</div>
<div class="dr-log-item">
<div class="dr-log-date">13 feb</div>
<div class="dr-log-info"><h4>Montering - Nilsson Renovering</h4><p>Monterade 4 av 6 fönster. Återstående 2 kräver specialbeslag. Beställt. 156 km.</p></div>
<span class="dr-log-tag" style="background:#fef9c3;color:#854d0e">Pågår</span>
</div>
<div class="dr-log-item">
<div class="dr-log-date">12 feb</div>
<div class="dr-log-info"><h4>Offertarbete - Berg Fastigheter</h4><p>Skapade offert BRF renovering 32 fönster. Skickade för granskning. Kontoret.</p></div>
<span class="dr-log-tag" style="background:#dcfce7;color:#166534">Godkänd</span>
</div>
</div>
</div>
<!-- MIN LÖN -->
<div class="page-content" id="page-minlon">
<h1 class="page-title">Min Lön</h1>
<p class="page-subtitle">Lönespecifikation och provisioner</p>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<span style="font-size:14px;font-weight:500;color:#64748b">Period:</span>
<select class="lon-month-select">
<option>Februari 2026</option>
<option>Januari 2026</option>
<option>December 2025</option>
<option>November 2025</option>
</select>
</div>
<div class="lon-overview">
<div class="lon-card highlight"><h4>Nettolön</h4><div class="lon-val">34 280 kr</div><div class="lon-sub">Betalas ut 25 feb</div></div>
<div class="lon-card"><h4>Bruttolön</h4><div class="lon-val">48 500 kr</div><div class="lon-sub">Grundlön + provision</div></div>
<div class="lon-card"><h4>Provision denna månad</h4><div class="lon-val">12 500 kr</div><div class="lon-sub">5 stängda affärer</div></div>
</div>
<div class="lon-breakdown">
<div class="lon-breakdown-header">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:#64748b;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Lönespecifikation
</div>
<div class="lon-row"><span class="lon-row-label">Grundlön</span><span class="lon-row-val">36 000 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Provision (5 affärer)</span><span class="lon-row-val">12 500 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Milersättning (823 km)</span><span class="lon-row-val">15 226 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Traktamente (3 dagar)</span><span class="lon-row-val">1 080 kr</span></div>
<div class="lon-row" style="border-top:2px solid #e5e7eb"><span class="lon-row-label" style="font-weight:600">Bruttolön</span><span class="lon-row-val">48 500 kr</span></div>
<div class="lon-row deduction"><span class="lon-row-label">Skatt (30%)</span><span class="lon-row-val">-14 220 kr</span></div>
<div class="lon-row total"><span class="lon-row-label">Nettolön</span><span class="lon-row-val">34 280 kr</span></div>
</div>
<div class="lon-breakdown">
<div class="lon-breakdown-header">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:#64748b;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>
Provisionhistorik
</div>
<div class="lon-row"><span class="lon-row-label">Andersson Bygg - 12 fönster + 2 dörrar</span><span class="lon-row-val" style="color:#10b981">3 200 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Nilsson Renovering - 6 fönster + dörr</span><span class="lon-row-val" style="color:#10b981">2 800 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Johansson & Co - Kontorsfönster 24 st</span><span class="lon-row-val" style="color:#10b981">3 500 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Pettersson & Söner - 8 fönster</span><span class="lon-row-val" style="color:#10b981">1 800 kr</span></div>
<div class="lon-row"><span class="lon-row-label">Gustafsson Bygg - 6 fönster</span><span class="lon-row-val" style="color:#10b981">1 200 kr</span></div>
</div>
</div>
<!-- ADMIN -->
<div class="page-content" id="page-admin">
<h1 class="page-title">Admin</h1>
<p class="page-subtitle">Användarhantering och roller</p>
<div style="display:flex;gap:0;margin-bottom:24px;border-bottom:2px solid #e5e7eb">
<button class="admin-tab active" data-admin-tab="users" onclick="switchAdminTab('users')" style="padding:10px 20px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid #024550;color:#024550;margin-bottom:-2px;font-family:inherit">Användare</button>
<button class="admin-tab" data-admin-tab="pending" onclick="switchAdminTab('pending')" style="padding:10px 20px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Väntande <span id="adminPendingCount" style="background:#ef4444;color:#fff;border-radius:10px;padding:1px 7px;font-size:10px;font-weight:700;margin-left:4px;display:none"></span></button>
<button class="admin-tab" data-admin-tab="permissions" onclick="switchAdminTab('permissions')" style="padding:10px 20px;font-size:14px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Behörighet</button>
</div>
<!-- Användare-tab -->
<div id="adminUsersPanel">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;gap:12px;flex-wrap:wrap">
<div style="display:flex;gap:10px;align-items:center;flex:1;min-width:200px">
<input type="text" id="adminUserSearch" oninput="filterAdminUsers()" placeholder="Sök namn eller e-post..." autocomplete="off" style="padding:8px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;width:100%;max-width:280px;transition:border-color .2s" onfocus="this.style.borderColor='#024550'" onblur="this.style.borderColor='#e5e7eb'">
<select id="adminUserRoleFilter" onchange="filterAdminUsers()" style="padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff;cursor:pointer">
<option value="">Alla roller</option>
<option value="admin">Admin</option>
<option value="saljchef">Säljchef</option>
<option value="saljare">Säljare</option>
<option value="installator">Installatör</option>
<option value="projektledare">Projektledare</option>
<option value="ekonomi">Ekonomi</option>
</select>
</div>
<div style="font-size:13px;color:#64748b" id="adminUserCount"></div>
</div>
<div style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead>
<tr style="background:#f8fafc;border-bottom:2px solid #e5e7eb">
<th style="text-align:left;padding:10px 12px;font-weight:600;color:#64748b">Namn</th>
<th style="text-align:left;padding:10px 12px;font-weight:600;color:#64748b">E-post</th>
<th style="text-align:left;padding:10px 12px;font-weight:600;color:#64748b">Roll</th>
<th style="text-align:left;padding:10px 12px;font-weight:600;color:#64748b">Telefon</th>
<th style="text-align:center;padding:10px 12px;font-weight:600;color:#64748b">Aktiv</th>
<th style="text-align:left;padding:10px 12px;font-weight:600;color:#64748b">Senaste inlogg</th>
<th style="text-align:center;padding:10px 12px;font-weight:600;color:#64748b"></th>
</tr>
</thead>
<tbody id="adminUsersBody"></tbody>
</table>
</div>
</div>
<!-- Väntande-tab -->
<div id="adminPendingPanel" style="display:none">
<div id="adminPendingEmpty" style="text-align:center;padding:40px;color:#94a3b8;display:none">
<svg viewBox="0 0 24 24" style="width:48px;height:48px;stroke:#cbd5e1;fill:none;stroke-width:1.2;margin-bottom:12px"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<p style="font-size:15px;font-weight:600;margin-bottom:4px">Inga väntande</p>
<p style="font-size:13px">Alla ansökningar är behandlade</p>
</div>
<div id="adminPendingList" style="display:flex;flex-direction:column;gap:12px"></div>
</div>
<!-- Behörighet-tab --> <div id="adminPermissionsPanel" style="display:none"> <div style="margin-bottom:16px"> <p style="font-size:13px;color:#64748b;margin-bottom:16px">Välj vilka menyer varje roll ska se. Ändringar sparas direkt.</p> <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:20px" id="permRoleTabs"></div> </div> <div id="permMatrixContainer" style="overflow-x:auto"></div> </div>
</div>
<!-- EKONOMI / EVA AI -->
<div class="page-content" id="page-ekonomi">
<h1 class="page-title">Ekonomi</h1>
<p class="page-subtitle">Leveranser, fakturor, kundreskontra och utlägg</p>
<div class="eko-layout">
<div class="eko-main">
<div class="eko-tabs">
<button class="eko-tab active" onclick="ekoTab(this,'eko-leverans')">Leveranser</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-order')">Order</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-faktura')">Fakturor</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-plock')">Montering</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-reskontra')">Kundreskontra</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-utlagg')">Utlägg</button>
<button class="eko-tab" onclick="ekoTab(this,'eko-inkop')">Inköp</button>
</div>
<!-- LEVERANSER -->
<div class="eko-panel active" id="eko-leverans">
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:20px;text-align:center">
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Offert</h4><div class="dr-val" style="font-size:18px">2</div></div>
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Order</h4><div class="dr-val" style="font-size:18px">4</div></div>
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Produktion</h4><div class="dr-val" style="font-size:18px">2</div></div>
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Leverans</h4><div class="dr-val" style="font-size:18px">1</div></div>
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Montering</h4><div class="dr-val" style="font-size:18px">3</div></div>
<div class="dr-card" style="padding:10px 8px"><h4 style="font-size:10px">Klart</h4><div class="dr-val" style="font-size:18px;color:#10b981">1</div></div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:16px 20px;display:flex;align-items:center;gap:16px">
<div style="width:10px;height:10px;border-radius:50%;background:#10b981;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Andersson Bygg AB</div>
<div style="font-size:12px;color:#64748b">12 fönster + 2 balkongdörrar • PRJ-2024-001</div>
</div>
<div style="text-align:right">
<span class="status-badge green">Levererad</span>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Montering 100% • Besiktning godkänd</div>
</div>
</div>
<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:16px 20px;display:flex;align-items:center;gap:16px">
<div style="width:10px;height:10px;border-radius:50%;background:#3b82f6;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Svensson Fastigheter</div>
<div style="font-size:12px;color:#64748b">8 fönster villa • PRJ-2024-002</div>
</div>
<div style="text-align:right">
<span class="status-badge blue">Under produktion</span>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Montering 75% klar • Väntar besiktning</div>
</div>
<div style="min-width:80px"><div class="progress-bar" style="height:16px"><div class="progress-fill" style="width:75%"></div><span class="progress-text" style="font-size:10px">75%</span></div></div>
</div>
<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:16px 20px;display:flex;align-items:center;gap:16px">
<div style="width:10px;height:10px;border-radius:50%;background:#eab308;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Johansson & Co</div>
<div style="font-size:12px;color:#64748b">Kontorsfönster 24 st • PRJ-2024-003</div>
</div>
<div style="text-align:right">
<span class="status-badge yellow">Väntar godkännande</span>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Montering 30% klar</div>
</div>
<div style="min-width:80px"><div class="progress-bar" style="height:16px"><div class="progress-fill" style="width:30%"></div><span class="progress-text" style="font-size:10px">30%</span></div></div>
</div>
<div style="background:#fff;border-radius:10px;border:2px solid #ef4444;padding:16px 20px;display:flex;align-items:center;gap:16px">
<div style="width:10px;height:10px;border-radius:50%;background:#ef4444;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Nilsson Renovering</div>
<div style="font-size:12px;color:#64748b">6 fönster + entredörr • PRJ-2024-004</div>
</div>
<div style="text-align:right">
<span class="status-badge" style="background:#fee2e2;color:#991b1b">Försenad</span>
<div style="font-size:11px;color:#ef4444;font-weight:500;margin-top:4px">Specialbeslag saknas • +1 vecka försening</div>
</div>
</div>
<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:16px 20px;display:flex;align-items:center;gap:16px">
<div style="width:10px;height:10px;border-radius:50%;background:#94a3b8;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Karlsson Entreprenad</div>
<div style="font-size:12px;color:#64748b">Flerfamiljshus 48 fönster • PRJ-2024-005</div>
</div>
<div style="text-align:right">
<span class="status-badge gray">Offert skickad</span>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Väntar svar sedan 2 veckor</div>
</div>
</div>
<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:16px 20px;display:flex;align-items:center;gap:16px;opacity:.7">
<div style="width:10px;height:10px;border-radius:50%;background:#d1d5db;flex-shrink:0"></div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:2px">Berg Fastigheter AB</div>
<div style="font-size:12px;color:#64748b">BRF renovering 32 fönster • PRJ-2024-006</div>
</div>
<div style="text-align:right">
<span class="status-badge gray">Utkast</span>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Offert ej skickad</div>
</div>
</div>
</div>
</div>
<!-- ORDER -->
<div class="eko-panel" id="eko-order">
<div class="dummy-actions"><button class="dummy-btn">+ Ny order</button><button class="dummy-btn secondary">Exportera</button></div>
<div class="dummy-table">
<div class="dummy-table-header">Orderöversikt</div>
<table>
<thead><tr><th>Order #</th><th>Kund</th><th>Beskrivning</th><th>Belopp</th><th>Orderdatum</th><th>Leveransdatum</th><th>Status</th></tr></thead>
<tbody>
<tr><td>ORD-2026-018</td><td>Andersson Bygg AB</td><td>12 fönster + 2 balkongdörrar</td><td>185 400 kr</td><td>2026-01-15</td><td>2026-02-28</td><td><span class="status-badge green">Levererad</span></td></tr>
<tr><td>ORD-2026-017</td><td>Svensson Fastigheter</td><td>8 fönster villa</td><td>92 500 kr</td><td>2026-01-22</td><td>2026-03-05</td><td><span class="status-badge blue">Under produktion</span></td></tr>
<tr><td>ORD-2026-016</td><td>Johansson & Co</td><td>24 kontorsfönster</td><td>287 200 kr</td><td>2026-02-01</td><td>2026-03-15</td><td><span class="status-badge blue">Under produktion</span></td></tr>
<tr><td>ORD-2026-015</td><td>Nilsson Renovering</td><td>6 fönster + entredörr</td><td>67 300 kr</td><td>2026-01-28</td><td>2026-03-01</td><td><span class="status-badge yellow">Försenad</span></td></tr>
<tr><td>ORD-2026-014</td><td>Pettersson & Söner</td><td>Solpaneler 16st + inverter</td><td>165 000 kr</td><td>2026-02-05</td><td>2026-03-10</td><td><span class="status-badge blue">Bekräftad</span></td></tr>
<tr><td>ORD-2026-013</td><td>Wallin Fastigheter</td><td>6 fönster + foder</td><td>112 000 kr</td><td>2026-02-03</td><td>2026-03-12</td><td><span class="status-badge green">Levererad</span></td></tr>
<tr><td>ORD-2026-012</td><td>Larsson & Partners</td><td>10 fönster radhus</td><td>134 200 kr</td><td>2026-02-08</td><td>2026-03-20</td><td><span class="status-badge blue">Bekräftad</span></td></tr>
<tr><td>ORD-2026-011</td><td>Olsson Bygg & Mål</td><td>4 fönster + balkongdörr</td><td>58 300 kr</td><td>2026-02-10</td><td>2026-03-18</td><td><span class="status-badge gray">Väntar material</span></td></tr>
</tbody>
</table>
</div>
</div>
<!-- FAKTUROR -->
<div class="eko-panel" id="eko-faktura">
<div class="dummy-actions"><button class="dummy-btn">+ Ny faktura</button><button class="dummy-btn secondary">Exportera</button></div>
<div class="dummy-table">
<div class="dummy-table-header">Fakturor</div>
<table>
<thead><tr><th>Faktura #</th><th>Kund</th><th>Belopp</th><th>Förfaller</th><th>Status</th></tr></thead>
<tbody>
<tr><td>FAK-2026-041</td><td>Andersson Bygg AB</td><td>185 400 kr</td><td>2026-03-10</td><td><span class="status-badge green">Betald</span></td></tr>
<tr><td>FAK-2026-040</td><td>Nilsson Renovering</td><td>67 300 kr</td><td>2026-03-08</td><td><span class="status-badge green">Betald</span></td></tr>
<tr><td>FAK-2026-039</td><td>Svensson Fastigheter</td><td>92 500 kr</td><td>2026-03-14</td><td><span class="status-badge yellow">Förfaller snart</span></td></tr>
<tr><td>FAK-2026-038</td><td>Johansson & Co</td><td>287 200 kr</td><td>2026-03-20</td><td><span class="status-badge blue">Skickad</span></td></tr>
<tr><td>FAK-2026-037</td><td>Wallin Fastigheter</td><td>112 000 kr</td><td>2026-02-28</td><td><span class="status-badge yellow">Förfallen</span></td></tr>
<tr><td>FAK-2026-036</td><td>Pettersson & Söner</td><td>78 900 kr</td><td>2026-03-05</td><td><span class="status-badge blue">Skickad</span></td></tr>
</tbody>
</table>
</div>
</div>
<!-- PLOCK -->
<div class="eko-panel" id="eko-plock">
<div class="dummy-table">
<div class="dummy-table-header">Monteringsöversikt</div>
<table>
<thead><tr><th>Mont. #</th><th>Kund</th><th>Beskrivning</th><th>Montör</th><th>Datum</th><th>Progress</th><th>Status</th></tr></thead>
<tbody>
<tr><td>MNT-012</td><td>Andersson Bygg AB</td><td>12 fönster + 2 dörrar</td><td>Erik Lundström</td><td>2026-02-10</td><td class="progress-cell"><div class="progress-bar"><div class="progress-fill" style="width:100%"></div><span class="progress-text">100%</span></div></td><td><span class="status-badge green">Klar</span></td></tr>
<tr><td>MNT-013</td><td>Svensson Fastigheter</td><td>8 fönster villa</td><td>Johan Bergman</td><td>2026-02-14</td><td class="progress-cell"><div class="progress-bar"><div class="progress-fill" style="width:75%"></div><span class="progress-text">75%</span></div></td><td><span class="status-badge blue">Pågår</span></td></tr>
<tr><td>MNT-014</td><td>Johansson & Co</td><td>Kontorsfönster 24 st</td><td>Erik Lundström</td><td>2026-02-18</td><td class="progress-cell"><div class="progress-bar"><div class="progress-fill" style="width:30%"></div><span class="progress-text">30%</span></div></td><td><span class="status-badge blue">Pågår</span></td></tr>
<tr><td>MNT-015</td><td>Nilsson Renovering</td><td>6 fönster + entredörr</td><td>Sofia Nilsson</td><td>2026-02-20</td><td class="progress-cell"><div class="progress-bar"><div class="progress-fill" style="width:0%"></div><span class="progress-text">0%</span></div></td><td><span class="status-badge yellow">Väntar beslag</span></td></tr>
<tr><td>MNT-016</td><td>Pettersson & Söner</td><td>8 fönster</td><td>Johan Bergman</td><td>2026-02-24</td><td class="progress-cell"><div class="progress-bar"><div class="progress-fill" style="width:0%"></div><span class="progress-text">0%</span></div></td><td><span class="status-badge gray">Planerad</span></td></tr>
</tbody>
</table>
</div>
</div>
<!-- KUNDRESKONTRA -->
<div class="eko-panel" id="eko-reskontra">
<div class="dummy-table">
<div class="dummy-table-header">Kundreskontra - Utestående fordringar</div>
<table>
<thead><tr><th>Kund</th><th>Fakturor</th><th>Utestående</th><th>Förfallet</th><th>Senaste betalning</th><th>Kreditgräns</th></tr></thead>
<tbody>
<tr><td>Svensson Fastigheter</td><td>2 st</td><td>92 500 kr</td><td style="color:#ef4444;font-weight:600">0 kr</td><td>2026-02-01</td><td>200 000 kr</td></tr>
<tr><td>Johansson & Co</td><td>1 st</td><td>287 200 kr</td><td style="color:#ef4444;font-weight:600">0 kr</td><td>2026-01-28</td><td>400 000 kr</td></tr>
<tr><td>Wallin Fastigheter</td><td>1 st</td><td>112 000 kr</td><td style="color:#ef4444;font-weight:600">112 000 kr</td><td>2026-01-15</td><td>150 000 kr</td></tr>
<tr><td>Pettersson & Söner</td><td>1 st</td><td>78 900 kr</td><td style="color:#ef4444;font-weight:600">0 kr</td><td>2026-02-05</td><td>100 000 kr</td></tr>
<tr><td>Karlsson Entreprenad</td><td>1 st</td><td>35 900 kr</td><td style="color:#ef4444;font-weight:600">0 kr</td><td>2026-02-10</td><td>300 000 kr</td></tr>
</tbody>
</table>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-top:16px">
<div class="dr-card"><h4>Totalt utestående</h4><div class="dr-val">606 500 kr</div></div>
<div class="dr-card"><h4>Förfallet</h4><div class="dr-val" style="color:#ef4444">112 000 kr</div></div>
<div class="dr-card"><h4>Snitt betaltid</h4><div class="dr-val">24 dagar</div></div>
</div>
</div>
<!-- UTLÄGG -->
<div class="eko-panel" id="eko-utlagg">
<div class="dummy-actions"><button class="dummy-btn">+ Nytt utlägg</button><button class="dummy-btn secondary">Exportera</button></div>
<div class="dummy-table">
<div class="dummy-table-header">Utlägg</div>
<table>
<thead><tr><th>Datum</th><th>Säljare</th><th>Kategori</th><th>Beskrivning</th><th>Belopp</th><th>Status</th></tr></thead>
<tbody>
<tr><td>2026-02-17</td><td>Erik Lundström</td><td>Drivmedel</td><td>Tankning kundbesök Göteborg</td><td>1 245 kr</td><td><span class="status-badge yellow">Granskas</span></td></tr>
<tr><td>2026-02-16</td><td>Maria Andersson</td><td>Parkering</td><td>P-hus Kungsholmen</td><td>180 kr</td><td><span class="status-badge green">Godkänd</span></td></tr>
<tr><td>2026-02-15</td><td>Erik Lundström</td><td>Representation</td><td>Lunch med kund Svensson</td><td>890 kr</td><td><span class="status-badge green">Godkänd</span></td></tr>
<tr><td>2026-02-14</td><td>Johan Bergman</td><td>Material</td><td>Provbitar till kundvisning</td><td>2 340 kr</td><td><span class="status-badge green">Godkänd</span></td></tr>
<tr><td>2026-02-13</td><td>Sofia Nilsson</td><td>Drivmedel</td><td>Resa Lund-Malmö</td><td>680 kr</td><td><span class="status-badge green">Godkänd</span></td></tr>
<tr><td>2026-02-12</td><td>Erik Lundström</td><td>Hotell</td><td>Övernattning Umeå</td><td>1 490 kr</td><td><span class="status-badge green">Godkänd</span></td></tr>
</tbody>
</table>
</div>
</div>
<!-- INKÖP -->
<div class="eko-panel" id="eko-inkop">
<div class="dummy-actions"><button class="dummy-btn">+ Ny beställning</button><button class="dummy-btn secondary">Exportera</button></div>
<div class="dummy-table">
<div class="dummy-table-header">Inköpsöversikt</div>
<table>
<thead><tr><th>Inköp #</th><th>Leverantör</th><th>Beskrivning</th><th>Belopp</th><th>Beställd</th><th>Lev.datum</th><th>Status</th></tr></thead>
<tbody>
<tr><td>INK-2026-034</td><td>Optima Fönster AB</td><td>Spröjsade fönster 2-luft, 24 st</td><td>154 800 kr</td><td>2026-02-01</td><td>2026-03-10</td><td><span class="status-badge blue">Under produktion</span></td></tr>
<tr><td>INK-2026-033</td><td>Optima Fönster AB</td><td>Balkongdörrar 2-glas, 4 st</td><td>35 800 kr</td><td>2026-01-28</td><td>2026-03-05</td><td><span class="status-badge yellow">Packas</span></td></tr>
<tr><td>INK-2026-032</td><td>SolarTech Nordic</td><td>Solpaneler 410W Mono, 40 st</td><td>168 000 kr</td><td>2026-02-05</td><td>2026-02-25</td><td><span class="status-badge green">Levererad</span></td></tr>
<tr><td>INK-2026-031</td><td>Tak & Fasad Grossist</td><td>Betongtakpannor, 320 m²</td><td>272 000 kr</td><td>2026-02-03</td><td>2026-03-01</td><td><span class="status-badge blue">Fraktad</span></td></tr>
<tr><td>INK-2026-030</td><td>Dörr & Port Sverige</td><td>Ytterdörrar ek massiv, 3 st</td><td>37 500 kr</td><td>2026-02-08</td><td>2026-03-15</td><td><span class="status-badge blue">Under produktion</span></td></tr>
<tr><td>INK-2026-029</td><td>Isolerings Experten</td><td>Monteringskit + isolering, 50 set</td><td>38 500 kr</td><td>2026-02-10</td><td>2026-02-20</td><td><span class="status-badge green">Levererad</span></td></tr>
<tr><td>INK-2026-028</td><td>Huawei Energy</td><td>Hybridinverter 8kW, 3 st</td><td>67 500 kr</td><td>2026-02-12</td><td>2026-03-08</td><td><span class="status-badge gray">Väntar bekräftelse</span></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- EVA CHAT (compact sidebar) -->
<div class="eva-chat-wrap">
<div class="eva-chat-header">
<img src="eva.png" alt="Eva" class="eva-avatar">
<div class="eva-header-info"><h3>Eva Andersson <span class="eva-online"></span></h3><p>Solar Energy Group</p></div>
</div>
<div class="eva-messages" id="evaMessages">
<div class="eva-msg eva">
<img src="eva.png" alt="Eva" class="eva-msg-avatar">
<div><div class="eva-msg-bubble">Hej! Fråga mig om ekonomi, leveranser eller försäljning.</div><div class="eva-msg-time">nu</div></div>
</div>
</div>
<div class="eva-suggestions" id="evaSuggestions">
<button class="eva-suggestion" onclick="evaSuggest(this)">Försäljning?</button>
<button class="eva-suggestion" onclick="evaSuggest(this)">Leveransplan?</button>
<button class="eva-suggestion" onclick="evaSuggest(this)">Problem?</button>
<button class="eva-suggestion" onclick="evaSuggest(this)">Bästa säljare?</button>
</div>
<div class="eva-input-wrap">
<textarea class="eva-input" id="evaInput" placeholder="Fråga Eva..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();evaSend()}"></textarea>
<button class="eva-send" id="evaSendBtn" onclick="evaSend()"><svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
</div>
</div>
</div>
</div>
<!-- BILDGENERERING -->
<div class="page-content" id="page-bildgen">
<h1 class="page-title">Bildgenerering</h1>
<p class="page-subtitle">Generera visualiseringar av renovationsprojekt med AI</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
<div style="background:#fff;border-radius:12px;padding:25px;border:1px solid #e5e7eb">
<h3 style="font-size:16px;font-weight:600;margin-bottom:16px">Skapa visualization</h3>
<!-- Steg 1: Foto -->
<div style="margin-bottom:18px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">1. Foto av huset</label>
<div style="display:flex;gap:8px">
<input type="file" id="bgFileInput" accept="image/*" style="display:none" onchange="handleBgUpload(this)">
<input type="file" id="bgCameraInput" accept="image/*" capture="environment" style="display:none" onchange="handleBgUpload(this)">
<button onclick="document.getElementById('bgFileInput').click()" style="flex:1;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:13px">Ladda upp</button>
<button onclick="document.getElementById('bgCameraInput').click()" style="flex:1;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:13px">Ta foto</button>
</div>
<div id="bgPreview" style="margin-top:8px;display:none;position:relative">
<img id="bgPreviewImg" style="width:100%;max-height:300px;object-fit:contain;border-radius:8px;background:#f1f5f9">
<button onclick="clearBgUpload()" style="position:absolute;top:6px;right:6px;background:#ef4444;color:#fff;border:none;border-radius:6px;width:28px;height:28px;cursor:pointer;font-size:14px">✕</button>
</div>
</div>
<!-- Steg 2: Välj typ -->
<div style="margin-bottom:18px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:8px">2. Vad vill du visa?</label>
<input type="hidden" id="bgProjectType" value="">
<input type="hidden" id="bgPrompt" value="">
<div id="bgOptionGrid" style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
</div>
</div>
<!-- Steg 3: Detaljer (visas efter val) -->
<div id="bgDetailsPanel" style="display:none;margin-bottom:18px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:8px">3. Välj stil</label>
<div id="bgSubOptions" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px"></div>
<div id="bgCountPanel" style="display:none;margin-top:12px">
<label id="bgCountLabel" style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:8px">Antal paneler</label>
<div id="bgCountOptions" style="display:flex;flex-wrap:wrap;gap:6px"></div>
</div>
</div>
<!-- Steg 4: Extra (valfritt) -->
<div style="margin-bottom:14px">
<label style="display:block;font-size:13px;font-weight:600;color:#334155;margin-bottom:6px">Extra detaljer (valfritt)</label>
<input type="text" id="bgExtraPrompt" placeholder="T.ex. 'sommardag, blå himmel, grön trädgård'" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">
</div>
<!-- AI-motor -->
<div style="margin-bottom:14px">
<select id="bgProvider" style="width:100%;padding:8px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;color:#64748b">
<option value="openai">OpenAI GPT Image (~0.65 kr/bild)</option>
<option value="openai-mini">OpenAI Mini (~0.20 kr/bild)</option>
<option value="gemini">Google Gemini (~0.30 kr/bild)</option>
</select>
</div>
<button id="bgGenerateBtn" onclick="generateBgImage()" style="width:100%;padding:12px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s">Generera bild</button>
</div>
<div style="background:#fff;border-radius:12px;padding:25px;border:1px solid #e5e7eb">
<h3 style="font-size:16px;font-weight:600;margin-bottom:16px">Genererad bild</h3>
<div id="bgResultArea">
<div id="bgPlaceholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:400px;border:2px dashed #e5e7eb;border-radius:12px;color:#94a3b8">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
<p style="margin-top:12px;font-size:14px">Din genererade bild visas här</p>
<p style="margin-top:4px;font-size:12px">Fyll i formuläret och klicka på "Generera bild"</p>
</div>
<div id="bgLoading" style="display:none;flex-direction:column;align-items:center;justify-content:center;height:400px;border:2px dashed #e5e7eb;border-radius:12px;color:#024550">
<div style="width:40px;height:40px;border:3px solid #e5e7eb;border-top:3px solid #024550;border-radius:50%;animation:bgspin 1s linear infinite"></div>
<p style="margin-top:16px;font-size:14px;font-weight:500">Genererar bild...</p>
<p style="margin-top:4px;font-size:12px;color:#94a3b8">Detta kan ta upp till 30 sekunder</p>
</div>
<div id="bgResult" style="display:none">
<img id="bgResultImg" style="width:100%;border-radius:12px;border:1px solid #e5e7eb">
<div style="display:flex;gap:8px;margin-top:12px">
<button onclick="downloadBgImage()" style="flex:1;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:13px;font-weight:500">⬇ Ladda ner</button>
<button onclick="resetBgResult()" style="flex:1;padding:10px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fafbfc;cursor:pointer;font-family:inherit;font-size:13px;font-weight:500">🔄 Ny bild</button>
</div>
</div>
</div>
<!-- Galleri -->
<div id="bgGallery" style="margin-top:20px;display:none">
<h4 style="font-size:14px;font-weight:600;margin-bottom:10px">Sparade bilder</h4>
<div id="bgGalleryGrid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px"></div>
</div>
</div>
</div>
<style>@keyframes bgspin{to{transform:rotate(360deg)}}</style>
</div>
<!-- PERSONAL -->
<div class="page-content" id="page-personal">
<!-- LIST VIEW -->
<div id="staffListView">
<h1 class="page-title">Personal</h1>
<p class="page-subtitle">Hantera teammedlemmar och roller <span id="staffCount" style="color:#94a3b8;font-weight:400;font-size:13px"></span></p>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center">
<button class="dummy-btn" onclick="showStaffModal()">+ Lägg till personal</button>
<select id="staffRoleFilter" onchange="loadStaff()" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">
<option value="">Alla roller</option>
<option value="admin">Admin</option>
<option value="saljare">Säljare</option>
<option value="saljchef">Säljchef</option>
<option value="installator">Installatör</option>
<option value="projektledare">Projektledare</option>
<option value="ekonomi">Ekonomi</option>
</select>
</div>
<table id="staffDT" class="display" style="width:100%">
<thead><tr><th>Namn</th><th>E-post</th><th>Roll</th><th>Status</th><th style="text-align:right">Försäljning</th></tr></thead>
<tbody></tbody>
</table>
</div>
<!-- DETAIL VIEW -->
<div id="staffDetailView" style="display:none">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
<button onclick="closeStaffDetail()" style="background:none;border:none;cursor:pointer;font-size:20px;color:#64748b;padding:4px 8px">←</button>
<h1 class="page-title" id="staffDetailName" style="margin:0"></h1>
<span id="staffDetailRole" class="status-badge green" style="margin-left:8px"></span>
</div>
<!-- Tabs -->
<div style="display:flex;gap:0;margin-bottom:20px;border-bottom:2px solid #e5e7eb">
<button class="staff-tab active" onclick="switchStaffTab('info')" data-tab="info" style="padding:10px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid #024550;color:#024550;margin-bottom:-2px;font-family:inherit">Info</button>
<button class="staff-tab" onclick="switchStaffTab('loner')" data-tab="loner" style="padding:10px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Löner</button>
<button class="staff-tab" onclick="switchStaffTab('salj')" data-tab="salj" style="padding:10px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Sälj samtal</button>
<button class="staff-tab" onclick="switchStaffTab('utveckling')" data-tab="utveckling" style="padding:10px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Utvecklings samtal</button>
<button class="staff-tab" onclick="switchStaffTab('affarslista')" data-tab="affarslista" style="padding:10px 20px;font-size:13px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Affärer</button>
</div>
<div id="staffTabInfo" class="staff-tab-panel"></div>
<div id="staffTabLoner" class="staff-tab-panel" style="display:none"></div>
<div id="staffTabSalj" class="staff-tab-panel" style="display:none"></div>
<div id="staffTabUtveckling" class="staff-tab-panel" style="display:none"></div>
<div id="staffTabAffarslista" class="staff-tab-panel" style="display:none"></div>
</div>
</div>
<!-- Staff Modal -->
<div id="staffModal" style="display:none;position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;overflow-y:auto;padding:20px">
<div style="background:#fff;border-radius:16px;padding:32px;width:640px;max-width:95vw;box-shadow:0 20px 60px rgba(0,0,0,.2);margin:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">
<h3 id="staffModalTitle" style="font-size:18px;font-weight:700;color:#1a1a1a">Lägg till personal</h3>
<button onclick="closeStaffModal()" style="background:none;border:none;cursor:pointer;font-size:20px;color:#94a3b8;padding:4px">×</button>
</div>
<input type="hidden" id="staffEditId">
<h4 style="font-size:12px;font-weight:700;color:#024550;text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid #e5e7eb">Grunduppgifter</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px">
<div class="dummy-form-group" style="margin:0"><label>Namn *</label><input type="text" id="staffName" placeholder="Anna Andersson"></div>
<div class="dummy-form-group" style="margin:0"><label>E-post *</label><input type="email" id="staffEmail" placeholder="anna@foretag.se"></div>
<div class="dummy-form-group" style="margin:0"><label>Telefon</label><input type="tel" id="staffPhone" placeholder="070-123 45 67"></div>
<div class="dummy-form-group" style="margin:0"><label>Personnummer</label><input type="text" id="staffPnr" placeholder="YYYYMMDD-XXXX"></div>
<div class="dummy-form-group" style="margin:0">
<label>Roll</label>
<select id="staffRole" style="width:100%;padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:'Inter',sans-serif">
<option value="saljare">Säljare</option>
<option value="saljchef">Säljchef</option>
<option value="admin">Admin</option>
<option value="installator">Installatör</option>
<option value="projektledare">Projektledare</option>
<option value="ekonomi">Ekonomi</option>
</select>
</div>
<div class="dummy-form-group" style="margin:0"><label>Titel</label><input type="text" id="staffTitle" placeholder="Projektledare Syd"></div>
</div>
<h4 style="font-size:12px;font-weight:700;color:#024550;text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid #e5e7eb">Adress</h4>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:12px;margin-bottom:20px">
<div class="dummy-form-group" style="margin:0"><label>Adress</label><input type="text" id="staffAddress" placeholder="Storgatan 1"></div>
<div class="dummy-form-group" style="margin:0"><label>Postnummer</label><input type="text" id="staffZip" placeholder="123 45"></div>
<div class="dummy-form-group" style="margin:0"><label>Stad</label><input type="text" id="staffCity" placeholder="Stockholm"></div>
</div>
<h4 style="font-size:12px;font-weight:700;color:#024550;text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid #e5e7eb">Anställning</h4>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:20px">
<div class="dummy-form-group" style="margin:0"><label>Startdatum</label><input type="date" id="staffStartDate"></div>
<div class="dummy-form-group" style="margin:0"><label>Grundlön (kr/mån)</label><input type="number" id="staffGrundlon" placeholder="0"></div>
<div class="dummy-form-group" style="margin:0"><label>Skattesats (%)</label><input type="number" id="staffSkatt" placeholder="30" step="0.1"></div>
</div>
<div style="display:flex;gap:10px;margin-top:24px">
<button class="dummy-btn" onclick="saveStaff()" style="flex:1">Spara</button>
<button class="dummy-btn secondary" onclick="closeStaffModal()">Avbryt</button>
</div>
<div id="staffModalError" style="display:none;margin-top:12px;padding:10px;background:#fef2f2;color:#991b1b;border-radius:8px;font-size:13px"></div>
</div>
</div>
<!-- INSTÄLLNINGAR -->
<div class="page-content" id="page-settings">
<h1 class="page-title">Inställningar</h1>
<p class="page-subtitle">Hantera ditt konto och preferenser</p>
<!-- Settings Tabs -->
<div style="display:flex;gap:0;margin-bottom:24px;border-bottom:2px solid #e5e7eb">
<button class="settings-tab active" data-settings-tab="profil" onclick="switchSettingsTab('profil')">Profil</button>
<button class="settings-tab" data-settings-tab="ai" onclick="switchSettingsTab('ai')">AI Bildgenerering</button>
<button class="settings-tab" data-settings-tab="foretag" onclick="switchSettingsTab('foretag')">Företag</button>
<button class="settings-tab" data-settings-tab="google" onclick="switchSettingsTab('google')">Google API</button>
<button class="settings-tab" data-settings-tab="notiser" onclick="switchSettingsTab('notiser')">Notifieringar</button>
<button class="settings-tab" data-settings-tab="monday" onclick="switchSettingsTab('monday')">Monday.com</button>
<button class="settings-tab" data-settings-tab="valuta" onclick="switchSettingsTab('valuta')">Valuta</button>
</div>
<!-- Tab: Profil -->
<div class="settings-panel active" id="settings-profil">
<div class="dummy-form">
<h3>Profilinställningar</h3>
<div class="dummy-form-group"><label>Namn</label><input type="text" value="Admin User"></div>
<div class="dummy-form-group"><label>E-post</label><input type="email" value="admin@solargroup.se"></div>
<div class="dummy-form-group"><label>Telefon</label><input type="tel" value="070-123 45 67"></div>
<div class="dummy-form-group">
<label>Roll</label>
<select><option>Administratör</option><option>Säljare</option><option>Installatör</option><option>Projektledare</option><option>Ekonomi</option></select>
</div>
<div class="dummy-form-group"><label>Språk</label><select><option>Svenska</option><option>English</option></select></div>
<button class="dummy-btn" style="margin-top:10px">Spara ändringar</button>
</div>
<div class="dummy-form" style="margin-top:20px">
<h3>Byt lösenord</h3>
<div class="dummy-form-group"><label>Nuvarande lösenord</label><input type="password"></div>
<div class="dummy-form-group"><label>Nytt lösenord</label><input type="password"></div>
<div class="dummy-form-group"><label>Bekräfta nytt lösenord</label><input type="password"></div>
<button class="dummy-btn" style="margin-top:10px">Uppdatera lösenord</button>
</div>
</div>
<!-- Tab: AI Bildgenerering -->
<div class="settings-panel" id="settings-ai">
<div class="dummy-form">
<h3>AI Bildgenerering</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:20px">Konfigurera API-nycklar och modeller för bildgenerering. Alla nycklar sparas i databasen.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:12px;display:flex;align-items:center;gap:8px"><span style="background:#000;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">OpenAI</span> GPT Image</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">API-nyckel</label>
<div style="position:relative"><input type="password" id="settOpenaiKey" placeholder="sk-proj-..." style="font-family:monospace;font-size:12px;padding-right:40px"><button type="button" onclick="const i=document.getElementById('settOpenaiKey');const t=i.type==='password'?'text':'password';i.type=t;this.textContent=t==='password'?'Visa':'Dölj'" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#6366f1;font-size:11px;cursor:pointer;font-weight:600">Visa</button></div>
</div>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">Modell</label>
<select id="settOpenaiModel" style="font-size:12px">
<option value="gpt-image-1">gpt-image-1</option>
<option value="gpt-image-1-mini">gpt-image-1-mini</option>
<option value="dall-e-3">DALL-E 3</option>
</select>
</div>
<div id="openaiTestStatus" style="font-size:12px;margin-top:8px"></div>
</div>
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:12px;display:flex;align-items:center;gap:8px"><span style="background:#4285f4;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">Google</span> Gemini</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">API-nyckel</label>
<div style="position:relative"><input type="password" id="settGeminiKey" placeholder="AIzaSy..." style="font-family:monospace;font-size:12px;padding-right:40px"><button type="button" onclick="const i=document.getElementById('settGeminiKey');const t=i.type==='password'?'text':'password';i.type=t;this.textContent=t==='password'?'Visa':'Dölj'" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#6366f1;font-size:11px;cursor:pointer;font-weight:600">Visa</button></div>
</div>
<div style="font-size:11px;color:#94a3b8;margin-top:4px">Modell: gemini-2.5-flash-image</div>
<div id="geminiTestStatus" style="font-size:12px;margin-top:8px"></div>
</div>
</div>
<div style="margin-top:16px;display:flex;gap:8px;align-items:center">
<label style="font-size:13px;font-weight:600;color:#334155">Standard-provider:</label>
<select id="settDefaultProvider" style="padding:6px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff">
<option value="openai">OpenAI</option>
<option value="gemini">Google Gemini</option>
</select>
</div>
<button class="dummy-btn" style="margin-top:16px" onclick="saveAiSettings()">Spara AI-inställningar</button>
</div>
</div>
<!-- Tab: Företag -->
<div class="settings-panel" id="settings-foretag">
<div class="dummy-form">
<h3>Företagsinformation</h3>
<div class="dummy-form-group"><label>Företagsnamn</label><input type="text" value="Solar Group AB"></div>
<div class="dummy-form-group"><label>Org.nummer</label><input type="text" placeholder="556xxx-xxxx"></div>
<div class="dummy-form-group"><label>Adress</label><input type="text" placeholder="Gatan 1, 123 45 Stad"></div>
<div class="dummy-form-group"><label>Telefon</label><input type="tel" placeholder="08-xxx xx xx"></div>
<div class="dummy-form-group"><label>E-post</label><input type="email" placeholder="info@solargroup.se"></div>
<div class="dummy-form-group"><label>Hemsida</label><input type="url" placeholder="https://solargroup.se"></div>
<button class="dummy-btn" style="margin-top:10px">Spara företagsinfo</button>
</div>
<div class="dummy-form" style="margin-top:20px">
<h3>Logotyp</h3>
<div style="display:flex;align-items:center;gap:16px">
<div style="width:80px;height:80px;background:#f1f5f9;border-radius:12px;display:flex;align-items:center;justify-content:center;border:1px solid #e5e7eb">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</div>
<button class="dummy-btn secondary">Ladda upp logotyp</button>
</div>
</div>
</div>
<!-- Tab: Google API -->
<div class="settings-panel" id="settings-google">
<div class="dummy-form">
<h3>Google OAuth & API-nycklar</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:16px">Skapa OAuth-uppgifter i <a href="https://console.cloud.google.com/apis/credentials" target="_blank" style="color:#024550;font-weight:600">Google Cloud Console</a>. Aktivera Gmail API och Google Calendar API.</p>
<div style="background:#eff6ff;border:1px solid #dbeafe;border-radius:8px;padding:12px 16px;margin-bottom:16px">
<p style="font-size:12px;color:#1e40af;margin:0"><strong>Redirect URI att lägga till i Google Console:</strong></p>
<code id="googleRedirectUri" style="font-size:12px;color:#1e40af;background:#dbeafe;padding:2px 6px;border-radius:4px;display:inline-block;margin-top:4px"></code>
</div>
<div class="dummy-form-group">
<label>Google Client ID</label>
<input type="text" id="settGoogleClientId" placeholder="xxxxxx.apps.googleusercontent.com" style="font-family:monospace;font-size:12px">
</div>
<div class="dummy-form-group">
<label>Google Client Secret</label>
<input type="password" id="settGoogleClientSecret" placeholder="GOCSPX-xxxxxxxx" style="font-family:monospace;font-size:12px">
<span id="settGoogleSecretStatus" style="font-size:11px;color:#10b981;margin-left:8px"></span>
</div>
<div class="dummy-form-group">
<label>Google Maps API-nyckel</label>
<input type="text" id="settGoogleMapsKey" placeholder="AIzaSy..." style="font-family:monospace;font-size:12px">
</div>
<button class="dummy-btn" style="margin-top:10px" onclick="saveGoogleSettings()">Spara Google-inställningar</button>
<div id="googleSettingsMsg" style="margin-top:8px;font-size:13px"></div>
</div>
<div class="dummy-form" style="margin-top:20px">
<h3>Scopes som begärs vid inloggning</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:12px">Dessa behörigheter begärs automatiskt när en användare loggar in med Google:</p>
<div style="display:flex;flex-direction:column;gap:8px">
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:#f0fdf4;border-radius:8px;border:1px solid #dcfce7">
<span style="font-size:18px">📧</span>
<div><strong style="font-size:13px">Gmail</strong><br><span style="font-size:11px;color:#64748b">Läsa, skicka och hantera e-post</span></div>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:#f0fdf4;border-radius:8px;border:1px solid #dcfce7">
<span style="font-size:18px">📅</span>
<div><strong style="font-size:13px">Google Kalender</strong><br><span style="font-size:11px;color:#64748b">Visa och skapa kalenderhändelser</span></div>
</div>
<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:#f0fdf4;border-radius:8px;border:1px solid #dcfce7">
<span style="font-size:18px">👤</span>
<div><strong style="font-size:13px">Profil & E-post</strong><br><span style="font-size:11px;color:#64748b">Grundläggande profilinfo och e-postadress</span></div>
</div>
</div>
</div>
</div>
<!-- Tab: Notifieringar -->
<div class="settings-panel" id="settings-notiser">
<div class="dummy-form">
<h3>Notifieringsinställningar</h3>
<div style="display:flex;flex-direction:column;gap:16px">
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<input type="checkbox" checked style="width:18px;height:18px;accent-color:#024550">
<div><strong style="font-size:14px">E-postnotifieringar</strong><br><span style="font-size:12px;color:#94a3b8">Få e-post vid nya offertförfrågningar</span></div>
</label>
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<input type="checkbox" checked style="width:18px;height:18px;accent-color:#024550">
<div><strong style="font-size:14px">Nya kunder</strong><br><span style="font-size:12px;color:#94a3b8">Notifiera vid nya kundregistreringar</span></div>
</label>
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<input type="checkbox" style="width:18px;height:18px;accent-color:#024550">
<div><strong style="font-size:14px">Daglig sammanfattning</strong><br><span style="font-size:12px;color:#94a3b8">Daglig rapport via e-post kl 08:00</span></div>
</label>
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<input type="checkbox" checked style="width:18px;height:18px;accent-color:#024550">
<div><strong style="font-size:14px">Projektuppdateringar</strong><br><span style="font-size:12px;color:#94a3b8">Notifiera vid statusändringar i projekt</span></div>
</label>
</div>
<button class="dummy-btn" style="margin-top:20px">Spara notifieringar</button>
</div>
</div>
<!-- Tab: Monday.com -->
<div class="settings-panel" id="settings-monday">
<div class="dummy-form">
<h3>Monday.com Integration</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:16px">Koppla ihop med Monday.com för att synka projekt, affärer och kontakter.</p>
<div class="dummy-form-group">
<label>API Token</label>
<div style="display:flex;gap:8px">
<input type="password" id="settMondayToken" placeholder="eyJhbGciOiJIUzI1NiJ9..." style="font-family:monospace;font-size:12px;flex:1">
<button onclick="testMondayConnection()" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;white-space:nowrap">Testa</button>
</div>
<div id="mondayConnectionStatus" style="margin-top:8px;font-size:12px"></div>
</div>
<button onclick="saveMondaySettings()" class="dummy-btn" style="margin-top:12px;margin-bottom:24px">Spara</button>
<div id="mondayBoardsSection" style="display:none">
<h3 style="margin-top:0">Boards</h3>
<p style="font-size:12px;color:#94a3b8;margin-bottom:12px">Alla boards i ert Monday-konto</p>
<div id="mondayBoardsList" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
</div>
</div>
<!-- Tab: Valuta -->
<div class="settings-panel" id="settings-valuta">
<div class="dummy-form">
<h3>Valutainställningar</h3>
<p style="font-size:13px;color:#64748b;margin-bottom:20px">Hantera valutakurser för inköpspriser. Produkter kan ha inköpspris i olika valutor — kursen används för att räkna om till SEK.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px">
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:16px;display:flex;align-items:center;gap:8px"><span style="background:#003399;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">EUR</span> Euro</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">Kurs (1 EUR = ? SEK)</label>
<input type="number" step="0.0001" id="settCurrencyEUR" placeholder="11.49" style="font-size:14px;font-weight:600">
</div>
<div id="eurRateInfo" style="font-size:11px;color:#94a3b8"></div>
</div>
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:16px;display:flex;align-items:center;gap:8px"><span style="background:#00247d;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">USD</span> US Dollar</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">Kurs (1 USD = ? SEK)</label>
<input type="number" step="0.0001" id="settCurrencyUSD" placeholder="10.35" style="font-size:14px;font-weight:600">
</div>
<div id="usdRateInfo" style="font-size:11px;color:#94a3b8"></div>
</div>
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:16px;display:flex;align-items:center;gap:8px"><span style="background:#c60c30;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">DKK</span> Dansk Krona</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">Kurs (1 DKK = ? SEK)</label>
<input type="number" step="0.0001" id="settCurrencyDKK" placeholder="1.54" style="font-size:14px;font-weight:600">
</div>
</div>
<div style="background:#f8fafc;border-radius:10px;padding:20px;border:1px solid #e5e7eb">
<h4 style="font-size:15px;margin-bottom:16px;display:flex;align-items:center;gap:8px"><span style="background:#ba0c2f;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">NOK</span> Norsk Krona</h4>
<div class="dummy-form-group" style="margin-bottom:12px">
<label style="font-size:12px">Kurs (1 NOK = ? SEK)</label>
<input type="number" step="0.0001" id="settCurrencyNOK" placeholder="0.98" style="font-size:14px;font-weight:600">
</div>
</div>
</div>
<div style="display:flex;gap:12px;align-items:center">
<button class="dummy-btn" onclick="saveCurrencySettings()">Spara valutakurser</button>
<button onclick="fetchLiveRates()" style="padding:8px 16px;background:#f0f9ff;color:#0284c7;border:1px solid #bae6fd;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Hämta aktuella kurser</button>
<div id="currencySaveStatus" style="font-size:12px"></div>
</div>
<div style="margin-top:24px;padding:16px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px">
<h4 style="font-size:13px;font-weight:700;margin:0 0 8px;color:#059669">Produktvaluta</h4>
<p style="font-size:12px;color:#64748b;margin:0;line-height:1.6">
I produktredigeraren kan du ange inköpsvaluta per produkt. Systemet räknar automatiskt om till SEK med ovanstående kurser vid marginalberäkning.
<br>Standard: <strong>SEK</strong> — om ingen valuta anges räknas priset i SEK.
</p>
</div>
</div>
</div>
</div>
</main>
<!-- MODAL -->
<div class="modal-overlay" id="quoteModal">
<div class="modal">
<div class="modal-header"><h2>Offertförfrågan</h2><button class="modal-close" onclick="closeModal()">x</button></div>
<div class="modal-body">
<div class="modal-grid">
<div class="modal-left">
<div class="modal-section"><div class="modal-section-title">Produkt</div><div class="modal-product"><img src="Photo/2glasspj600.webp" alt="" class="modal-product-image" id="modalProductImage"><div class="modal-product-info"><h3 id="modalProductName">Spröjsat fönster 2-luft</h3><p id="modalDimensions">1190 x 1390 mm</p><p id="modalQuantity" style="color:#10b981;font-weight:600">Antal: 1 st</p></div></div></div>
<div class="modal-section"><div class="modal-section-title">Varukorg</div><div class="modal-cart" id="modalCart"></div></div>
<div class="modal-section"><div class="modal-section-title">Pris</div><div class="modal-price-summary" id="modalPrices"></div></div>
</div>
<div class="modal-right">
<div class="modal-section"><div class="modal-section-title">Kontakt</div>
<form class="modal-form" onsubmit="submitQuote(event)">
<div class="modal-form-row"><div class="modal-form-group"><label>Förnamn *</label><input type="text" id="firstName" required></div><div class="modal-form-group"><label>Efternamn *</label><input type="text" id="lastName" required></div></div>
<div class="modal-form-group full"><label>E-post *</label><input type="email" id="email" required></div>
<div class="modal-form-group full"><label>Telefon *</label><input type="tel" id="phone" required></div>
<div class="modal-form-group full"><label>Adress</label><input type="text" id="address"></div>
<div class="modal-form-group full"><label>Meddelande</label><textarea id="message"></textarea></div>
<button type="submit" class="modal-submit">Skicka offertförfrågan</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
var STAFF_API = '/api/staff.php';
var roleLabels = {systemadmin:'Systemadmin',admin:'Admin',saljchef:'Säljchef',saljare:'Säljare',installator:'Installatör',projektledare:'Projektledare',ekonomi:'Ekonomi',pending:'Pending'};
var roleColors = {systemadmin:'purple',admin:'purple',saljchef:'green',saljare:'green',installator:'blue',projektledare:'yellow',ekonomi:'gray',pending:'gray'};
// === GLOBAL VARS (must be at top to avoid TDZ) ===
var gAccessToken = sessionStorage.getItem('gAccessToken') || '';
var gTokenExpiry = parseInt(sessionStorage.getItem('gTokenExpiry') || '0');
var gStaffId = parseInt(sessionStorage.getItem('gStaffId') || '0');
var gUserEmail = sessionStorage.getItem('gUserEmail') || '';
var gUserName = sessionStorage.getItem('gUserName') || '';
var gUserAvatar = sessionStorage.getItem('gUserAvatar') || '';
var gMailSignature = localStorage.getItem('mailSignature') || '';
function showPage(p){ navigateTo(p); }
var currentPage = 'dashboard';
function navigateTo(page){
currentPage = page;
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
document.querySelectorAll('.nav-item[data-page="'+page+'"]').forEach(n=>n.classList.add('active'));
document.querySelectorAll('.page-content').forEach(p=>p.classList.remove('active'));
const el = document.getElementById('page-'+page);
if(el) el.classList.add('active');
// Update mobile nav
document.querySelectorAll('.mobile-nav a').forEach(a=>{
a.classList.toggle('active', a.dataset.page===page);
});
if(page === 'admin') initAdminPage();
if(page === 'personal') try{loadStaff();}catch(e){}
if(page === 'ue') try{loadUEPage();}catch(e){}
if(page === 'faltsalj'){
var _tryMap = function(retries){
if(typeof google !== 'undefined' && google.maps) {
initFaltMap();
if(gMap) google.maps.event.trigger(gMap,'resize');
} else if(retries > 0) {
setTimeout(function(){ _tryMap(retries-1); }, 500);
}
};
setTimeout(function(){ _tryMap(10); }, 200);
}
if(page === 'inkop') try{loadInkopPage();}catch(e){}
if(page === 'leverantorer') try{loadLeverantorerPage();}catch(e){}
if(page === 'inkorg') try{if(gAccessToken){loadGmailLabels();loadGmailInbox();}}catch(e){}
if(page === 'kalender') try{if(gAccessToken){loadCalendarEvents();}}catch(e){}
// Kalkyler: visa listan om inte redan i konfiguratorvyn
if(page === 'konfigurator'){
showKalkylList();
}
}
document.querySelectorAll('.nav-item').forEach(item=>{
item.addEventListener('click',function(){
navigateTo(this.dataset.page);
});
});
document.querySelectorAll('.mobile-nav a').forEach(item=>{
item.addEventListener('click',function(e){
e.preventDefault();
navigateTo(this.dataset.page);
});
});
/* Kalkyl list/config view toggle */
function showKalkylConfig(){ var _cs2=document.getElementById('categorySelect');if(_cs2)_cs2.value=''; ['solarConfigView','genericConfigView','fonsterConfigView','batteriConfigView','laddboxConfigView','taktvatConfigView','varmepumpConfigView','takConfigView','isoleringConfigView'].forEach(function(v){var el=document.getElementById(v);if(el)el.style.display='none';}); if(typeof loadCfgCategories==='function')loadCfgCategories();
document.getElementById('kalkylListView').style.display='none';
document.getElementById('kalkylConfigView').style.display='block';
const afv = document.getElementById('affarView');
if(afv) afv.style.display='none';
const cat = document.getElementById('categorySelect')?.value;
if(cat) changeCategory();
}
function addQuoteToProspect(pIdx, qId){
if(pIdx < 0 || !faltProspects[pIdx]) return;
if(!faltProspects[pIdx].quoteIds){
faltProspects[pIdx].quoteIds = faltProspects[pIdx].quoteId ? [faltProspects[pIdx].quoteId] : [];
}
if(!faltProspects[pIdx].quoteIds.includes(qId)){
faltProspects[pIdx].quoteIds.push(qId);
}
faltProspects[pIdx].quoteId = qId;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
}
function showProspectQuotes(prospectIdx){
var p = faltProspects[prospectIdx];
if(!p) return;
var ids = p.quoteIds || (p.quoteId ? [p.quoteId] : []);
if(ids.length === 0){ alert('Inga kalkyler'); return; }
if(ids.length === 1){ kalkylOrigin = 'faltsalj'; openQuoteFromList(ids[0]); return; }
var modal = document.createElement('div');
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = function(e){ if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;padding:24px;width:420px;max-width:95vw;box-shadow:0 20px 60px rgba(0,0,0,.2)"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3 style="font-size:17px;font-weight:700;margin:0">V\u00e4lj kalkyl</h3><button id="closeQPBtn" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">\u00d7</button></div><div style="font-size:13px;color:#64748b;margin-bottom:14px">'+(p.addr||'')+(p.city?', '+p.city:'')+'</div><div id="qpList" style="display:flex;flex-direction:column;gap:8px"><div style="text-align:center;padding:20px;color:#94a3b8">Laddar...</div></div></div>';
document.body.appendChild(modal);
document.getElementById('closeQPBtn').onclick = function(){ modal.remove(); };
Promise.all(ids.map(function(id){ return fetch('/api/quotes.php?id='+id).then(function(r){return r.json();}).catch(function(){return null;}); })).then(function(results){
var html = '';
results.forEach(function(data){
if(!data || !data.success || !data.quote) return;
var q = data.quote;
var cat = q.category || 'solceller';
var date = q.updated_at ? new Date(q.updated_at).toLocaleDateString('sv-SE') : '';
var price = q.total_price ? Math.round(q.total_price).toLocaleString('sv-SE') + ' kr' : '';
var st = q.status || 'utkast';
var sc = st === 'offert' ? '#f59e0b' : st === 'order' ? '#10b981' : '#64748b';
html += '<div data-qid="'+q.id+'" class="qp-item" style="padding:14px 16px;border:1.5px solid #e5e7eb;border-radius:10px;cursor:pointer;display:flex;justify-content:space-between;align-items:center"><div><div style="font-weight:600;font-size:14px;color:#1a1a1a;text-transform:capitalize">'+cat+'</div><div style="font-size:12px;color:#64748b;margin-top:2px">#'+q.id+' \u2014 '+date+'</div></div><div style="text-align:right"><div style="font-weight:700;font-size:14px;color:#024550">'+price+'</div><span style="font-size:11px;font-weight:600;color:'+sc+';text-transform:uppercase">'+st+'</span></div></div>';
});
var list = document.getElementById('qpList');
if(list) list.innerHTML = html || '<div style="text-align:center;padding:20px;color:#94a3b8">Inga kalkyler</div>';
document.querySelectorAll('.qp-item').forEach(function(el){
el.onmouseover = function(){ el.style.borderColor='#024550'; el.style.background='#f0fdfa'; };
el.onmouseout = function(){ el.style.borderColor='#e5e7eb'; el.style.background='#fff'; };
el.onclick = function(){ var qid = parseInt(el.getAttribute('data-qid')); modal.remove(); kalkylOrigin='faltsalj'; openQuoteFromList(qid); };
});
});
}
function kalkylGoBack(){
if(kalkylOrigin === 'faltsalj'){
kalkylOrigin = 'konfigurator';
navigateTo('faltsalj');
} else {
showKalkylList();
}
}
function showKalkylList(){
kalkylOrigin = 'konfigurator';
document.getElementById('kalkylConfigView').style.display='none';
const afv = document.getElementById('affarView');
if(afv) afv.style.display='none';
document.getElementById('kalkylListView').style.display='block';
kalkylImages = [];
const kpSec = document.getElementById('kalkylPhotosSection');
if(kpSec) kpSec.style.display = 'none';
loadQuotesList();
}
function changeCategory(){ if(typeof renderCfgCategoryGrid==='function')renderCfgCategoryGrid();
const cat=document.getElementById('categorySelect').value;
const views=['solarConfigView','genericConfigView','fonsterConfigView','batteriConfigView','laddboxConfigView','taktvatConfigView','varmepumpConfigView','takConfigView','isoleringConfigView'];
views.forEach(v=>{const el=document.getElementById(v);if(el)el.style.display='none'});
const afv=document.getElementById('affarView');
if(afv) afv.style.display='none';
const map={
'solceller':['solarConfigView','initSolarConfig'],
'batteri':['batteriConfigView','initBatConfig'],
'laddbox':['laddboxConfigView','initLbConfig'],
'taktvatt':['taktvatConfigView','initTtConfig'],
'varmepump':['varmepumpConfigView','initVpConfig'],
'fonster':['fonsterConfigView','initFonsterConfig'],
'tak':['takConfigView','initTakConfig'],
'isolering':['isoleringConfigView','initIsoConfig']
};
if(map[cat]){
document.getElementById(map[cat][0]).style.display='block';
if(typeof window[map[cat][1]]==='function') window[map[cat][1]]();
} else if(cat) {
document.getElementById('genericConfigView').style.display='block';
}
}
function fmt(p){return p.toLocaleString('sv-SE')+' kr'}
function fmtOpt(p){return p===0?'0,00 kr':p>0?'+'+p.toLocaleString('sv-SE')+' kr':p.toLocaleString('sv-SE')+' kr'}
// === SOLAR CONFIGURATOR ===
// Pricing from SimCRM: full price 116,250 for 8 panels (before green tech deduction)
// Green tech = 20% → customer pays 93,000 (=116,250 × 0.80)
const SOL_FULL_BASE_PRICE = 116250; // Full price before deduction for 8 panels
const SOL_BASE_PANELS = 8;
const SOL_FULL_PRICE_PER_PANEL = 3688; // Full price per extra panel
const SOL_MATERIAL_RATIO = 0.65; // 65% material, 35% installation
const SOL_LABOR_RATIO = 0.35;
const SOL_PANEL_AREA = 1.87; // m² per panel
const SOL_ELECTRICITY_PRICE = 1.50; // kr/kWh
const SOL_SELF_USE = 0.70;
const SOL_SELL_PRICE = 0.50; // kr/kWh for surplus
const SOL_FINANCE_RATE = 0.049; // 4.9% annual
let SOL_FINANCE_YEARS = 15;
const GREEN_TECH_RATE = 0.20; // 20% of total (material+labor+battery+charger)
const ROT_RATE = 0.30; // 30% of labor only
let solSelectedPanel = null;
let solSelectedBattery = 0;
let solSelectedBatteryPrice = 0;
let solSelectedCharger = null;
let solSelectedChargerPrice = 0;
let solOwnerCount = 1;
let solGreenTechMax = 50000;
let solDeductionType = 'green'; // 'green', 'rot', 'both', 'none'
// Current calc state for Affär
let currentCalcState = {};
let kalkylOrigin = 'konfigurator';
// EV chargers from SimCRM
const evChargers = [
{brand:'Garo',model:'22kW',price:26000,desc:'Inkl. lastbalans'},
{brand:'Zaptec',model:'GO',price:26000,desc:'Inkl. lastbalans'},
{brand:'Zaptec',model:'PRO 22kW',price:33000,desc:'Inkl. lastbalans'},
{brand:'Charge Amps',model:'Aura 22kW (2 uttag)',price:40000,desc:'Inkl. lastbalans'}
];
// Battery options from SimCRM
const batteryOptions = [
{kwh:5,price:35000,label:'5 kWh'},
{kwh:10,price:50000,label:'10 kWh'},
{kwh:15,price:65000,label:'15 kWh'},
{kwh:20,price:85000,label:'20 kWh'}
];
function initSolarConfig(){
// Get solar panels from catalog
const panels = catalogProducts.filter(p => p.watt && p.watt > 0 && p.cat === 'solceller');
const container = document.getElementById('solarPanelCards');
if(!panels.length){
// Retry after catalog loads
container.innerHTML='<div style="padding:20px;color:#94a3b8;text-align:center"><div class="spinner" style="display:inline-block;width:20px;height:20px;border:2px solid #e5e7eb;border-top-color:#024550;border-radius:50%;animation:bgspin .6s linear infinite"></div><p style="margin-top:6px;font-size:12px">Laddar paneler...</p></div>';
if(!window._solarRetryCount) window._solarRetryCount = 0;
if(window._solarRetryCount < 10){
window._solarRetryCount++;
setTimeout(initSolarConfig, 500);
} else {
container.innerHTML='<div style="padding:20px;color:#94a3b8;text-align:center">Inga solpaneler i katalogen. Lägg till via Produkter.</div>';
}
// Still run updateSolarCalc with defaults so price shows
updateSolarCalc();
return;
}
window._solarRetryCount = 0;
// Calculate per-panel system cost (inkl. arbete/installation)
const avgSystemPricePerPanel = Math.round(SOL_FULL_BASE_PRICE / SOL_BASE_PANELS);
container.innerHTML = panels.map((p,i) => {
const sel = i===0 ? 'border-color:#3b82f6;background:#f0f9ff' : 'border-color:#e5e7eb;background:#fff';
const dot = i===0 ? '<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>' : '';
const greenTag = p.greenTechEligible ? '<span style="background:#dcfce7;color:#166534;font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:6px">Grönt avdrag</span>' : '';
return '<div class="sol-panel-card" data-panel-id="'+p.id+'" onclick="selectPanel(this,\''+p.id+'\')" style="padding:14px 16px;border:2px solid;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;'+sel+'">'
+'<div style="width:20px;height:20px;border:2px solid '+(i===0?'#3b82f6':'#cbd5e1')+';border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0">'+dot+'</div>'
+(p.img?'<img src="'+p.img+'" style="width:48px;height:48px;object-fit:cover;border-radius:6px;flex-shrink:0">':'<div style="width:48px;height:48px;background:#f1f5f9;border-radius:6px;flex-shrink:0"></div>')
+'<div style="flex:1"><div style="font-weight:600;font-size:14px">'+p.name+greenTag+'</div><div style="font-size:12px;color:#64748b">'+p.watt+'W per panel'+(p.desc?' — '+p.desc:'')+'</div></div>'
+'<div style="text-align:right"><div style="font-weight:700;font-size:14px;color:#1a1a1a">'+avgSystemPricePerPanel.toLocaleString('sv-SE')+' kr/st</div><div style="font-size:10px;color:#94a3b8">inkl. installation</div></div>'
+'</div>';
}).join('');
if(panels.length) solSelectedPanel = panels[0];
// Render battery options
const batContainer = document.getElementById('solarBatteryCards');
batContainer.innerHTML = '<div class="sol-addon-card" data-battery="0" onclick="selectBattery(this)" style="padding:14px 16px;border:2px solid #3b82f6;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:#f0f9ff">'
+'<div style="width:20px;height:20px;border:2px solid #3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"><div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div></div>'
+'<div style="flex:1"><div style="font-weight:600;font-size:14px">Inget batteri</div></div>'
+'<div style="font-weight:700;font-size:14px;color:#1a1a1a">0 kr</div></div>'
+ batteryOptions.map(b => '<div class="sol-addon-card" data-battery="'+b.kwh+'" data-price="'+b.price+'" onclick="selectBattery(this)" style="padding:14px 16px;border:2px solid #e5e7eb;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px">'
+'<div style="width:20px;height:20px;border:2px solid #cbd5e1;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"></div>'
+'<div style="flex:1"><div style="font-weight:600;font-size:14px">'+b.label+' batteri</div><div style="font-size:12px;color:#64748b">Lagra överskottsel'+
'<span style="background:#dcfce7;color:#166534;font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:6px">Grönt avdrag</span></div></div>'
+'<div style="font-weight:700;font-size:14px;color:#1a1a1a">'+b.price.toLocaleString('sv-SE')+' kr</div></div>').join('');
// Render EV charger options
const chgContainer = document.getElementById('solarChargerCards');
chgContainer.innerHTML = '<div class="sol-addon-card" data-charger="0" onclick="selectCharger(this)" style="padding:14px 16px;border:2px solid #3b82f6;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:#f0f9ff">'
+'<div style="width:20px;height:20px;border:2px solid #3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"><div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div></div>'
+'<div style="flex:1"><div style="font-weight:600;font-size:14px">Ingen laddare</div></div>'
+'<div style="font-weight:700;font-size:14px;color:#1a1a1a">0 kr</div></div>'
+ evChargers.map(c => '<div class="sol-addon-card" data-charger="'+c.brand+'-'+c.model+'" data-price="'+c.price+'" onclick="selectCharger(this)" style="padding:14px 16px;border:2px solid #e5e7eb;border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px">'
+'<div style="width:20px;height:20px;border:2px solid #cbd5e1;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"></div>'
+'<div style="flex:1"><div style="font-weight:600;font-size:14px">'+c.brand+' '+c.model+'</div><div style="font-size:12px;color:#64748b">'+c.desc+
'<span style="background:#dcfce7;color:#166534;font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:6px">Grönt avdrag</span></div></div>'
+'<div style="font-weight:700;font-size:14px;color:#1a1a1a">'+c.price.toLocaleString('sv-SE')+' kr</div></div>').join('');
// Sync owner count from customer data if available
if(pendingKalkylCustomer){
const ot = pendingKalkylCustomer.ownerType;
if(ot === '2' || ot === 2) setOwnerCount(2);
else if(ot === 'brf') { setOwnerCount(1); document.getElementById('solGreenSlider').value = 0; updateGreenSlider(); }
}
updateSolarCalc();
}
function selectPanel(el, id){
const p = catalogProducts.find(x => x.id === id);
if(!p) return;
solSelectedPanel = p;
document.querySelectorAll('.sol-panel-card').forEach(c => {
const isSel = c.dataset.panelId === id;
c.style.borderColor = isSel ? '#3b82f6' : '#e5e7eb';
c.style.background = isSel ? '#f0f9ff' : '#fff';
const dot = c.querySelector('div > div');
dot.style.borderColor = isSel ? '#3b82f6' : '#cbd5e1';
dot.innerHTML = isSel ? '<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>' : '';
});
updateSolarCalc();
}
function selectBattery(el){
const kwh = parseInt(el.dataset.battery)||0;
solSelectedBattery = kwh;
solSelectedBatteryPrice = parseInt(el.dataset.price)||0;
document.querySelectorAll('#solarBatteryCards .sol-addon-card').forEach(c => {
const isSel = c === el;
c.style.borderColor = isSel ? '#3b82f6' : '#e5e7eb';
c.style.background = isSel ? '#f0f9ff' : '#fff';
const dot = c.firstElementChild;
dot.style.borderColor = isSel ? '#3b82f6' : '#cbd5e1';
dot.innerHTML = isSel ? '<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>' : '';
});
updateSolarCalc();
}
function selectCharger(el){
const val = el.dataset.charger;
solSelectedCharger = val === '0' ? null : val;
solSelectedChargerPrice = parseInt(el.dataset.price)||0;
document.querySelectorAll('#solarChargerCards .sol-addon-card').forEach(c => {
const isSel = c === el;
c.style.borderColor = isSel ? '#3b82f6' : '#e5e7eb';
c.style.background = isSel ? '#f0f9ff' : '#fff';
const dot = c.firstElementChild;
dot.style.borderColor = isSel ? '#3b82f6' : '#cbd5e1';
dot.innerHTML = isSel ? '<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>' : '';
});
updateSolarCalc();
}
function updateSolarCalc(){
const count = parseInt(document.getElementById('solPanelCount').value)||14;
document.getElementById('solPanelCountVal').textContent = count;
const watt = solSelectedPanel ? solSelectedPanel.watt : 430;
const kwp = (count * watt / 1000).toFixed(1);
const roofArea = Math.round(count * SOL_PANEL_AREA);
const yearlyKwh = Math.round(parseFloat(kwp) * 900);
document.getElementById('solKwpVal').textContent = kwp + ' kWp';
document.getElementById('solRoofVal').textContent = roofArea + ' m²';
document.getElementById('solProdVal').textContent = '~' + yearlyKwh.toLocaleString('sv-SE') + ' kWh';
// Full system price (before green tech deduction)
let systemFullPrice;
if(count <= SOL_BASE_PANELS){
systemFullPrice = SOL_FULL_BASE_PRICE;
} else {
systemFullPrice = SOL_FULL_BASE_PRICE + (count - SOL_BASE_PANELS) * SOL_FULL_PRICE_PER_PANEL;
}
// Split into material and labor
const materialCost = Math.round(systemFullPrice * SOL_MATERIAL_RATIO);
const laborCost = systemFullPrice - materialCost;
const batteryPrice = solSelectedBatteryPrice;
const chargerPrice = solSelectedChargerPrice;
const subtotal = systemFullPrice + batteryPrice + chargerPrice;
// Deduction calculation based on type
let sliderMax = parseInt(document.getElementById('solGreenSlider')?.value);
if(isNaN(sliderMax)) sliderMax = solGreenTechMax;
let totalDeduction = 0;
let deductDetail = '';
const totalLabor = laborCost; // Labor portion of system price
if(solDeductionType === 'green'){
const greenCalc = Math.round(subtotal * GREEN_TECH_RATE);
totalDeduction = Math.min(greenCalc, sliderMax);
deductDetail = '20% av ' + fmt(subtotal) + ' = ' + fmt(greenCalc) + (greenCalc > sliderMax ? ' (begränsat till ' + fmt(sliderMax) + ')' : '');
} else if(solDeductionType === 'rot'){
const rotCalc = Math.round(totalLabor * ROT_RATE);
totalDeduction = Math.min(rotCalc, sliderMax);
deductDetail = '30% av arbete ' + fmt(totalLabor) + ' = ' + fmt(rotCalc) + (rotCalc > sliderMax ? ' (begränsat till ' + fmt(sliderMax) + ')' : '');
} else if(solDeductionType === 'both'){
// Green tech on material+battery+charger, ROT on labor
const greenBase = materialCost + batteryPrice + chargerPrice;
const greenCalc = Math.round(greenBase * GREEN_TECH_RATE);
const rotCalc = Math.round(totalLabor * ROT_RATE);
const bothTotal = greenCalc + rotCalc;
totalDeduction = Math.min(bothTotal, sliderMax);
deductDetail = 'Grönt: ' + fmt(greenCalc) + ' + ROT: ' + fmt(rotCalc) + ' = ' + fmt(bothTotal);
} else {
totalDeduction = 0;
deductDetail = 'Inget avdrag valt';
}
const total = subtotal - totalDeduction;
document.getElementById('solPrMaterial').textContent = fmt(materialCost);
document.getElementById('solPrLabor').textContent = fmt(laborCost);
document.getElementById('solPrBattery').textContent = batteryPrice > 0 ? fmt(batteryPrice) : '0 kr';
document.getElementById('solPrCharger').textContent = chargerPrice > 0 ? fmt(chargerPrice) : '0 kr';
document.getElementById('solPrSubtotal').textContent = fmt(subtotal);
document.getElementById('solPrGreenTech').textContent = totalDeduction > 0 ? '-' + fmt(totalDeduction) : '0 kr';
const detailEl = document.getElementById('solDeductDetail');
if(detailEl) detailEl.textContent = deductDetail;
const el = document.getElementById('solPrTotal');
if(el) el.textContent = fmt(total);
// Monthly financing
const r = SOL_FINANCE_RATE / 12;
const n = SOL_FINANCE_YEARS * 12;
const monthly = total > 0 ? Math.round(total * r * Math.pow(1+r,n) / (Math.pow(1+r,n)-1)) : 0;
document.getElementById('solPrMonthly').textContent = monthly.toLocaleString('sv-SE') + ' kr/mån';
// Yearly savings
const selfUseKwh = yearlyKwh * SOL_SELF_USE;
const surplusKwh = yearlyKwh * (1 - SOL_SELF_USE);
const yearlySaving = Math.round(selfUseKwh * SOL_ELECTRICITY_PRICE + surplusKwh * SOL_SELL_PRICE);
const payback = total > 0 ? (total / yearlySaving).toFixed(1) : 0;
document.getElementById('solPrSaving').textContent = yearlySaving.toLocaleString('sv-SE') + ' kr/år';
document.getElementById('solPrPayback').textContent = payback + ' år';
// Store state for Affär
currentCalcState = {
panelId: solSelectedPanel?.id,
panelName: solSelectedPanel?.name || 'Okänd',
panelWatt: watt,
panelCount: count,
kwp: parseFloat(kwp),
roofArea,
yearlyKwh,
materialCost,
laborCost,
systemFullPrice,
batteryKwh: solSelectedBattery,
batteryPrice,
chargerName: solSelectedCharger,
chargerPrice,
subtotal,
greenTechDeduction: totalDeduction,
deductionType: solDeductionType,
total,
monthly,
yearlySaving,
payback: parseFloat(payback),
financeYears: SOL_FINANCE_YEARS,
financeRate: SOL_FINANCE_RATE,
ownerCount: solOwnerCount
};
}
function setDeductionType(type){
solDeductionType = type;
const label = document.getElementById('solDeductLabel');
const ownerSection = document.getElementById('solOwnerSection');
const sliderSection = document.getElementById('solSliderSection');
const colors = {green:'#059669', rot:'#2563eb', both:'#7c3aed', none:'#94a3b8'};
const labels = {green:'GRÖNT TEKNIK-AVDRAG', rot:'ROT-AVDRAG', both:'GRÖNT TEKNIK + ROT', none:'INGET AVDRAG'};
const col = colors[type] || '#059669';
if(label){ label.textContent = labels[type] || ''; label.style.color = col; }
document.getElementById('solPrGreenTech').style.color = col;
// Show/hide owner + slider for all except 'none'
if(ownerSection) ownerSection.style.display = type === 'none' ? 'none' : '';
if(sliderSection) sliderSection.style.display = type === 'none' ? 'none' : '';
// Update slider accent color
const slider = document.getElementById('solGreenSlider');
if(slider) slider.style.accentColor = col;
// Update button styles
document.querySelectorAll('.deduct-btn').forEach(b => {
const dt = b.dataset.dt;
if(dt === type){
b.style.background = col; b.style.color = '#fff'; b.style.borderColor = col;
} else {
b.style.background = '#fff'; b.style.color = col; b.style.borderColor = '#e5e7eb';
}
});
updateSolarCalc();
}
function setOwnerCount(count){
solOwnerCount = count;
const maxPerPerson = 50000;
const newMax = count * maxPerPerson;
const slider = document.getElementById('solGreenSlider');
if(slider){
slider.max = newMax;
// If current value exceeds new max, or was at old max, set to new max
if(parseInt(slider.value) > newMax || parseInt(slider.value) === solGreenTechMax){
slider.value = newMax;
}
}
solGreenTechMax = newMax;
document.getElementById('solGreenSliderMax').textContent = newMax.toLocaleString('sv-SE') + ' kr';
document.querySelectorAll('.owner-btn').forEach(b => {
const oc = parseInt(b.dataset.oc);
if(oc === count){
b.style.background = '#059669'; b.style.color = '#fff'; b.style.borderColor = '#059669';
} else {
b.style.background = '#fff'; b.style.color = '#059669'; b.style.borderColor = '#bbf7d0';
}
});
updateGreenSlider();
}
function updateGreenSlider(){
const val = parseInt(document.getElementById('solGreenSlider').value)||0;
document.getElementById('solGreenSliderVal').textContent = val.toLocaleString('sv-SE') + ' kr';
updateSolarCalc();
}
function setFinanceYears(yrs){
SOL_FINANCE_YEARS = yrs;
document.querySelectorAll('.fin-yr-btn').forEach(b => {
const y = parseInt(b.dataset.yr);
if(y === yrs){
b.style.background = '#3b82f6';
b.style.color = '#fff';
b.style.borderColor = '#3b82f6';
} else {
b.style.background = '#fff';
b.style.color = '#3b82f6';
b.style.borderColor = '#bfdbfe';
}
});
const info = document.getElementById('solFinanceInfo');
if(info) info.textContent = '('+yrs+' år, 4.9%)';
updateSolarCalc();
}
// === AFFÄR (DEAL) FUNCTIONS ===
let currentQuoteId = null;
let affarExtraCosts = [];
let affarImages = [];
function goToAffar(){
if(!currentCalcState.panelCount){
alert('Välj panel och antal först');
return;
}
// Hide kalkyl views, show affär
document.getElementById('solarConfigView').style.display='none';
document.getElementById('affarView').style.display='block';
// Fill customer banner
const banner = document.getElementById('affarCustomerBanner');
const c = pendingKalkylCustomer;
if(c){
banner.innerHTML='<div style="background:linear-gradient(135deg,#ecfdf5,#f0f9ff);border:1px solid #bbf7d0;border-radius:12px;padding:14px 18px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">'
+'<div style="flex:1;min-width:200px"><div style="font-weight:700;font-size:15px;color:#1a1a1a">'+c.name+'</div><div style="font-size:12px;color:#64748b">'+c.address+'</div></div>'
+(c.phone?'<div style="font-size:13px;color:#334155">'+c.phone+'</div>':'')
+(c.email?'<div style="font-size:13px;color:#334155">'+c.email+'</div>':'')
+'</div>';
} else {
banner.innerHTML='';
}
// Fill config summary
const cs = currentCalcState;
const summary = document.getElementById('affarConfigSummary');
let rows = '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Solpaneler</span><span style="font-size:13px;font-weight:600">'+cs.panelName+' × '+cs.panelCount+' ('+cs.kwp+' kWp)</span></div>';
if(cs.batteryKwh>0) rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Batteri</span><span style="font-size:13px;font-weight:600">'+cs.batteryKwh+' kWh</span></div>';
if(cs.chargerName) rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Laddare</span><span style="font-size:13px;font-weight:600">'+cs.chargerName+'</span></div>';
rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Takyta</span><span style="font-size:13px;font-weight:600">'+cs.roofArea+' m²</span></div>';
rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#ecfdf5;border-radius:8px"><span style="font-size:13px;color:#059669;font-weight:600">Uppsk. produktion</span><span style="font-size:13px;font-weight:700;color:#059669">~'+cs.yearlyKwh.toLocaleString('sv-SE')+' kWh/år</span></div>';
summary.innerHTML = rows;
// Reset extras
affarExtraCosts = [];
// Överför valda kalkylbilder till affär
affarImages = kalkylImages.filter(i => i.selected).map(i => ({ url: i.url, type: i.type, created: i.created }));
currentQuoteId = null;
document.getElementById('affarNotes').value = '';
document.getElementById('affarMarginSlider').value = 0;
renderAffarExtras();
renderAffarImages();
updateAffarCalc();
}
function backToKalkylFromAffar(){
document.getElementById('affarView').style.display='none';
document.getElementById('solarConfigView').style.display='block';
}
function addExtraCost(){
affarExtraCosts.push({name:'',amount:0});
renderAffarExtras();
}
function removeExtraCost(idx){
affarExtraCosts.splice(idx,1);
renderAffarExtras();
updateAffarCalc();
}
function renderAffarExtras(){
const container = document.getElementById('affarExtraCosts');
const empty = document.getElementById('affarExtraCostsEmpty');
if(affarExtraCosts.length===0){
container.innerHTML='';
empty.style.display='block';
return;
}
empty.style.display='none';
container.innerHTML = affarExtraCosts.map((ec,i) =>
'<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">'
+'<input type="text" value="'+(ec.name||'').replace(/"/g,'"')+'" placeholder="Beskrivning (t.ex. Extra kabelarbete)" oninput="affarExtraCosts['+i+'].name=this.value" style="flex:1;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit">'
+'<input type="number" value="'+(ec.amount||0)+'" placeholder="Belopp" oninput="affarExtraCosts['+i+'].amount=parseInt(this.value)||0;updateAffarCalc()" style="width:120px;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;text-align:right">'
+'<span style="font-size:12px;color:#64748b;flex-shrink:0">kr</span>'
+'<button onclick="removeExtraCost('+i+')" style="background:none;border:none;cursor:pointer;padding:4px;color:#ef4444;flex-shrink:0"><svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
+'</div>'
).join('');
}
function updateAffarCalc(){
const cs = currentCalcState;
if(!cs.subtotal) return;
const margin = parseInt(document.getElementById('affarMarginSlider').value)||0;
document.getElementById('affarMarginVal').textContent = margin + '%';
const extrasTotal = affarExtraCosts.reduce((sum,ec)=>sum+(ec.amount||0),0);
const baseBeforeMargin = cs.subtotal + extrasTotal;
const marginAmount = Math.round(baseBeforeMargin * margin / 100);
const totalBeforeDeduction = baseBeforeMargin + marginAmount;
// Use same deduction logic as kalkyl based on deductionType
const dt = cs.deductionType || 'green';
const sliderMax = cs.greenTechDeduction > 0 ? (cs.total + cs.greenTechDeduction - cs.subtotal + cs.greenTechDeduction) : 50000;
const ownerMax = (cs.ownerCount || 1) * 50000;
let totalDeduction = 0;
const totalLabor = cs.laborCost + Math.round(extrasTotal * SOL_LABOR_RATIO) + Math.round(marginAmount * SOL_LABOR_RATIO);
const totalMaterial = totalBeforeDeduction - totalLabor;
if(dt === 'green'){
totalDeduction = Math.min(Math.round(totalBeforeDeduction * GREEN_TECH_RATE), ownerMax);
} else if(dt === 'rot'){
totalDeduction = Math.min(Math.round(totalLabor * ROT_RATE), ownerMax);
} else if(dt === 'both'){
const greenCalc = Math.round(totalMaterial * GREEN_TECH_RATE);
const rotCalc = Math.round(totalLabor * ROT_RATE);
totalDeduction = Math.min(greenCalc + rotCalc, ownerMax);
}
const dealTotal = totalBeforeDeduction - totalDeduction;
// Update sidebar
document.getElementById('affarPrMaterial').textContent = fmt(cs.materialCost);
document.getElementById('affarPrLabor').textContent = fmt(cs.laborCost);
document.getElementById('affarPrBattery').textContent = cs.batteryPrice > 0 ? fmt(cs.batteryPrice) : '0 kr';
document.getElementById('affarPrCharger').textContent = cs.chargerPrice > 0 ? fmt(cs.chargerPrice) : '0 kr';
const extraRow = document.getElementById('affarPrExtraRow');
if(extrasTotal > 0){ extraRow.style.display=''; document.getElementById('affarPrExtra').textContent = fmt(extrasTotal); }
else extraRow.style.display='none';
const marginRow = document.getElementById('affarPrMarginRow');
if(margin > 0){ marginRow.style.display=''; document.getElementById('affarPrMargin').textContent = fmt(marginAmount); document.getElementById('affarPrMarginPct').textContent='('+margin+'%)'; }
else marginRow.style.display='none';
document.getElementById('affarPrSubtotal').textContent = fmt(totalBeforeDeduction);
document.getElementById('affarPrGreenTech').textContent = totalDeduction > 0 ? '-' + fmt(totalDeduction) : '0 kr';
// Update deduction label and colors in affär
const deductLabels = {green:'GRÖNT TEKNIK-AVDRAG', rot:'ROT-AVDRAG', both:'GRÖNT TEKNIK + ROT', none:'INGET AVDRAG'};
const deductColors = {green:'#059669', rot:'#2563eb', both:'#7c3aed', none:'#94a3b8'};
const deductBgs = {green:'#ecfdf5', rot:'#eff6ff', both:'#f5f3ff', none:'#f8fafc'};
const dLabel = document.getElementById('affarDeductLabel');
const dRow = document.getElementById('affarDeductionRow');
if(dLabel){ dLabel.textContent = deductLabels[dt] || 'AVDRAG'; dLabel.style.color = deductColors[dt] || '#059669'; }
if(dRow) dRow.style.background = deductBgs[dt] || '#ecfdf5';
document.getElementById('affarPrGreenTech').style.color = deductColors[dt] || '#059669';
document.getElementById('affarPrTotal').textContent = fmt(dealTotal);
// Monthly
const r = SOL_FINANCE_RATE / 12;
const n = SOL_FINANCE_YEARS * 12;
const monthly = dealTotal > 0 ? Math.round(dealTotal * r * Math.pow(1+r,n) / (Math.pow(1+r,n)-1)) : 0;
document.getElementById('affarPrMonthly').textContent = monthly.toLocaleString('sv-SE') + ' kr/mån';
// Margin info
document.getElementById('affarMarginKr').textContent = fmt(marginAmount);
const profit = marginAmount + extrasTotal;
document.getElementById('affarProfit').textContent = fmt(profit);
}
// Hämta prospektbilder från fältsälj
function addProspectPhotosToAffar(){
const c = pendingKalkylCustomer;
if(!c || c.prospectIdx === undefined) { alert('Ingen prospekt kopplad'); return; }
const p = faltProspects[c.prospectIdx];
if(!p || !p.photos || p.photos.length === 0) { alert('Inga bilder finns på prospektet. Ta bilder i FältSälj först.'); return; }
let added = 0;
p.photos.forEach(ph => {
const url = ph.full || ph.url || ph.dataUrl || ph.thumb;
if(url && !affarImages.find(ai => ai.url === url)){
affarImages.push({ url: url, type: ph.type || 'photo', created: ph.created || new Date().toISOString() });
added++;
}
});
if(added > 0) renderAffarImages();
else alert('Alla prospektbilder finns redan i offerten.');
}
// Skapa prospektbild (med husbilder + solceller)
async function createProspectImage(){
const btn = document.getElementById('affarGenImgBtn');
const cs = currentCalcState;
const c = pendingKalkylCustomer;
const addr = c ? c.address : 'Svenskt hus';
btn.disabled = true;
btn.innerHTML = '<svg style="width:14px;height:14px;animation:spin 1s linear infinite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Skapar bild...';
try {
const prompt = 'Photorealistic image of a Swedish residential house at '+addr+' with '+cs.panelCount+' modern black solar panels installed on the roof. Sunny day, blue sky, Swedish neighborhood. Professional real estate photography style.';
const resp = await fetch('/api/generate-image.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({prompt, size:'1024x1024'})
});
const data = await resp.json();
if(data.success && data.image_url){
affarImages.push({url: data.image_url, type: 'prospect', created: new Date().toISOString()});
renderAffarImages();
} else {
alert('Kunde inte skapa bild: ' + (data.error || 'Okänt fel'));
}
} catch(e){
alert('Kunde inte skapa bild: ' + e.message);
} finally {
btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg> Skapa prospektbild';
}
}
function renderAffarImages(){
const container = document.getElementById('affarImages');
const empty = document.getElementById('affarImagesEmpty');
if(affarImages.length===0){
container.innerHTML='';
empty.style.display='block';
return;
}
empty.style.display='none';
container.innerHTML = affarImages.map((img,i) =>
'<div style="position:relative;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb">'
+'<img src="'+img.url+'" style="width:100%;height:140px;object-fit:cover;cursor:pointer" onclick="openLightbox(\''+img.url.replace(/'/g,"\\'")+'\')">'
+'<button onclick="affarImages.splice('+i+',1);renderAffarImages()" style="position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center">×</button>'
+'</div>'
).join('');
}
// Lightbox
function openLightbox(url){
const lb = document.getElementById('imgLightbox');
const img = document.getElementById('imgLightboxImg');
if(lb && img){ img.src = url; lb.style.display = 'flex'; }
}
function closeLightbox(){
const lb = document.getElementById('imgLightbox');
if(lb) lb.style.display = 'none';
}
// Spara kalkyl från steg 1 (konfiguratorn, utan affär)
async function saveKalkylDraft(){
const cs = currentCalcState;
if(!cs.panelCount){ alert('Välj panel och antal först'); return; }
const c = pendingKalkylCustomer || {};
const selectedImages = kalkylImages.filter(i => i.selected);
const payload = {
customer_name: c.name || null,
customer_address: c.address || null,
customer_email: c.email || null,
customer_phone: c.phone || null,
customer_personnummer: (c.owner1 && c.owner1.pnr) || null,
owner_count: cs.ownerCount || 1,
panel_id: cs.panelId,
panel_name: cs.panelName,
panel_count: cs.panelCount,
panel_watt: cs.panelWatt,
battery_kwh: cs.batteryKwh || 0,
battery_price: cs.batteryPrice || 0,
charger_name: cs.chargerName || null,
charger_price: cs.chargerPrice || 0,
material_cost: cs.materialCost,
labor_cost: cs.laborCost,
subtotal: cs.subtotal,
green_tech_deduction: cs.greenTechDeduction || 0,
total_price: cs.totalPrice || cs.subtotal,
extra_costs: [],
margin_percent: 0,
deal_total: cs.totalPrice || cs.subtotal,
images: selectedImages,
notes: null,
status: 'utkast',
created_by: gStaffId || null,
finance_years: SOL_FINANCE_YEARS,
finance_rate: SOL_FINANCE_RATE,
yearly_kwh: cs.yearlyKwh,
roof_area: cs.roofArea,
prospect_data: c.solarData || null
};
if(currentQuoteId) payload.id = currentQuoteId;
const btn = document.getElementById('saveKalkylBtn');
btn.disabled = true;
btn.innerHTML = '<svg style="width:14px;height:14px;animation:spin 1s linear infinite" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Sparar...';
try {
const resp = await fetch('/api/quotes.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const data = await resp.json();
if(data.success){
currentQuoteId = data.id || currentQuoteId;
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><circle cx="12" cy="12" r="10"/><path d="M9 12l2 2 4-4"/></svg> Sparad!';
btn.style.background = '#059669';
setTimeout(()=>{ btn.innerHTML='<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Spara utkast'; btn.style.background='#f59e0b'; }, 2000);
// Uppdatera prospektstatus
const pIdx = (pendingKalkylCustomer||{}).prospectIdx;
if(pIdx >= 0 && faltProspects[pIdx] && faltProspects[pIdx].status !== 'offert'){
faltProspects[pIdx].status = 'kalkyl';
addQuoteToProspect(pIdx, currentQuoteId);
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
}
loadQuotesList();
} else {
alert('Kunde inte spara: '+(data.error||'Okänt fel'));
}
} catch(e){
alert('Kunde inte spara: '+e.message);
} finally {
btn.disabled = false;
}
}
// Save as draft (from affär view)
async function saveQuoteAsDraft(){
const cs = currentCalcState;
const c = pendingKalkylCustomer || {};
const margin = parseInt(document.getElementById('affarMarginSlider').value)||0;
const extrasTotal = affarExtraCosts.reduce((sum,ec)=>sum+(ec.amount||0),0);
const baseBeforeMargin = cs.subtotal + extrasTotal;
const marginAmount = Math.round(baseBeforeMargin * margin / 100);
const totalBeforeDeduction = baseBeforeMargin + marginAmount;
// Same deduction logic as updateAffarCalc
const dt = cs.deductionType || 'green';
const ownerMax = (cs.ownerCount || 1) * 50000;
const totalLabor = cs.laborCost + Math.round(extrasTotal * SOL_LABOR_RATIO) + Math.round(marginAmount * SOL_LABOR_RATIO);
const totalMaterial = totalBeforeDeduction - totalLabor;
let totalDeduction = 0;
if(dt === 'green') totalDeduction = Math.min(Math.round(totalBeforeDeduction * GREEN_TECH_RATE), ownerMax);
else if(dt === 'rot') totalDeduction = Math.min(Math.round(totalLabor * ROT_RATE), ownerMax);
else if(dt === 'both'){ totalDeduction = Math.min(Math.round(totalMaterial * GREEN_TECH_RATE) + Math.round(totalLabor * ROT_RATE), ownerMax); }
const dealTotal = totalBeforeDeduction - totalDeduction;
const payload = {
customer_name: c.name || null,
customer_address: c.address || null,
customer_email: c.email || null,
customer_phone: c.phone || null,
customer_personnummer: c.personnummer || null,
owner_count: cs.ownerCount || 1,
panel_id: cs.panelId,
panel_name: cs.panelName,
panel_count: cs.panelCount,
panel_watt: cs.panelWatt,
battery_kwh: cs.batteryKwh || 0,
battery_price: cs.batteryPrice || 0,
charger_name: cs.chargerName || null,
charger_price: cs.chargerPrice || 0,
material_cost: cs.materialCost,
labor_cost: cs.laborCost,
subtotal: totalBeforeDeduction,
green_tech_deduction: totalDeduction,
total_price: dealTotal,
extra_costs: affarExtraCosts.filter(ec => ec.name && ec.amount),
margin_percent: margin,
deal_total: dealTotal,
images: affarImages,
notes: document.getElementById('affarNotes').value || null,
status: 'utkast',
created_by: gStaffId || null,
finance_years: SOL_FINANCE_YEARS,
finance_rate: SOL_FINANCE_RATE,
yearly_kwh: cs.yearlyKwh,
roof_area: cs.roofArea,
prospect_data: c.solarData || null
};
if(currentQuoteId) payload.id = currentQuoteId;
try {
const resp = await fetch('/api/quotes.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
});
const data = await resp.json();
if(data.success){
currentQuoteId = data.id || currentQuoteId;
const statusEl = document.getElementById('affarStatus');
statusEl.textContent = 'Utkast sparad ✓';
statusEl.style.background = '#dcfce7';
statusEl.style.color = '#166534';
setTimeout(()=>{ statusEl.textContent='Utkast'; statusEl.style.background='#fef3c7'; statusEl.style.color='#92400e'; }, 2000);
// Uppdatera prospektstatus till "kalkyl"
const pIdx = (pendingKalkylCustomer||{}).prospectIdx;
if(pIdx >= 0 && faltProspects[pIdx] && faltProspects[pIdx].status !== 'offert'){
faltProspects[pIdx].status = 'kalkyl';
addQuoteToProspect(pIdx, currentQuoteId);
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
}
loadQuotesList();
} else {
alert('Kunde inte spara: ' + (data.error || 'Okänt fel'));
}
} catch(e){
alert('Kunde inte spara: ' + e.message);
}
}
async function createFinalQuote(){
await saveQuoteAsDraft();
if(!currentQuoteId) return;
// Update status to offert
try {
await fetch('/api/quotes.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({id: currentQuoteId, status: 'offert'})
});
const statusEl = document.getElementById('affarStatus');
statusEl.textContent = 'Offert skapad!';
statusEl.style.background = '#dcfce7';
statusEl.style.color = '#166534';
// Uppdatera prospektstatus till "offert"
const pIdx = (pendingKalkylCustomer||{}).prospectIdx;
if(pIdx >= 0 && faltProspects[pIdx]){
faltProspects[pIdx].status = 'offert';
addQuoteToProspect(pIdx, currentQuoteId);
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
}
loadQuotesList();
alert('Offert #' + currentQuoteId + ' skapad!\n\nOffert-PDF generering kommer i nästa steg.');
} catch(e){
alert('Kunde inte skapa offert: ' + e.message);
}
}
// Load quotes from DB into list
async function loadQuotesList(){
const body = document.getElementById('quotesListBody');
if(!body) return;
const status = document.getElementById('quotesFilterStatus')?.value || '';
const search = document.getElementById('quotesSearch')?.value.trim() || '';
let url = '/api/quotes.php?';
if(status) url += 'status='+encodeURIComponent(status)+'&';
if(search) url += 'search='+encodeURIComponent(search)+'&';
try {
const resp = await fetch(url);
const data = await resp.json();
if(!data.success || !data.quotes || data.quotes.length === 0){
body.innerHTML = '<tr><td colspan="9" style="padding:40px;text-align:center;color:#94a3b8;font-size:14px">Inga kalkyler hittade</td></tr>';
return;
}
const statusMap = {
utkast: {bg:'#fef3c7',color:'#92400e',label:'Utkast'},
skickad: {bg:'#e0f2fe',color:'#0369a1',label:'Skickad'},
offert: {bg:'#e0f2fe',color:'#0369a1',label:'Offert'},
'godkänd': {bg:'#dcfce7',color:'#166534',label:'Godkänd'},
'förlorad': {bg:'#fee2e2',color:'#991b1b',label:'Förlorad'}
};
body.innerHTML = data.quotes.map(q => {
const st = statusMap[q.status] || {bg:'#f1f5f9',color:'#64748b',label:q.status||'—'};
const date = q.updated_at ? q.updated_at.slice(0,10) : (q.created_at||'').slice(0,10);
const amount = q.deal_total ? parseInt(q.deal_total).toLocaleString('sv-SE')+' kr' : (q.total_price ? parseInt(q.total_price).toLocaleString('sv-SE')+' kr' : '—');
const product = q.panel_name ? q.panel_name + (q.panel_count ? ' × '+q.panel_count : '') : '—';
// Parse images JSON
let imgs = [];
try { if(q.images) imgs = typeof q.images === 'string' ? JSON.parse(q.images) : q.images; } catch(e){}
if(!Array.isArray(imgs)) imgs = [];
let imgHtml = '';
if(imgs.length > 0){
const thumbs = imgs.slice(0,3);
imgHtml = '<div style="display:flex;gap:4px;align-items:center">' + thumbs.map(function(im){
var src = typeof im === 'string' ? im : (im.url||'');
return '<img src="'+src+'" onclick="event.stopPropagation();openLightbox(this.src)" style="width:32px;height:32px;border-radius:4px;object-fit:cover;cursor:pointer;border:1px solid #e5e7eb" onerror="this.style.display=\'none\'">';
}).join('') + (imgs.length > 3 ? '<span style="font-size:10px;color:#94a3b8">+' + (imgs.length-3) + '</span>' : '') + '</div>';
} else {
imgHtml = '<span style="color:#cbd5e1;font-size:11px">—</span>';
}
return '<tr style="border-bottom:1px solid #f1f5f9;cursor:pointer" onclick="openQuoteFromList('+q.id+')" onmouseover="this.style.background=\'#fafbfc\'" onmouseout="this.style.background=\'\'">'
+'<td style="padding:10px 14px;font-size:13px;font-weight:600;color:#024550">#'+q.id+'</td>'
+'<td style="padding:10px 14px;font-size:13px"><div style="font-weight:600;color:#1a1a1a">'+(q.customer_name||'Ej angiven')+'</div><div style="font-size:11px;color:#94a3b8;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(q.customer_address||'')+'</div></td>'
+'<td style="padding:10px 14px;font-size:12px;color:#64748b">'+product+'</td>'
+'<td style="padding:10px 14px;font-size:13px;font-weight:700;color:#1a1a1a;text-align:right">'+amount+'</td>'
+'<td style="padding:10px 14px">'+imgHtml+'</td>'
+'<td style="padding:10px 14px;font-size:12px;color:#64748b">'+(q.created_by_name||'—')+'</td>'
+'<td style="padding:10px 14px;font-size:12px;color:#94a3b8">'+date+'</td>'
+'<td style="padding:10px 14px"><span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:'+st.bg+';color:'+st.color+'">'+st.label+'</span></td>'
+'<td style="padding:10px 14px"><button onclick="event.stopPropagation();deleteQuote('+q.id+')" style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px" title="Ta bort">×</button></td>'
+'</tr>';
}).join('');
} catch(e){
body.innerHTML = '<tr><td colspan="9" style="padding:40px;text-align:center;color:#ef4444;font-size:13px">Kunde inte ladda kalkyler: '+e.message+'</td></tr>';
}
}
async function openQuoteFromList(id){
if(kalkylOrigin !== 'faltsalj') kalkylOrigin = 'konfigurator';
try {
const resp = await fetch('/api/quotes.php?id='+id);
const data = await resp.json();
if(!data.success || !data.quote) { alert('Kunde inte ladda kalkyl'); return; }
const q = data.quote;
currentQuoteId = q.id;
// Bygg pendingKalkylCustomer
pendingKalkylCustomer = {
prospectIdx: -1,
name: q.customer_name || '',
email: q.customer_email || '',
phone: q.customer_phone || '',
address: q.customer_address || '',
ownerType: q.owner_count == 2 ? '2' : (q.owner_count == 0 ? 'brf' : '1'),
maxDeduction: (q.owner_count||1) * 50000,
owner1: { name: q.customer_name || '', pnr: q.customer_personnummer || '' },
owner2: null,
product: 'Solceller',
solarData: q.prospect_data || {},
created: q.created_at
};
const savedImages = q.images || [];
const isOffert = (q.status === 'offert' || q.status === 'skickad' || q.status === 'godkänd');
// Navigera till konfigurator-sidan (viktigt om vi kommer från fältsälj etc)
navigateTo('konfigurator');
// Visa konfiguratorn
showKalkylConfig();
populateKalkylFromCustomer();
// Ladda sparade bilder till kalkylImages
kalkylImages = savedImages.map(img => ({
url: img.url || img,
type: img.type || 'photo',
selected: true,
created: img.created || new Date().toISOString()
}));
renderKalkylPhotos();
// Ladda in kalkylinställningar
setTimeout(() => {
const catSel = document.getElementById('categorySelect');
if(catSel){ catSel.value = 'solceller'; changeCategory(); }
if(q.panel_count){
const slider = document.getElementById('solPanelCount');
if(slider) slider.value = q.panel_count;
}
if(q.battery_kwh){
const bSlider = document.getElementById('solBatteryKwh');
if(bSlider) bSlider.value = q.battery_kwh;
}
updateSolarCalc();
// Bara öppna affär-vyn om det är en offert
if(isOffert){
setTimeout(() => {
const savedExtras = q.extra_costs || [];
const savedNotes = q.notes || '';
const savedMargin = q.margin_percent || 0;
document.getElementById('solarConfigView').style.display='none';
document.getElementById('affarView').style.display='block';
const banner = document.getElementById('affarCustomerBanner');
const c = pendingKalkylCustomer;
if(c && banner){
banner.innerHTML='<div style="background:linear-gradient(135deg,#ecfdf5,#f0f9ff);border:1px solid #bbf7d0;border-radius:12px;padding:14px 18px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">'
+'<div style="flex:1;min-width:200px"><div style="font-weight:700;font-size:15px;color:#1a1a1a">'+(c.name||'Ej angiven')+'</div><div style="font-size:12px;color:#64748b">'+c.address+'</div></div>'
+(c.phone?'<div style="font-size:13px;color:#334155">'+c.phone+'</div>':'')
+(c.email?'<div style="font-size:13px;color:#334155">'+c.email+'</div>':'')
+'</div>';
}
const cs = currentCalcState;
const summary = document.getElementById('affarConfigSummary');
if(summary){
let rows = '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Solpaneler</span><span style="font-size:13px;font-weight:600">'+cs.panelName+' × '+cs.panelCount+' ('+cs.kwp+' kWp)</span></div>';
if(cs.batteryKwh>0) rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Batteri</span><span style="font-size:13px;font-weight:600">'+cs.batteryKwh+' kWh</span></div>';
if(cs.chargerName) rows += '<div style="display:flex;justify-content:space-between;padding:8px 12px;background:#f8fafc;border-radius:8px"><span style="font-size:13px;color:#64748b">Laddare</span><span style="font-size:13px;font-weight:600">'+cs.chargerName+'</span></div>';
summary.innerHTML = rows;
}
affarExtraCosts = savedExtras;
affarImages = savedImages;
currentQuoteId = q.id;
const affarNotesEl = document.getElementById('affarNotes');
if(affarNotesEl) affarNotesEl.value = savedNotes;
const marginSlider = document.getElementById('affarMarginSlider');
if(marginSlider) marginSlider.value = savedMargin;
renderAffarExtras();
renderAffarImages();
updateAffarCalc();
}, 200);
}
}, 100);
} catch(e){
alert('Kunde inte öppna kalkyl: '+e.message);
}
}
async function deleteQuote(id){
if(!confirm('Ta bort kalkyl #'+id+'?')) return;
try {
await fetch('/api/quotes.php?id='+id, {method:'DELETE'});
// Rensa prospekt-koppling i localStorage
faltProspects.forEach(function(p){
if(p.quoteId === id) p.quoteId = null;
if(p.quoteIds){
p.quoteIds = p.quoteIds.filter(function(qid){ return qid !== id; });
if(p.quoteIds.length === 0 && p.status === 'kalkyl') p.status = 'interested';
} else if(p.quoteId === null && p.status === 'kalkyl'){
p.status = 'interested';
}
});
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
loadQuotesList();
} catch(e){ alert('Kunde inte ta bort: '+e.message); }
}
// Edit lead note modal
function editLeadNote(idx){
const p = faltProspects[idx];
if(!p) return;
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;padding:28px;width:440px;max-width:95vw;box-shadow:0 20px 60px rgba(0,0,0,.2)">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'
+'<h3 style="font-size:18px;font-weight:700;margin:0">Anteckning</h3>'
+'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">×</button>'
+'</div>'
+'<div style="font-size:13px;color:#64748b;margin-bottom:12px">'+p.addr+(p.city?', '+p.city:'')+'</div>'
+'<textarea id="leadNoteText" rows="4" style="width:100%;padding:12px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:14px;font-family:inherit;resize:vertical;box-sizing:border-box">'+(p.note||'')+'</textarea>'
+'<div style="display:flex;gap:8px;margin-top:16px;justify-content:flex-end">'
+'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:10px 20px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;cursor:pointer;background:#fff;font-family:inherit">Avbryt</button>'
+'<button onclick="saveLeadNote('+idx+')" style="padding:10px 20px;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;background:#024550;color:#fff;font-family:inherit">Spara</button>'
+'</div></div>';
document.body.appendChild(modal);
setTimeout(()=>document.getElementById('leadNoteText')?.focus(),100);
}
function saveLeadNote(idx){
const text = document.getElementById('leadNoteText')?.value || '';
faltProspects[idx].note = text;
document.querySelector('div[style*="fixed"][style*="9999"]')?.remove();
renderLeadsTable();
}
function closeModal(){const m=document.getElementById('quoteModal');if(m)m.classList.remove('active');document.body.style.overflow=''}
function submitQuote(e){if(e)e.preventDefault();alert('Tack! Vi återkommer.\n\n(DEMO)');closeModal()}
/* === KUNDER MAP === */
/* === DASHBOARD === */
async function loadDashboard() {
try {
// Load deal stats
const [statsR, dealsR, custR] = await Promise.all([
fetch('api/deals.php?action=stats&t='+Date.now()),
fetch('api/deals.php?limit=5&status_exclude=anger,ej_godkand,avbrott&t='+Date.now()),
fetch('api/customers.php?limit=1&t='+Date.now())
]);
const stats = await statsR.json();
const dealsData = await dealsR.json();
const custData = await custR.json();
const el = (id,v) => { const e=document.getElementById(id); if(e) e.textContent=v; };
// Total value (all active deals)
let totalVal = 0, offertCount = 0, acceptedCount = 0;
Object.entries(stats).forEach(([k,v]) => {
totalVal += v.value || 0;
if (k === 'offert') offertCount = v.count || 0;
if (['order','projektering','bestallning','leverans','montering','besiktning','fardigstall'].includes(k)) acceptedCount += v.count || 0;
});
el('dashTotalValue', fmtKr(totalVal));
el('dashOffertar', offertCount);
el('dashKunder', custData.total || 0);
el('dashAccepted', acceptedCount);
// Recent deals
const container = document.getElementById('dashRecentDeals');
if (container && dealsData.deals) {
if (!dealsData.deals.length) {
container.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Inga affärer ännu</div>';
} else {
container.innerHTML = dealsData.deals.map(d => {
const products = (d.product_types||'').split(',').filter(Boolean).map(p => PRODUCT_LABELS[p]||p).join(', ');
const color = DEAL_STATUS_COLORS[d.status]||'#94a3b8';
const val = d.ordervarde_ink_moms ? Math.round(parseFloat(d.ordervarde_ink_moms)).toLocaleString('sv-SE')+' kr' : '-';
return '<div class="offert-item" style="cursor:pointer" onclick="showPage(\'projekt\');setTimeout(()=>showDealDetail('+d.id+'),200)">'
+'<div class="offert-item-left"><h4>'+d.customer_name+'</h4><p>'+(products||d.deal_number)+' • '+(d.datum_salj||'')+'</p></div>'
+'<div class="offert-item-right"><div class="offert-price">'+val+'</div><div class="offert-status" style="color:'+color+'">'+DEAL_STATUS_LABELS[d.status]+'</div></div>'
+'</div>';
}).join('');
}
}
// Top sellers
const tsContainer = document.getElementById('dashTopSellers');
if (tsContainer) {
try {
const tsR = await fetch('api/deals.php?action=top_sellers&t='+Date.now());
const sellers = await tsR.json();
if (sellers.length) {
const medals = ['medal-gold','medal-silver','medal-bronze'];
tsContainer.innerHTML = sellers.slice(0,5).map((s,i) =>
'<div class="top-seller-item">'
+(i<3?'<div class="top-seller-medal '+medals[i]+'">'+(i+1)+'</div>':'<div style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#94a3b8">'+(i+1)+'</div>')
+'<div class="top-seller-info"><div class="top-seller-name">'+s.name+'</div><div class="top-seller-detail">'+s.deal_count+' order</div></div>'
+'<div class="top-seller-amount">'+fmtKr(s.total_value)+'</div>'
+'</div>'
).join('');
} else {
tsContainer.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Ingen data</div>';
}
} catch(e) { tsContainer.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">-</div>'; }
}
} catch(e) { console.error('loadDashboard error:', e); }
}
function fmtKr(n){return n?Math.round(n).toLocaleString('sv-SE')+' kr':'-'}
/* === KUNDER (DataTable) === */
let customerDT = null;
let customersLoaded = false;
let allCustData = [];
async function loadCustomers() {
if (customersLoaded && customerDT && !arguments[0]) return;
try {
const r = await fetch('api/customers.php?limit=10000&offset=0&with_cities=1&t='+Date.now());
const data = await r.json();
const customers = data.customers || [];
allCustData = customers;
document.getElementById('customerCount').textContent = '('+data.total+' kunder)';
// Populate city filter (datalist for search)
if (data.cities) {
var dl = document.getElementById('custCityList');
if (dl) dl.innerHTML = data.cities.map(c => '<option value="'+c+'">').join('');
}
// Populate region filter
populateCustRegionFilter(customers);
loadCustFilters();
var hasFilters = document.getElementById('custCityFilter').value || document.getElementById('custStatusFilter').value || document.getElementById('custDateFrom').value || document.getElementById('custDateTo').value;
if (hasFilters) { applyCustomerFilters(); } else { buildCustomerDT(customers); }
customersLoaded = true;
} catch(e) { console.error('loadCustomers error:', e); }
}
function buildCustomerDT(customers) {
const rows = customers.map(c => [
c.id,
c.name + (c.name2 ? ' & '+c.name2 : ''),
c.city || '',
c.region || '',
c.saljare_name || '',
c.last_deal_date || '',
c.last_deal_status || '',
c.last_salj_status || '',
c.last_status_order || '',
parseInt(c.deal_count) || 0,
parseFloat(c.total_value) || 0
]);
if (customerDT) { customerDT.destroy(); customerDT = null; }
customerDT = $('#customerDT').DataTable({
data: rows,
columns: [
{ title:'ID', width:'50px' },
{ title:'Namn' },
{ title:'Stad', render: function(d){ return '<span style="font-size:12px">'+d+'</span>'; } },
{ title:'Region', render: function(d){ return d ? '<span style="font-size:11px;color:#1e40af">'+d+'</span>' : ''; } },
{ title:'Säljare', render: function(d){ return '<span style="font-size:12px">'+(d||'-')+'</span>'; } },
{ title:'Datum', render: function(d){ return '<span style="font-size:12px;color:#64748b">'+(d||'-')+'</span>'; }, width:'90px' },
{ title:'Status', render: function(d){ if(!d) return '-'; var lbl = DEAL_STATUS_LABELS[d] || d; var clr = DEAL_STATUS_COLORS[d] || '#94a3b8'; return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;color:#fff;background:'+clr+'">'+lbl+'</span>'; } },
{ title:'Status sälj', render: function(d){ if(!d) return '-'; var lbl = SALJ_STATUS_LABELS[d] || d; var clr = SALJ_STATUS_COLORS[d] || '#c4c4c4'; return '<span style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;color:#fff;background:'+clr+'">'+lbl+'</span>'; } },
{ title:'Status order', render: function(d){ return d ? '<span style="font-size:11px;font-weight:600;color:#334155">'+d+'</span>' : '<span style="color:#cbd5e1">-</span>'; } },
{ title:'Antal', className:'dt-center', width:'50px' },
{ title:'Totalvärde', className:'dt-right', render: function(d){ return d ? d.toLocaleString('sv-SE')+' kr' : '-'; } }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ kunder', infoEmpty:'Inga kunder',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Inga kunder hittades'
},
pageLength: 50,
lengthMenu: [25, 50, 100, 250],
order: [[5,'desc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showCustomerDetail(data[0]); });
}
});
}
function applyCustomerFilters() {
var city = document.getElementById('custCityFilter').value;
var status = document.getElementById('custStatusFilter').value;
var saljStatus = document.getElementById('custSaljStatusFilter') ? document.getElementById('custSaljStatusFilter').value : '';
var region = document.getElementById('custRegionFilter') ? document.getElementById('custRegionFilter').value : '';
var dateFrom = document.getElementById('custDateFrom').value;
var dateTo = document.getElementById('custDateTo').value;
var filtered = allCustData;
if (city) { var cl = city.toLowerCase(); filtered = filtered.filter(function(c){ return c.city && c.city.toLowerCase().indexOf(cl) !== -1; }); }
if (status) filtered = filtered.filter(function(c){ return c.last_deal_status === status; });
if (saljStatus) filtered = filtered.filter(function(c){ return c.last_salj_status === saljStatus; });
if (region) filtered = filtered.filter(function(c){ return c.region === region; });
if (dateFrom) filtered = filtered.filter(function(c){ return c.last_deal_date && c.last_deal_date >= dateFrom; });
if (dateTo) filtered = filtered.filter(function(c){ return c.last_deal_date && c.last_deal_date <= dateTo; });
// Update dynamic filter options based on filtered data
updateCustDynamicFilters(filtered, {status:status, saljStatus:saljStatus, region:region, city:city});
buildCustomerDT(filtered);
}
function updateCustDynamicFilters(filtered, current) {
// Status filter - show counts
var statusSel = document.getElementById('custStatusFilter');
if (statusSel) {
var sCounts = {};
filtered.forEach(function(c) { var s = c.last_deal_status; if (s) sCounts[s] = (sCounts[s]||0) + 1; });
var statusOpts = [
{v:'offert',l:'Ny order'},{v:'order',l:'Godkänd'},{v:'projektering',l:'Projektering'},
{v:'bestallning',l:'Beställning'},{v:'leverans',l:'Leverans'},{v:'montering',l:'Montering'},
{v:'fardigstall',l:'Färdigställt'},{v:'anger',l:'Ånger'},{v:'ej_godkand',l:'Ej godkänd'},{v:'avbrott',l:'Avbrott'}
];
statusSel.innerHTML = '<option value="">Alla statusar</option>';
statusOpts.forEach(function(o) {
var cnt = current.status ? '' : (sCounts[o.v] ? ' ('+sCounts[o.v]+')' : '');
// Only show if count > 0 or currently selected
if (sCounts[o.v] || current.status === o.v) {
statusSel.innerHTML += '<option value="'+o.v+'"'+(current.status===o.v?' selected':'')+'>'+o.l+cnt+'</option>';
}
});
}
// Sälj status filter - show counts
var saljSel = document.getElementById('custSaljStatusFilter');
if (saljSel) {
var sjCounts = {};
filtered.forEach(function(c) { var s = c.last_salj_status; if (s) sjCounts[s] = (sjCounts[s]||0) + 1; });
var saljOpts = [
{v:'godkand',l:'Godkänd'},{v:'anger',l:'ÅNGER'},{v:'inte_godkand',l:'Inte godkänd'},
{v:'ej_hanterad',l:'Ej hanterad'},{v:'avvakta',l:'Avvakta'},{v:'underpris',l:'Underpris'},
{v:'halv_provis',l:'Halv provis'},{v:'ingen_provis',l:'Ingen provis'},
{v:'raddad_anger',l:'Räddad ånger'},{v:'senare_loneunderlag',l:'Senare löneunderlag'}
];
saljSel.innerHTML = '<option value="">Alla säljstatus</option>';
saljOpts.forEach(function(o) {
var cnt = current.saljStatus ? '' : (sjCounts[o.v] ? ' ('+sjCounts[o.v]+')' : '');
if (sjCounts[o.v] || current.saljStatus === o.v) {
saljSel.innerHTML += '<option value="'+o.v+'"'+(current.saljStatus===o.v?' selected':'')+'>'+o.l+cnt+'</option>';
}
});
}
// Region filter - show counts
var regSel = document.getElementById('custRegionFilter');
if (regSel) {
var rCounts = {};
filtered.forEach(function(c) { if (c.region) rCounts[c.region] = (rCounts[c.region]||0) + 1; });
regSel.innerHTML = '<option value="">Alla regioner</option>';
Object.keys(rCounts).sort().forEach(function(r) {
var cnt = current.region ? '' : ' ('+rCounts[r]+')';
regSel.innerHTML += '<option value="'+r+'"'+(current.region===r?' selected':'')+'>'+r+cnt+'</option>';
});
}
// City datalist - update from filtered
var dl = document.getElementById('custCityList');
if (dl) {
var cities = {};
filtered.forEach(function(c) { if (c.city) cities[c.city] = true; });
dl.innerHTML = Object.keys(cities).sort().map(function(c) { return '<option value="'+c+'">'; }).join('');
}
}
function populateCustRegionFilter(customers) {
updateCustDynamicFilters(customers, {status:'', saljStatus:'', region:'', city:''});
}
function saveCustFilters() {
var f = {
city: document.getElementById('custCityFilter').value,
status: document.getElementById('custStatusFilter').value,
saljStatus: document.getElementById('custSaljStatusFilter') ? document.getElementById('custSaljStatusFilter').value : '',
region: document.getElementById('custRegionFilter') ? document.getElementById('custRegionFilter').value : '',
dateFrom: document.getElementById('custDateFrom').value,
dateTo: document.getElementById('custDateTo').value
};
document.cookie = 'custFilters=' + encodeURIComponent(JSON.stringify(f)) + ';path=/;max-age=31536000';
}
function loadCustFilters() {
var match = document.cookie.match(/custFilters=([^;]+)/);
if (!match) return;
try {
var f = JSON.parse(decodeURIComponent(match[1]));
if (f.city) document.getElementById('custCityFilter').value = f.city;
if (f.status) document.getElementById('custStatusFilter').value = f.status;
if (f.saljStatus && document.getElementById('custSaljStatusFilter')) document.getElementById('custSaljStatusFilter').value = f.saljStatus;
if (f.region && document.getElementById('custRegionFilter')) document.getElementById('custRegionFilter').value = f.region;
if (f.dateFrom) document.getElementById('custDateFrom').value = f.dateFrom;
if (f.dateTo) document.getElementById('custDateTo').value = f.dateTo;
} catch(e) {}
}
function clearCustFilters() {
document.getElementById('custCityFilter').value = '';
document.getElementById('custStatusFilter').value = '';
if (document.getElementById('custSaljStatusFilter')) document.getElementById('custSaljStatusFilter').value = '';
if (document.getElementById('custRegionFilter')) document.getElementById('custRegionFilter').value = '';
document.getElementById('custDateFrom').value = '';
document.getElementById('custDateTo').value = '';
document.cookie = 'custFilters=;path=/;max-age=0';
buildCustomerDT(allCustData);
}
function filterCustomers() { /* handled by DataTables built-in search */ }
function showNewCustomerModal() {
const html = '<div id="newCustOverlay" style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:40px 20px;overflow-y:auto" onclick="if(event.target===this)this.remove()">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:560px;box-shadow:0 20px 60px rgba(0,0,0,.2)">'
+'<div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center">'
+'<h2 style="font-size:18px;font-weight:700;margin:0">Ny kund</h2>'
+'<button onclick="document.getElementById(\'newCustOverlay\').remove()" style="background:none;border:none;font-size:24px;cursor:pointer;color:#94a3b8">×</button>'
+'</div>'
+'<div style="padding:20px 24px">'
+'<div class="dummy-form-group"><label>Namn *</label><input type="text" id="ncName" placeholder="Förnamn Efternamn"></div>'
+'<div class="dummy-form-group"><label>Namn 2 (medsökande)</label><input type="text" id="ncName2" placeholder=""></div>'
+'<div class="dummy-form-group"><label>Personnummer</label><input type="text" id="ncPnr" placeholder="YYYYMMDD-XXXX"></div>'
+'<div class="dummy-form-group"><label>Telefon</label><input type="text" id="ncPhone" placeholder="07X-XXXXXXX"></div>'
+'<div class="dummy-form-group"><label>E-post</label><input type="email" id="ncEmail" placeholder="kund@example.com"></div>'
+'<div class="dummy-form-group"><label>Adress</label><input type="text" id="ncAddress" placeholder="Gatuadress"></div>'
+'<div class="dummy-form-group"><label>Stad</label><input type="text" id="ncCity" placeholder="Stad"></div>'
+'<div class="dummy-form-group"><label>Anteckningar</label><textarea id="ncNotes" rows="2" placeholder=""></textarea></div>'
+'<button onclick="submitNewCustomer()" style="width:100%;padding:12px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;margin-top:8px">Skapa kund</button>'
+'</div></div></div>';
document.body.insertAdjacentHTML('beforeend', html);
}
async function submitNewCustomer() {
const name = document.getElementById('ncName').value.trim();
if (!name) { alert('Namn krävs'); return; }
const body = {
name: name,
name2: document.getElementById('ncName2').value.trim() || null,
personnummer: document.getElementById('ncPnr').value.trim() || null,
notes: document.getElementById('ncNotes').value.trim() || null,
staff_id: gStaffId || null,
contacts: []
};
const phone = document.getElementById('ncPhone').value.trim();
const email = document.getElementById('ncEmail').value.trim();
if (phone) body.contacts.push({type:'phone', value:phone, is_primary:1});
if (email) body.contacts.push({type:'email', value:email, is_primary:phone?0:1});
try {
const r = await fetch('api/customers.php', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)});
const data = await r.json();
if (data.error) { alert(data.error); return; }
// Create property if address given
const addr = document.getElementById('ncAddress').value.trim();
const city = document.getElementById('ncCity').value.trim();
if (addr && data.id) {
await fetch('api/customers.php?action=property', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({customer_id:data.id, address:addr, city:city||null})});
}
document.getElementById('newCustOverlay').remove();
customersLoaded = false;
loadCustomers();
} catch(e) { alert('Fel: '+e.message); }
}
var _custDetailPrevPage = 'kunder';
var _custDetailData = null;
async function showCustomerDetail(id) {
try {
_custDetailPrevPage = currentPage || 'kunder';
var r = await fetch('api/customers.php?id='+id+'&t='+Date.now());
var c = await r.json();
if (c.error) return;
_custDetailData = c;
var eco = c.economy || {};
var fmtE = function(v){ return v ? parseFloat(v).toLocaleString('sv-SE') + ' kr' : '0 kr'; };
var props = c.properties || [];
var selProp = props.length > 0 ? props[0] : null;
var page = document.getElementById('page-customer-detail');
if (!page) return;
// Property select
var propSelect = '';
if (props.length > 1) {
propSelect = '<select id="cdPropSelect" onchange="switchCustProperty()" style="padding:6px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;min-width:200px">'
+ props.map(function(p,i){ return '<option value="'+i+'">'+p.address+(p.city?' - '+p.city:'')+'</option>'; }).join('')
+ '</select>';
} else if (props.length === 1) {
propSelect = '<span style="font-size:13px;color:#64748b;background:#f1f5f9;padding:6px 14px;border-radius:8px">'+props[0].address+(props[0].city?' - '+props[0].city:'')+'</span>';
}
// Title bar
var titleBar = '<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap">'
+'<button onclick="showPage(_custDetailPrevPage)" style="background:none;border:none;font-size:22px;cursor:pointer;color:#64748b;padding:0;line-height:1">←</button>'
+'<h1 style="font-size:24px;font-weight:800;color:#1a1a1a;margin:0">'+c.name+'</h1>'
+(c.name2?'<span style="font-size:16px;color:#64748b;font-weight:400">& '+c.name2+'</span>':'')
+'<span style="font-size:12px;color:#94a3b8">ID #'+c.id+'</span>'
+'<div style="flex:1"></div>'
+ propSelect
+'</div>';
// Contacts badges row
var contactsBadges = '<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">'
+(c.contacts||[]).map(function(ct){
return '<a href="'+(ct.type==='phone'?'tel:'+ct.value:'mailto:'+ct.value)+'" style="display:flex;align-items:center;gap:6px;background:'+(ct.type==='phone'?'#f0fdf4':'#eff6ff')+';padding:5px 12px;border-radius:8px;border:1px solid '+(ct.type==='phone'?'#86efac':'#93c5fd')+';text-decoration:none;color:#334155;font-size:13px;font-weight:600">'
+(ct.type==='phone'?'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>':'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>')
+ct.value
+(ct.is_primary==='1'||ct.is_primary===1?'<span style="font-size:9px;background:#dbeafe;color:#1e40af;padding:1px 5px;border-radius:4px">Prim\u00e4r</span>':'')
+'</a>';
}).join('')
+'</div>';
// Notes
var notesHtml = '';
if (c.notes) {
notesHtml = '<div style="background:#fefce8;border:1px solid #fde68a;border-radius:10px;padding:10px 14px;margin-bottom:16px">'
+'<span style="font-size:11px;font-weight:700;color:#92400e;text-transform:uppercase">Anteckningar: </span>'
+'<span style="font-size:13px;color:#78350f">'+c.notes+'</span></div>';
}
// ========== 3-COLUMN HEADER ==========
// Col 1: Kundinfo + Fastighetsinfo
var infoRow = function(lbl,val){ if(!val) return ''; return '<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:12px"><span style="color:#94a3b8">'+lbl+'</span><span style="font-weight:600;color:#334155">'+val+'</span></div>'; };
var kundBox = '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin-bottom:10px">'
+'<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #f1f5f9">Kundinformation</div>'
+'<div style="font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:4px">'+c.name+'</div>'
+infoRow('Personnummer', c.personnummer)
+(c.name2?'<div style="font-size:14px;font-weight:600;color:#334155;margin-top:6px">'+c.name2+'</div>':'')
+infoRow('Personnummer 2', c.personnummer2)
+infoRow('Kund-ID', '#'+c.id)
+'</div>';
var fastBox = '<div id="cdFastBox" style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px">'
+'<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #f1f5f9">Fastighetsinformation</div>';
if (selProp) {
fastBox += '<div style="font-size:14px;font-weight:700;color:#1a1a1a;margin-bottom:6px">'+selProp.address+'</div>'
+(selProp.city?'<div style="font-size:12px;color:#64748b;margin-bottom:6px">'+selProp.city+(selProp.zip?' '+selProp.zip:'')+'</div>':'')
+infoRow('Beteckning', selProp.fastighetsbeteckning)
+infoRow('Huvuds\u00e4kring', selProp.huvudsakring?(selProp.huvudsakring+' A'):null)
+infoRow('Takmaterial', selProp.takmaterial)
+infoRow('Taktyp', selProp.taktyp)
+infoRow('N\u00e4t\u00e4gare', selProp.natagare)
+infoRow('Anl\u00e4ggnings-ID', selProp.anlaggningsid)
+infoRow('Elm\u00e4tare', selProp.placering_elmatare)
+infoRow('Relation', selProp.relation_type==='owner'?'\u00c4gare':(selProp.relation_type||'\u00c4gare'));
} else {
fastBox += '<div style="font-size:13px;color:#94a3b8;text-align:center;padding:20px 0">Ingen fastighet kopplad</div>';
}
fastBox += '</div>';
var col1 = '<div>' + kundBox + fastBox + '</div>';
// Col 2: Map
var mapAddr = selProp ? (selProp.address + (selProp.city ? ', ' + selProp.city : '') + ', Sverige') : '';
var col2 = '<div id="cdMapCol" style="min-height:100%">';
if (mapAddr) {
col2 += '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;height:100%">'
+'<iframe id="cdMapFrame" width="100%" height="100%" style="border:0;min-height:340px" loading="lazy" allowfullscreen referrerpolicy="no-referrer-when-downgrade"'
+' src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8&q='+encodeURIComponent(mapAddr)+'"></iframe></div>';
} else {
col2 += '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;height:100%;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:13px">Ingen karta tillg\u00e4nglig</div>';
}
col2 += '</div>';
// Col 3: Economy blocks stacked
var fakturerat = parseFloat(eco.total_intakter || 0);
var ordervarde = parseFloat(eco.total_ordervarde || 0);
var reskontra = ordervarde - fakturerat;
function ecoBlock(label,val,bgColor,borderColor,textColor,icon){
return '<div style="background:'+bgColor+';border:1px solid '+borderColor+';border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:12px">'
+'<div style="width:36px;height:36px;border-radius:8px;background:'+textColor+'15;color:'+textColor+';display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0">'+icon+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px;font-weight:600">'+label+'</div>'
+'<div style="font-size:17px;font-weight:800;color:'+textColor+';margin-top:2px">'+fmtE(val)+'</div>'
+'</div></div>';
}
var col3 = '<div style="display:flex;flex-direction:column;gap:8px">'
+ ecoBlock('Orderv\u00e4rde', eco.total_ordervarde, '#f0fdf4', '#86efac', '#15803d', '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>')
+ ecoBlock('Fakturerat', eco.total_intakter, '#eff6ff', '#93c5fd', '#1d4ed8', '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>')
+ ecoBlock('Reskontra', reskontra>0?reskontra:0, reskontra>0?'#fef2f2':'#f8fafc', reskontra>0?'#fca5a5':'#e2e8f0', reskontra>0?'#ef4444':'#64748b', '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>')
+ ecoBlock('ROT-avdrag', eco.total_rot, '#faf5ff', '#d8b4fe', '#7c3aed', 'R')
+ ecoBlock('Marginal', eco.total_marginal, '#fefce8', '#fde68a', '#a16207', 'M')
+ '<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:12px 14px;display:flex;align-items:center;gap:12px">'
+'<div style="width:36px;height:36px;border-radius:8px;background:#02455015;color:#024550;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:800;flex-shrink:0">'+(eco.active_deals||0)+'</div>'
+'<div style="flex:1"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px;font-weight:600">Aff\u00e4rer</div>'
+'<div style="font-size:15px;font-weight:700;color:#1e293b;margin-top:2px">'+(eco.active_deals||0)+' aktiva <span style="font-size:12px;color:#94a3b8">/ '+(eco.total_deals||0)+' totalt</span></div></div></div>'
+ '</div>';
var headerGrid = '<div style="display:grid;grid-template-columns:280px 1fr 220px;gap:14px;margin-bottom:20px">'
+ col1 + col2 + col3
+ '</div>';
// ========== DEALS TABLE ==========
var dealsSection = '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
+'<div style="font-size:14px;font-weight:700;color:#1a1a1a">Aff\u00e4rer <span style="font-size:12px;color:#94a3b8;font-weight:400">('+(c.deals||[]).length+')</span></div>'
+'</div>'
+'<table id="custDealsDT" class="display" style="width:100%"><thead><tr>'
+'<th>Aff\u00e4rsnr</th><th>Produkter</th><th style="text-align:right">Orderv\u00e4rde</th><th>Status</th><th>Status s\u00e4lj</th><th>Status order</th><th>S\u00e4ljare</th><th>Region</th><th>UE</th><th>Datum</th>'
+'</tr></thead><tbody></tbody></table></div>';
page.innerHTML = titleBar + contactsBadges + notesHtml + headerGrid + dealsSection;
showPage('customer-detail');
// Build deals DataTable
setTimeout(function() {
var dRows = (c.deals||[]).map(function(d) {
var prods = (d.product_types||'').split(',').filter(Boolean).map(function(p){ return PRODUCT_LABELS[p]||p; }).join(', ');
return [d.id, d.deal_number||'', prods, parseFloat(d.ordervarde_ink_moms||0),
d.status||'', d.salj_status||'', d.status_order||'', d.saljare1_name||'',
d.region||'', d.tilldelad_ue||'', d.datum_salj||''];
});
if ($.fn.DataTable.isDataTable('#custDealsDT')) $('#custDealsDT').DataTable().destroy();
$('#custDealsDT').DataTable({
data: dRows,
columns: [
{ title:'Aff\u00e4rsnr', render:function(d,t,r){ return '<strong style="color:#024550">'+r[1]+'</strong>'; }},
{ title:'Produkter', render:function(d,t,r){ return '<span style="font-size:12px;color:#64748b">'+(r[2]||'-')+'</span>'; }},
{ title:'Orderv\u00e4rde', className:'dt-right', render:function(d,t,r){ return t==='sort'?r[3]:r[3]?r[3].toLocaleString('sv-SE')+' kr':'-'; }},
{ title:'Status', render:function(d,t,r){ var cl=DEAL_STATUS_COLORS[r[4]]||'#94a3b8'; return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+cl+'">'+(DEAL_STATUS_LABELS[r[4]]||r[4]||'-')+'</span>'; }},
{ title:'Status s\u00e4lj', render:function(d,t,r){ var cl=SALJ_STATUS_COLORS[r[5]]||'#c4c4c4'; return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+cl+'">'+(SALJ_STATUS_LABELS[r[5]]||r[5]||'-')+'</span>'; }},
{ title:'Status order', render:function(d,t,r){ return r[6]||'<span style="color:#cbd5e1">-</span>'; }},
{ title:'S\u00e4ljare', render:function(d,t,r){ return '<span style="font-size:12px">'+(r[7]||'-')+'</span>'; }},
{ title:'Region', render:function(d,t,r){ return r[8]?'<span style="font-size:11px;color:#1e40af">'+r[8]+'</span>':''; }},
{ title:'UE', render:function(d,t,r){ return r[9]?'<span style="font-size:11px;color:#9a3412" title="'+r[9]+'">'+r[9].substring(0,20)+(r[9].length>20?'...':'')+'</span>':''; }},
{ title:'Datum', render:function(d,t,r){ return '<span style="font-size:12px;color:#64748b">'+(r[10]||'-')+'</span>'; }, width:'90px'}
],
language: { search:'S\u00f6k:', lengthMenu:'Visa _MENU_', info:'_START_-_END_ av _TOTAL_', infoEmpty:'Inga aff\u00e4rer', paginate:{next:'N\u00e4sta',previous:'F\u00f6reg.'}, zeroRecords:'Inga aff\u00e4rer' },
pageLength: 25, order:[[9,'desc']],
createdRow:function(row,data){ $(row).css('cursor','pointer').on('click',function(){ showDealDetail(data[0]); }); }
});
}, 100);
} catch(e) { console.error('showCustomerDetail error:', e); }
}
function switchCustProperty() {
if (!_custDetailData) return;
var sel = document.getElementById('cdPropSelect');
if (!sel) return;
var idx = parseInt(sel.value);
var p = (_custDetailData.properties||[])[idx];
if (!p) return;
var infoRow = function(lbl,val){ if(!val) return ''; return '<div style="display:flex;justify-content:space-between;padding:3px 0;font-size:12px"><span style="color:#94a3b8">'+lbl+'</span><span style="font-weight:600;color:#334155">'+val+'</span></div>'; };
var fastBox = document.getElementById('cdFastBox');
if (fastBox) {
fastBox.innerHTML = '<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #f1f5f9">Fastighetsinformation</div>'
+'<div style="font-size:14px;font-weight:700;color:#1a1a1a;margin-bottom:6px">'+p.address+'</div>'
+(p.city?'<div style="font-size:12px;color:#64748b;margin-bottom:6px">'+p.city+(p.zip?' '+p.zip:'')+'</div>':'')
+infoRow('Beteckning', p.fastighetsbeteckning)
+infoRow('Huvuds\u00e4kring', p.huvudsakring?(p.huvudsakring+' A'):null)
+infoRow('Takmaterial', p.takmaterial)
+infoRow('Taktyp', p.taktyp)
+infoRow('N\u00e4t\u00e4gare', p.natagare)
+infoRow('Anl\u00e4ggnings-ID', p.anlaggningsid)
+infoRow('Elm\u00e4tare', p.placering_elmatare)
+infoRow('Relation', p.relation_type==='owner'?'\u00c4gare':(p.relation_type||'\u00c4gare'));
}
// Update map
var mapCol = document.getElementById('cdMapCol');
if (mapCol && p.address) {
var addr = p.address + (p.city ? ', ' + p.city : '') + ', Sverige';
mapCol.innerHTML = '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden;height:100%">'
+'<iframe width="100%" height="100%" style="border:0;min-height:340px" loading="lazy" allowfullscreen referrerpolicy="no-referrer-when-downgrade"'
+' src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8&q='+encodeURIComponent(addr)+'"></iframe></div>';
}
}
/* === AFFÄRER / DEALS === */
var allDeals = [];
var dealOffset = 0;
var dealTotal = 0;
var DEAL_LIMIT = 5000;
var dealView = 'table';
var mainDealsDT = null;
const DEAL_STATUS_LABELS = {
offert:'Ny order', order:'Godkänd', projektering:'Projektering',
bestallning:'Beställning', leverans:'Leverans', montering:'Montering',
besiktning:'Besiktning', fardigstall:'Färdigställt',
anger:'Ånger', ej_godkand:'Ej godkänd', avbrott:'Avbrott'
};
const DEAL_STATUS_COLORS = {
offert:'#eab308', order:'#16a34a', projektering:'#3b82f6',
bestallning:'#8b5cf6', leverans:'#06b6d4', montering:'#f97316',
besiktning:'#ec4899', fardigstall:'#10b981',
anger:'#ef4444', ej_godkand:'#dc2626', avbrott:'#94a3b8'
};
const SALJ_STATUS_LABELS = {
ej_hanterad:'Ej hanterad', godkand:'Godkänd', inte_godkand:'Inte godkänd',
anger:'ÅNGER', anger_fraga:'Ånger?', raddad_anger:'Räddad ånger',
avvakta:'Avvakta', underpris:'Underpris', halv_provis:'Halv provis',
ingen_provis:'Ingen provis', senare_loneunderlag:'Senare löneunderlag'
};
const SALJ_STATUS_COLORS = {
ej_hanterad:'#c4c4c4', godkand:'#00c875', inte_godkand:'#df2f4a',
anger:'#bb3354', anger_fraga:'#007eb5', raddad_anger:'#9d50dd',
avvakta:'#ff6d3b', underpris:'#cab641', halv_provis:'#ffcb00',
ingen_provis:'#ff7575', senare_loneunderlag:'#ff007f'
};
const PRODUCT_LABELS = {
sol_batteri:'Sol & Batteri', batteri:'Batteri', tak:'Tak', fonster:'Fönster',
luftvarmepump:'Luftvärmepump', isolering:'Isolering', taktvatt:'Taktvätt',
utvandig_malning:'Utvändig målning', laddbox:'Laddbox', solceller:'Solceller', fagelband:'Fågelband'
};
function setDealView(v) {
dealView = v;
loadDeals();
}
var dealsLoaded = false;
var allDealsCache = [];
async function loadDeals() {
if (!dealsLoaded) {
try {
var r = await fetch('api/deals.php?limit=' + DEAL_LIMIT + '&t=' + Date.now());
var data = await r.json();
allDealsCache = data.deals || [];
populateDealFilters(allDealsCache);
dealsLoaded = true;
} catch(e) { console.error('loadDeals error:', e); return; }
}
applyDealFilters();
loadDealStats();
}
function reloadDeals() {
applyDealFilters();
}
function applyDealFilters() {
var status = document.getElementById('dealFilterStatus').value;
var product = document.getElementById('dealFilterProduct').value;
var month = document.getElementById('dealFilterMonth') ? document.getElementById('dealFilterMonth').value : '';
var saljStatus = document.getElementById('dealFilterSaljStatus') ? document.getElementById('dealFilterSaljStatus').value : '';
var region = document.getElementById('dealFilterRegion') ? document.getElementById('dealFilterRegion').value : '';
allDeals = allDealsCache.filter(function(d) {
if (status && d.status !== status) return false;
if (product && !(d.product_types || '').split(',').includes(product)) return false;
if (month && !(d.datum_salj || '').startsWith(month)) return false;
if (saljStatus && d.salj_status !== saljStatus) return false;
if (region && d.region !== region) return false;
return true;
});
dealTotal = allDeals.length;
buildMainDealsDT(allDeals);
}
function populateDealFilters(deals) {
var months = {};
var regions = {};
deals.forEach(function(d) {
var ds = d.datum_salj || '';
if (ds.length >= 7) months[ds.substring(0, 7)] = true;
if (d.region) regions[d.region] = true;
});
// Month filter
var keys = Object.keys(months).sort().reverse();
var sel = document.getElementById('dealFilterMonth');
if (sel) {
var old = sel.value;
sel.innerHTML = '<option value="">Alla månader</option>';
var mNames = ['','Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
keys.forEach(function(m) {
var p = m.split('-');
sel.innerHTML += '<option value="' + m + '">' + p[0] + ' ' + mNames[parseInt(p[1])] + '</option>';
});
if (old) sel.value = old;
}
// Region filter
var regSel = document.getElementById('dealFilterRegion');
if (regSel) {
var oldR = regSel.value;
regSel.innerHTML = '<option value="">Alla regioner</option>';
Object.keys(regions).sort().forEach(function(r) {
regSel.innerHTML += '<option value="' + r + '">' + r + '</option>';
});
if (oldR) regSel.value = oldR;
}
}
function buildMainDealsDT(deals) {
var rows = deals.map(function(d) {
var products = (d.product_types||'').split(',').filter(Boolean).map(function(p){ return PRODUCT_LABELS[p]||p; }).join(', ');
return [
d.id,
d.deal_number || '',
d.customer_name || '',
products || '',
parseFloat(d.ordervarde_ink_moms || 0),
d.saljare1_name || '',
d.datum_salj || '',
d.status || '',
d.salj_status || '',
d.status_order || '',
d.region || ''
];
});
if (mainDealsDT) { mainDealsDT.destroy(); mainDealsDT = null; }
mainDealsDT = $('#dealsDT').DataTable({
data: rows,
columns: [
{ title:'Affärsnr', render: function(d,t,r){ return '<strong style="color:#024550">' + r[1] + '</strong>'; } },
{ title:'Kund', render: function(d,t,r){ return r[2]; } },
{ title:'Produkter', render: function(d,t,r){ return '<span style="font-size:12px;color:#64748b">' + (r[3]||'-') + '</span>'; } },
{ title:'Ordervärde', className:'dt-right', render: function(d,t,r){
if (t === 'sort' || t === 'type') return r[4];
return r[4] ? r[4].toLocaleString('sv-SE') + ' kr' : '-';
}},
{ title:'Säljare', render: function(d,t,r){ return '<span style="font-size:12px">' + (r[5]||'-') + '</span>'; } },
{ title:'Datum', render: function(d,t,r){
if (t === 'sort' || t === 'type') return r[6];
return '<span style="font-size:12px;color:#64748b">' + (r[6]||'-') + '</span>';
}, width:'90px' },
{ title:'Status', render: function(d,t,r){
var color = DEAL_STATUS_COLORS[r[7]] || '#94a3b8';
var label = DEAL_STATUS_LABELS[r[7]] || r[7] || '-';
if (t === 'filter') return label;
return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+color+'">'+label+'</span>';
} },
{ title:'Status sälj', render: function(d,t,r){
var color = SALJ_STATUS_COLORS[r[8]] || '#c4c4c4';
var label = SALJ_STATUS_LABELS[r[8]] || r[8] || '-';
if (t === 'filter') return label;
return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+color+'">'+label+'</span>';
} },
{ title:'Status order', render: function(d,t,r){
if (t === 'filter') return r[9] || '-';
if (!r[9]) return '<span style="font-size:11px;color:#cbd5e1">-</span>';
return '<span style="font-size:11px;font-weight:600;color:#334155">' + r[9] + '</span>';
} },
{ title:'Region', render: function(d,t,r){
if (t === 'filter') return r[10] || '-';
if (!r[10]) return '';
return '<span style="font-size:11px;color:#64748b">' + r[10] + '</span>';
} }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ affärer', infoEmpty:'Inga affärer',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Inga affärer hittades'
},
pageLength: 50,
lengthMenu: [25, 50, 100, 200],
order: [[5,'desc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showDealDetail(data[0]); });
}
});
}
async function loadDealStats() {
try {
const r = await fetch('api/deals.php?action=stats&t='+Date.now());
const stats = await r.json();
const el = (id,v) => { const e=document.getElementById(id); if(e) e.textContent=v; };
const sum = (keys) => keys.reduce((a,k) => a + (stats[k]?.count||0), 0);
const sumV = (keys) => keys.reduce((a,k) => a + (stats[k]?.value||0), 0);
el('dStatOffert', stats.offert?.count||0);
el('dSumOffert', fmtKr(stats.offert?.value||0));
el('dStatOrder', stats.order?.count||0);
el('dSumOrder', fmtKr(stats.order?.value||0));
const pKeys = ['projektering','bestallning','leverans','montering','besiktning'];
el('dStatPagaende', sum(pKeys));
el('dSumPagaende', fmtKr(sumV(pKeys)));
el('dStatFardig', stats.fardigstall?.count||0);
el('dSumFardig', fmtKr(stats.fardigstall?.value||0));
const lKeys = ['anger','ej_godkand','avbrott'];
el('dStatLost', sum(lKeys));
el('dSumLost', fmtKr(sumV(lKeys)));
} catch(e) { console.error('loadDealStats error:', e); }
}
async function loadDealPipeline() {
try {
var params = 'action=pipeline&t='+Date.now();
var dateFrom = document.getElementById('pipelineDateFrom');
var dateTo = document.getElementById('pipelineDateTo');
var region = document.getElementById('pipelineRegion');
if(dateFrom && dateFrom.value) params += '&date_from='+dateFrom.value;
if(dateTo && dateTo.value) params += '&date_to='+dateTo.value;
if(region && region.value) params += '®ion='+encodeURIComponent(region.value);
var ue = document.getElementById('pipelineUE');
if(ue && ue.value) params += '&ue='+encodeURIComponent(ue.value);
var prod = document.getElementById('pipelineProduct');
if(prod && prod.value) params += '&product_type='+encodeURIComponent(prod.value);
const r = await fetch('api/deals.php?'+params);
const data = await r.json();
renderPipeline(data.pipeline);
// Populate region dropdown on first load
if(region && region.options.length <= 1){
fetch('api/deals.php?action=pipeline&t=0').then(function(r){return r.json();}).then(function(allData){
var regions = {};
var ues = {};
Object.values(allData.pipeline).flat().forEach(function(d){
if(d.region) regions[d.region] = true;
if(d.tilldelad_ue){
d.tilldelad_ue.split(',').forEach(function(u){
u = u.trim();
if(!u || u.indexOf('@') >= 0) return;
// Skip likely person names (two words, no company keywords)
var companyWords = /AB$|AB |Bygg|Tak|El[tj]|Sol[aeo]|Kyl|Plåt|Iso|Move|Clean|Nordic|Reko|Tech|Vänner|service|maskin|Entreprenad|Energi|Koppar|Furuviksplåt|Powermove|Solartech|Algust|Inovela|WBOAR|ZLATAN|DAKERT|HemsAB|TFM/i;
if(!companyWords.test(u)) return;
ues[u] = (ues[u]||0) + 1;
});
}
});
Object.keys(regions).sort().forEach(function(rg){
var opt = document.createElement('option');
opt.value = rg; opt.textContent = rg;
region.appendChild(opt);
});
var ueSel = document.getElementById('pipelineUE');
if(ueSel){
Object.keys(ues).sort().forEach(function(u){
var opt = document.createElement('option');
opt.value = u; opt.textContent = u + ' (' + ues[u] + ')';
ueSel.appendChild(opt);
});
}
var prods = {};
var prodLabels = typeof PRODUCT_LABELS !== 'undefined' ? PRODUCT_LABELS : {};
Object.values(allData.pipeline).flat().forEach(function(d){
if(d.product_types){
d.product_types.split(',').forEach(function(pt){
pt = pt.trim();
if(pt) prods[pt] = true;
});
}
});
var prodSel = document.getElementById('pipelineProduct');
if(prodSel){
Object.keys(prods).sort().forEach(function(pt){
var opt = document.createElement('option');
opt.value = pt;
opt.textContent = prodLabels[pt] || pt.charAt(0).toUpperCase() + pt.slice(1);
prodSel.appendChild(opt);
});
}
}).catch(function(){});
}
} catch(e) { console.error('loadDealPipeline error:', e); }
}
function clearPipelineFilters(){
document.getElementById('pipelineDateFrom').value = '';
document.getElementById('pipelineDateTo').value = '';
document.getElementById('pipelineRegion').value = '';
document.getElementById('pipelineUE').value = '';
document.getElementById('pipelineProduct').value = '';
_akutFilterActive = false;
_avbrottFilterActive = false;
_colAkutFilter = {};
var akutBox = document.getElementById('pipelineAkut');
akutBox.style.border = '1.5px solid #f59e0b';
akutBox.style.background = '#fef9c3';
var avbBox = document.getElementById('pipelineAvbrott');
avbBox.style.border = '1.5px solid #fca5a5';
avbBox.style.background = '#fef2f2';
loadDealPipeline();
}
var _pipelineCollapsed = {};
var _pipelineData = {};
var _akutFilterActive = false;
var _colAkutFilter = {};
var _avbrottFilterActive = false;
function toggleAkutExpand() {
_akutFilterActive = !_akutFilterActive;
var box = document.getElementById('pipelineAkut');
if(_akutFilterActive){
box.style.border = '2.5px solid #ef4444';
box.style.background = '#fef2f2';
} else {
box.style.border = '1.5px solid #f59e0b';
box.style.background = '#fef9c3';
}
renderPipeline(_pipelineData);
}
function toggleAvbrottExpand() {
_avbrottFilterActive = !_avbrottFilterActive;
var box = document.getElementById('pipelineAvbrott');
if(_avbrottFilterActive){
box.style.border = '2.5px solid #ef4444';
box.style.background = '#fecaca';
} else {
box.style.border = '1.5px solid #fca5a5';
box.style.background = '#fef2f2';
}
renderPipeline(_pipelineData);
}
function togglePipelineCol(stage) {
var cur = (stage in _pipelineCollapsed) ? _pipelineCollapsed[stage] : true;
_pipelineCollapsed[stage] = !cur;
renderPipeline(_pipelineData);
}
function toggleColAkut(stage, event) {
event.stopPropagation();
_colAkutFilter[stage] = !_colAkutFilter[stage];
renderPipeline(_pipelineData);
}
function pipelineColDragEnter(el, stage) {
el.style.opacity = '0.7';
clearTimeout(window._colExpandTimer);
window._colExpandTimer = setTimeout(function(){
_pipelineCollapsed[stage] = false;
renderPipeline(_pipelineData);
}, 500);
}
function renderPipeline(pipeline) {
_pipelineData = pipeline;
const stages = ['projektering','bestallning','leverans','montering','besiktning','fardigstall'];
const container = document.getElementById('pipelineColumns');
// Avbrott section (top-right)
const avbrott = [...(pipeline['avbrott']||[]), ...(pipeline['anger']||[]), ...(pipeline['ej_godkand']||[])];
document.getElementById('avbrottBadge').textContent = avbrott.length;
const avbrottTotal = avbrott.reduce((a,d) => a + parseFloat(d.ordervarde_ink_moms||0), 0);
document.getElementById('avbrottSum').textContent = avbrottTotal ? fmtKr(avbrottTotal) : '';
// Akut section - deals older than 30 days (not fardigstall/avbrott/anger/ej_godkand)
const now = Date.now();
const akutDeals = [];
['projektering','bestallning','leverans','montering','besiktning'].forEach(function(s){
(pipeline[s]||[]).forEach(function(d){
if(d.datum_salj && (now - new Date(d.datum_salj).getTime())/864e5 > 30) akutDeals.push(Object.assign({_stage:s},d));
});
});
document.getElementById('akutBadge').textContent = akutDeals.length;
var akutTotal = akutDeals.reduce(function(a,d){return a+parseFloat(d.ordervarde_ink_moms||0);},0);
document.getElementById('akutSum').textContent = akutTotal ? fmtKr(akutTotal) : '';
document.getElementById('pipelineAkut').style.animation = akutDeals.length > 0 ? 'blink-red 2s ease-in-out infinite' : 'none';
document.getElementById('akutList').innerHTML = akutDeals.length ? akutDeals.map(function(d){
var days = Math.floor((now - new Date(d.datum_salj).getTime())/864e5);
var stageLabel = DEAL_STATUS_LABELS[d._stage]||d._stage;
return '<div onclick="event.stopPropagation();showDealDetail('+d.id+')" style="background:#fff;border:1px solid #f59e0b;border-radius:6px;padding:6px 8px;margin-bottom:4px;cursor:pointer;font-size:11px">'
+'<div style="display:flex;justify-content:space-between"><span style="font-weight:600;color:#1a1a1a">'+(d.customer_name||'Okänd')+'</span><span style="color:#ef4444;font-weight:700;font-size:10px">'+days+' dagar</span></div>'
+'<div style="color:#64748b">'+(d.deal_number||'')+'</div>'
+'<div style="display:flex;justify-content:space-between;margin-top:2px"><span style="font-size:10px;color:#b45309">'+stageLabel+'</span>'+(d.ordervarde_ink_moms?'<span style="color:#024550;font-weight:600">'+fmtKr(parseFloat(d.ordervarde_ink_moms))+'</span>':'')+'</div>'
+'</div>';
}).join('') : '<div style="font-size:11px;color:#94a3b8;text-align:center;padding:8px">Inga akuta</div>';
document.getElementById('avbrottList').innerHTML = avbrott.length ? avbrott.map(d => {
return '<div onclick="event.stopPropagation();showDealDetail('+d.id+')" style="background:#fff;border:1px solid #fca5a5;border-radius:6px;padding:6px 8px;margin-bottom:4px;cursor:pointer;font-size:11px">'
+'<div style="font-weight:600;color:#1a1a1a">'+(d.customer_name||'Okänd')+'</div>'
+'<div style="color:#64748b">'+(d.deal_number||'')+'</div>'
+(d.avbrott_status?'<div style="color:#ef4444;font-weight:500;margin-top:2px">'+d.avbrott_status+'</div>':'')
+(d.ordervarde_ink_moms?'<div style="color:#024550;font-weight:600;margin-top:1px">'+fmtKr(parseFloat(d.ordervarde_ink_moms))+'</div>':'')
+'</div>';
}).join('') : '<div style="font-size:11px;color:#94a3b8;text-align:center;padding:8px">Inga avbrott</div>';
container.innerHTML = stages.map(s => {
var deals = pipeline[s] || [];
if(_avbrottFilterActive){
deals = [];
} else if(_akutFilterActive && s !== 'fardigstall'){
deals = deals.filter(function(d){ return d.datum_salj && (Date.now() - new Date(d.datum_salj).getTime())/864e5 > 30; });
} else if(_akutFilterActive && s === 'fardigstall'){
deals = [];
} else if(_colAkutFilter[s] && s !== 'fardigstall'){
deals = deals.filter(function(d){ return d.datum_salj && (Date.now() - new Date(d.datum_salj).getTime())/864e5 > 30; });
}
const color = DEAL_STATUS_COLORS[s];
const total = deals.reduce((a,d) => a + parseFloat(d.ordervarde_ink_moms||0), 0);
const collapsed = (s in _pipelineCollapsed) ? _pipelineCollapsed[s] : (deals.length === 0);
if (collapsed) {
return '<div style="min-width:44px;width:44px;cursor:pointer;user-select:none" onclick="togglePipelineCol(\''+s+'\')" ondragover="pipelineDragOver(event)" ondragenter="pipelineColDragEnter(this,\''+s+'\')" ondragleave="this.style.opacity=\'1\'" ondrop="pipelineDrop(event,\''+s+'\')">'
+'<div style="padding:8px 4px;background:'+color+'15;border-radius:10px;border-left:3px solid '+color+';height:100%;display:flex;flex-direction:column;align-items:center;gap:8px">'
+'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="'+color+'" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>'
+'<span style="writing-mode:vertical-lr;text-orientation:mixed;font-size:11px;font-weight:700;color:'+color+'">'+DEAL_STATUS_LABELS[s]+'</span>'
+'<span style="font-size:11px;font-weight:700;background:'+color+';color:#fff;padding:2px 6px;border-radius:8px">'+deals.length+'</span>'
+(total?'<span style="writing-mode:vertical-lr;text-orientation:mixed;font-size:9px;color:#64748b">'+fmtKr(total)+'</span>':'')
+'</div></div>';
}
return '<div style="min-width:155px;flex:1" data-stage="'+s+'">'
+'<div style="padding:6px 10px;background:'+color+'15;border-radius:10px 10px 0 0;border-bottom:3px solid '+color+';user-select:none">'
+'<div style="display:flex;justify-content:space-between;align-items:center">'
+'<div style="display:flex;align-items:center;gap:4px;cursor:pointer" onclick="togglePipelineCol(\''+s+'\')">'
+'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="'+color+'" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>'
+'<span style="font-size:11px;font-weight:700;color:'+color+'">'+DEAL_STATUS_LABELS[s]+'</span></div>'
+'<div style="display:flex;align-items:center;gap:4px">'
+(function(){var ac=0;if(s!=='fardigstall'){(pipeline[s]||[]).forEach(function(d){if(d.datum_salj&&(Date.now()-new Date(d.datum_salj).getTime())/864e5>30)ac++;});}return ac?'<span onclick="toggleColAkut(\''+s+'\',event)" style="font-size:9px;font-weight:700;background:'+(_colAkutFilter[s]?'#b91c1c':'#ef4444')+';color:#fff;padding:1px 5px;border-radius:8px;cursor:pointer;animation:blink-red 1.5s ease-in-out infinite;'+(_colAkutFilter[s]?'outline:2px solid #7f1d1d;':'')+'" title="Visa bara akuta">🔥'+ac+'</span>':'';}())
+'<span style="font-size:10px;font-weight:600;background:'+color+';color:#fff;padding:1px 7px;border-radius:10px">'+deals.length+'</span>'
+'</div></div>'
+'<div style="font-size:9px;color:#64748b;margin-top:1px;padding-left:18px">'+(total?fmtKr(total):'')+'</div></div>'
+'<div class="pipeline-drop" data-stage="'+s+'" ondragover="pipelineDragOver(event)" ondrop="pipelineDrop(event,\''+s+'\')" ondragenter="this.style.background=\''+color+'10\'" ondragleave="this.style.background=\'#f8f9fa\'" style="background:#f8f9fa;border-radius:0 0 10px 10px;padding:4px;min-height:80px;display:flex;flex-direction:column;gap:4px;transition:background .15s">'
+(deals.length ? deals.map(d => pipelineCard(d)).join('') : '<div style="text-align:center;color:#cbd5e1;font-size:11px;padding:16px 0">Inga projekt</div>')
+'</div></div>';
}).join('');
// If avbrott filter active, append avbrott columns
if(_avbrottFilterActive){
var avbrottGroups = {'avbrott':[],'anger':[],'ej_godkand':[]};
['avbrott','anger','ej_godkand'].forEach(function(s){
(pipeline[s]||[]).forEach(function(d){ avbrottGroups[s].push(d); });
});
var abLabels = {avbrott:'Avbrott',anger:'Ånger',ej_godkand:'Ej godkänd'};
var abColors = {avbrott:'#94a3b8',anger:'#ef4444',ej_godkand:'#dc2626'};
container.innerHTML = ['avbrott','anger','ej_godkand'].map(function(s){
var ds = avbrottGroups[s];
var cl = abColors[s];
var tot = ds.reduce(function(a,d){return a+parseFloat(d.ordervarde_ink_moms||0);},0);
return '<div style="min-width:200px;flex:1" data-stage="'+s+'">'
+'<div style="padding:6px 10px;background:'+cl+'15;border-radius:10px 10px 0 0;border-bottom:3px solid '+cl+'">'
+'<div style="display:flex;justify-content:space-between;align-items:center">'
+'<span style="font-size:11px;font-weight:700;color:'+cl+'">'+abLabels[s]+'</span>'
+'<span style="font-size:10px;font-weight:600;background:'+cl+';color:#fff;padding:1px 7px;border-radius:10px">'+ds.length+'</span></div>'
+'<div style="font-size:9px;color:#64748b;margin-top:1px">'+(tot?fmtKr(tot):'')+'</div></div>'
+'<div style="background:#f8f9fa;border-radius:0 0 10px 10px;padding:4px;min-height:80px;display:flex;flex-direction:column;gap:4px">'
+(ds.length?ds.map(function(d){return pipelineCard(d);}).join(''):'<div style="text-align:center;color:#cbd5e1;font-size:11px;padding:16px 0">Inga</div>')
+'</div></div>';
}).join('');
}
}
function pipelineCard(d) {
const products = (d.product_types||'').split(',').filter(Boolean).map(p => PRODUCT_LABELS[p]||p).join(', ');
return '<div draggable="true" ondragstart="pipelineDragStart(event,'+d.id+')" onclick="showDealDetail('+d.id+')" data-deal-id="'+d.id+'" style="background:#fff;border:1px solid #e5e7eb;border-radius:6px;padding:7px 8px;cursor:grab;transition:box-shadow .15s,opacity .15s" onmouseover="this.style.boxShadow=\'0 2px 8px rgba(0,0,0,.08)\'" onmouseout="this.style.boxShadow=\'none\'">'
+(function(){var _isAkut=d.datum_salj&&d.status!=='fardigstall'&&(Date.now()-new Date(d.datum_salj).getTime())/864e5>30;return _isAkut?'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px"><span style="font-size:11px;font-weight:600;color:#1a1a1a;line-height:1.2">'+(d.customer_name||'Okänd kund')+'</span><span style="font-size:12px;animation:blink-red 1.5s ease-in-out infinite" title="Akut - äldre än 30 dagar">🔥</span></div>':'<div style="font-size:11px;font-weight:600;color:#1a1a1a;margin-bottom:2px;line-height:1.2">'+(d.customer_name||'Okänd kund')+'</div>';}())
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:2px"><span style="font-size:10px;color:#64748b">'+(d.deal_number||'')+'</span>'+(d.datum_salj?(function(){var age=(Date.now()-new Date(d.datum_salj).getTime())/864e5;var old=age>30&&d.status!=='fardigstall';return '<span style="font-size:9px;font-weight:'+(old?'700':'400')+';color:'+(old?'#ef4444':'#94a3b8')+';'+(old?'animation:blink-red 1.5s ease-in-out infinite':'')+'">'+ d.datum_salj+'</span>'})():'')+'</div>'
+(products?'<div style="font-size:9px;color:#94a3b8;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+products+'</div>':'')
+(d.ordervarde_ink_moms?'<div style="font-size:11px;font-weight:700;color:#024550">'+fmtKr(parseFloat(d.ordervarde_ink_moms))+'</div>':'')
+'<div style="display:flex;gap:3px;flex-wrap:wrap;margin-top:2px;align-items:center">'
+(d.salj_status?'<span style="font-size:8px;padding:0px 5px;border-radius:3px;font-weight:600;'+(({'godkand':'background:#dcfce7;color:#166534','anger':'background:#fee2e2;color:#991b1b','inte_godkand':'background:#fee2e2;color:#991b1b','raddad_anger':'background:#f3e8ff;color:#7c3aed','anger_fraga':'background:#e0f2fe;color:#0369a1','ej_hanterad':'background:#f1f5f9;color:#64748b','avvakta':'background:#fef9c3;color:#854d0e'})[d.salj_status]||'background:#f1f5f9;color:#64748b')+'">'+(({'godkand':'Godkänd','anger':'Ånger','inte_godkand':'Ej godkänd','raddad_anger':'Räddad ånger','anger_fraga':'Ånger?','ej_hanterad':'Ej hanterad','avvakta':'Avvakta','underpris':'Underpris','halv_provis':'Halv provision','ingen_provis':'Ingen provision','senare_loneunderlag':'Senare löneunderlag'})[d.salj_status]||d.salj_status)+'</span>':'')
+(d.saljare1_name?'<span style="font-size:9px;color:#94a3b8">'+d.saljare1_name+'</span>':'')
+(d.region?'<span style="font-size:8px;background:#dbeafe;color:#1e40af;padding:0px 5px;border-radius:3px">'+d.region+'</span>':'')
+'</div>'
+(d.tilldelad_ue?'<div style="font-size:8px;color:#9a3412;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="'+d.tilldelad_ue+'">UE: '+d.tilldelad_ue+'</div>':'')
+'</div>';
}
var _dragDealId = null;
function pipelineDragStart(e, dealId) {
_dragDealId = dealId;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', dealId);
e.target.style.opacity = '0.5';
setTimeout(() => { if(e.target) e.target.style.opacity = '1'; }, 300);
}
function pipelineDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
async function pipelineDrop(e, newStage) {
e.preventDefault();
e.currentTarget.style.background = '#f8f9fa';
if (!_dragDealId) return;
const dealId = _dragDealId;
_dragDealId = null;
try {
const r = await fetch('api/deals.php?action=status', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({deal_id: dealId, status: newStage, staff_id: gStaffId})
});
const res = await r.json();
if (res.success) loadDealPipeline();
else alert(res.error || 'Kunde inte flytta');
} catch(err) { console.error('Drag drop error:', err); }
}
async function showDealDetail(id) {
try {
const r = await fetch('api/deals.php?id='+id+'&t='+Date.now());
const d = await r.json();
if (d.error) { alert(d.error); return; }
const products = (d.products||[]).map(p => PRODUCT_LABELS[p.product_type]||p.product_type).join(', ');
const contacts = (d.contact_methods||[]).map(c => {
var icon = c.type==='phone'?'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>':'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>';
return '<div style="display:flex;gap:6px;align-items:center;font-size:13px;color:#334155">'+icon+' <strong>'+c.value+'</strong>'+(c.label?' <span style="font-size:11px;color:#94a3b8">('+c.label+')</span>':'')+'</div>';
}).join('');
// Pipeline progress bar
const stages = ['offert','order','projektering','bestallning','leverans','montering','besiktning','fardigstall'];
const currentIdx = stages.indexOf(d.status);
const isNeg = ['anger','ej_godkand','avbrott'].includes(d.status);
const pipelineHtml = '<div style="display:flex;gap:3px;margin:0">' + stages.map((s,i) => {
const active = !isNeg && i <= currentIdx;
const color = active ? DEAL_STATUS_COLORS[s] : '#e5e7eb';
return '<div style="flex:1;text-align:center">'
+'<div style="height:8px;background:'+color+';border-radius:4px"></div>'
+'<div style="font-size:10px;color:'+(active?'#334155':'#cbd5e1')+';margin-top:4px;font-weight:'+(active?'600':'400')+'">'+DEAL_STATUS_LABELS[s]+'</div></div>';
}).join('') + '</div>';
// Activity timeline
var timelineItems = [];
if (d.datum_salj) timelineItems.push({date:d.datum_salj, label:'Sälj', color:'#eab308', icon:'S'});
if (d.projektering_bokad) timelineItems.push({date:d.projektering_bokad, label:'Projektering bokad', color:'#3b82f6', icon:'P'});
if (d.datum_fardigstall) timelineItems.push({date:d.datum_fardigstall, label:'Färdigställt', color:'#10b981', icon:'F'});
if (d.datum_avbrott) timelineItems.push({date:d.datum_avbrott, label:'Avbrott', color:'#ef4444', icon:'A'});
// Add status log entries
(d.status_log||[]).forEach(function(l) {
timelineItems.push({date:l.created_at, label:(DEAL_STATUS_LABELS[l.new_status]||l.new_status)+(l.changed_by_name?' (av '+l.changed_by_name+')':''), color:DEAL_STATUS_COLORS[l.new_status]||'#94a3b8', icon:'\u2192', fromLog:true});
});
timelineItems.sort(function(a,b){ return b.date.localeCompare(a.date); });
var timelineHtml = timelineItems.map(function(t){
return '<div style="display:flex;gap:12px;align-items:flex-start;padding:8px 0;border-bottom:1px solid #f1f5f9">'
+'<div style="min-width:36px;height:36px;border-radius:50%;background:'+t.color+'15;color:'+t.color+';display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;flex-shrink:0">'+t.icon+'</div>'
+'<div style="flex:1"><div style="font-size:13px;font-weight:600;color:#334155">'+t.label+'</div>'
+'<div style="font-size:11px;color:#94a3b8">'+t.date+'</div></div></div>';
}).join('');
// Section helper
function secTitle(t){ return '<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px;padding-bottom:4px;border-bottom:1px solid #f1f5f9">'+t+'</div>'; }
function infoRow(label,val,opts){ if(!val) return ''; opts=opts||{}; return '<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-size:13px"><span style="color:#64748b">'+label+'</span><span style="font-weight:'+(opts.bold?'700':'500')+';color:'+(opts.color||'#334155')+'">'+val+'</span></div>'; }
// Build the full overlay
var html = '<div style="position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto;backdrop-filter:blur(2px)" onclick="if(event.target===this)this.remove()" id="dealDetailOverlay">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:1100px;box-shadow:0 25px 80px rgba(0,0,0,.25);max-height:calc(100vh - 40px);display:flex;flex-direction:column">'
// Header
+'<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:flex-start;flex-shrink:0">'
+'<div style="flex:1">'
+'<div style="display:flex;align-items:center;gap:12px;margin-bottom:4px">'
+'<h2 style="font-size:22px;font-weight:800;color:#1a1a1a;margin:0">'+(d.deal_number||'Ny aff\u00e4r')+'</h2>'+(d.monday_item_id?'<a href="https://solargroup-unit.monday.com/boards/1844756189/pulses/'+d.monday_item_id+'" target="_blank" onclick="event.stopPropagation()" style="display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:8px;background:#6c3faa;color:#fff;font-size:11px;font-weight:600;text-decoration:none;white-space:nowrap" title="\u00d6ppna i Monday.com"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>Monday</a>':'')
+'<span style="padding:4px 14px;border-radius:20px;font-size:12px;font-weight:600;color:#fff;background:'+(isNeg?'#ef4444':DEAL_STATUS_COLORS[d.status]||'#94a3b8')+'">'+(DEAL_STATUS_LABELS[d.status]||d.status)+'</span>'
+(d.salj_status?'<span style="padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;color:#fff;background:'+(SALJ_STATUS_COLORS[d.salj_status]||'#c4c4c4')+'">'+(SALJ_STATUS_LABELS[d.salj_status]||d.salj_status)+'</span>':'')
+'</div>'
+'<div style="font-size:14px;color:#64748b">'+(d.customer_name||'')+(d.customer_name2?' & '+d.customer_name2:'')
+(d.property_address?' \u2014 '+d.property_address:'')+'</div>'
+'</div>'
+'<button onclick="document.getElementById(\'dealDetailOverlay\').remove()" style="background:none;border:none;font-size:28px;cursor:pointer;color:#94a3b8;padding:0 4px;line-height:1">×</button>'
+'</div>'
// Pipeline bar
+'<div style="padding:16px 28px;border-bottom:1px solid #f1f5f9;flex-shrink:0">'+pipelineHtml+'</div>'
// Content area - scrollable
+'<div style="overflow-y:auto;padding:20px 28px 28px;flex:1">'
// Summary cards row
+'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:20px">'
+(d.ordervarde_ink_moms?'<div style="background:linear-gradient(135deg,#024550,#036b78);border-radius:12px;padding:14px;color:#fff"><div style="font-size:10px;opacity:.7;text-transform:uppercase;letter-spacing:.5px">Orderv\u00e4rde</div><div style="font-size:20px;font-weight:800;margin-top:4px">'+fmtKr(parseFloat(d.ordervarde_ink_moms))+'</div></div>':'')
+(d.total_marginal_ink_moms?'<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Marginal</div><div style="font-size:20px;font-weight:800;color:#16a34a;margin-top:4px">'+fmtKr(parseFloat(d.total_marginal_ink_moms))+'</div></div>':'')
+(d.intakter?'<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Int\u00e4kter</div><div style="font-size:20px;font-weight:800;color:#16a34a;margin-top:4px">'+fmtKr(parseFloat(d.intakter))+'</div></div>':'')
+(d.kostnader?'<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px">Kostnader</div><div style="font-size:20px;font-weight:800;color:#ef4444;margin-top:4px">'+fmtKr(parseFloat(d.kostnader))+'</div></div>':'')
+(d.ue_pris?'<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px">UE-pris</div><div style="font-size:20px;font-weight:800;color:#9a3412;margin-top:4px">'+fmtKr(parseFloat(d.ue_pris))+'</div></div>':'')
+(d.ata?'<div style="background:#fefce8;border:1px solid #fde68a;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.5px">\u00c4TA</div><div style="font-size:20px;font-weight:800;color:#92400e;margin-top:4px">'+fmtKr(parseFloat(d.ata))+'</div></div>':'')
+'</div>'
// Three column grid
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px">'
// Column 1: Kund & Fastighet
+'<div>'
+secTitle('Kundinformation')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px">'
+'<div style="font-size:15px;font-weight:700;margin-bottom:6px">'+(d.customer_name||'')+'</div>'
+(d.personnummer?'<div style="font-size:12px;color:#64748b;margin-bottom:6px">Pnr: '+d.personnummer+'</div>':'')
+(d.customer_name2?'<div style="font-size:14px;font-weight:600;margin-top:8px">'+d.customer_name2+'</div>':'')
+(d.personnummer2?'<div style="font-size:12px;color:#64748b;margin-bottom:6px">Pnr: '+d.personnummer2+'</div>':'')
+'<div style="margin-top:8px;display:flex;flex-direction:column;gap:4px">'+contacts+'</div>'
+'</div>'
+secTitle('Fastighet')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px">'
+(d.property_address?'<div style="font-size:14px;font-weight:600;margin-bottom:4px">'+d.property_address+'</div>':'<div style="font-size:13px;color:#94a3b8">Ingen fastighet kopplad</div>')
+(d.property_city?'<div style="font-size:12px;color:#64748b;margin-bottom:6px">'+d.property_city+'</div>':'')
+infoRow('Beteckning',d.fastighetsbeteckning)
+infoRow('Huvuds\u00e4kring',d.huvudsakring?d.huvudsakring+' A':null)
+infoRow('Takmaterial',d.takmaterial)
+infoRow('Taktyp',d.taktyp)
+infoRow('N\u00e4t\u00e4gare',d.natagare)
+infoRow('Anl.id',d.anlaggningsid)
+'</div>'
+'</div>'
// Column 2: Statusar, Team, Produkt
+'<div>'
+secTitle('Statusar')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:6px">'
+infoRow('Status', DEAL_STATUS_LABELS[d.status]||d.status, {bold:true})
+infoRow('Status s\u00e4lj', d.salj_status?(SALJ_STATUS_LABELS[d.salj_status]||d.salj_status):null)
+infoRow('Status order', d.status_order, {bold:true, color:'#1e40af'})
+(d.avbrott_status?'<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 0"><span style="font-size:13px;color:#64748b">Avbrott</span><span style="font-size:11px;padding:3px 12px;border-radius:10px;color:#fff;background:#ef4444;font-weight:600">'+d.avbrott_status+'</span></div>':'')
+(d.ansvarig_avbrott?infoRow('Ansvarig avbrott',d.ansvarig_avbrott):'')
+(d.datum_avbrott?infoRow('Datum avbrott',d.datum_avbrott,{color:'#ef4444'}):'')
+infoRow('AP Status', d.ap_status)
+infoRow('AP Status 2', d.ap_status_2)
+infoRow('Region', d.region, {bold:true, color:'#1e40af'})
+infoRow('F\u00f6ranm\u00e4lan', d.foranmalan)
+'</div>'
+secTitle('S\u00e4ljare & Team')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px">'
+infoRow('S\u00e4ljare 1', d.saljare1_name, {bold:true})
+infoRow('S\u00e4ljare 2', d.saljare2_name)
+infoRow('Bokare', d.bokare_name)
+infoRow('Projekt\u00f6r', d.projektor_name)
+infoRow('Best\u00e4llare', d.bestallare_name)
+infoRow('M\u00f6tesbokare', d.motesbokare_name)
+'</div>'
+(products?secTitle('Produkter & Teknik')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px">'
+infoRow('Produkter', products, {bold:true})
+infoRow('Vad \u00e4r s\u00e5lt', d.vad_ar_salt)
+infoRow('Antal solceller', d.antal_solceller)
+infoRow('M\u00e4rke solceller', d.marke_solceller)
+infoRow('Batteri storlek', d.batteri_storlek)
+infoRow('M\u00e4rke batteri', d.marke_batteri)
+infoRow('Projektnummer', d.projektnummer)
+'</div>':'')
+'</div>'
// Column 3: Ekonomi, UE, Timeline
+'<div>'
+secTitle('Ekonomi')
+'<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px">'
+infoRow('Orderv\u00e4rde', d.ordervarde_ink_moms?fmtKr(parseFloat(d.ordervarde_ink_moms)):null, {bold:true, color:'#024550'})
+infoRow('Marginal', d.total_marginal_ink_moms?fmtKr(parseFloat(d.total_marginal_ink_moms)):null, {color:'#16a34a'})
+infoRow('Int\u00e4kter', d.intakter?fmtKr(parseFloat(d.intakter)):null, {bold:true, color:'#16a34a'})
+infoRow('Kostnader', d.kostnader?fmtKr(parseFloat(d.kostnader)):null, {color:'#ef4444'})
+infoRow('UE-pris', d.ue_pris?fmtKr(parseFloat(d.ue_pris)):null, {color:'#9a3412'})
+infoRow('\u00c4TA', d.ata?fmtKr(parseFloat(d.ata)):null, {color:'#92400e'})
+infoRow('GT', d.gt_belopp?fmtKr(parseFloat(d.gt_belopp)):null)
+infoRow('ROT', d.rot_belopp?fmtKr(parseFloat(d.rot_belopp)):null)
+infoRow('Finans via oss', d.finans_via_oss?fmtKr(parseFloat(d.finans_via_oss)):null)
+infoRow('Faktura', d.skapa_faktura)
+infoRow('Fakturastatus', d.faktura_status)
+infoRow('Betvillkor', d.betvillkor)
+'</div>'
+(d.tilldelad_ue?secTitle('Entrepren\u00f6r (UE)')
+'<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:14px">'
+'<div style="font-size:14px;font-weight:700;color:#9a3412;margin-bottom:4px">'+d.tilldelad_ue+'</div>'
+infoRow('UE-pris', d.ue_pris?fmtKr(parseFloat(d.ue_pris)):null, {color:'#9a3412',bold:true})
+'</div>':'')
+secTitle('Aktivitetslogg')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px;max-height:300px;overflow-y:auto">'
+(timelineHtml||'<div style="font-size:13px;color:#94a3b8;text-align:center;padding:16px 0">Ingen aktivitet loggad</div>')
+'</div>'
// Datum
+secTitle('Viktiga datum')
+'<div style="background:#f8f9fa;border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px">'
+infoRow('Datum s\u00e4lj', d.datum_salj, {bold:true})
+infoRow('Projektering bokad', d.projektering_bokad)
+infoRow('F\u00e4rdigst\u00e4llt', d.datum_fardigstall, {color:'#10b981',bold:true})
+infoRow('Avbrott', d.datum_avbrott, {color:'#ef4444'})
+'</div>'
+'</div>'
+'</div>' // end grid
// Status change buttons
+'<div style="margin-top:20px;padding-top:16px;border-top:1px solid #e5e7eb">'
+'<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">\u00c4ndra status</div>'
+'<div style="display:flex;gap:6px;flex-wrap:wrap">'
+stages.concat(['anger','ej_godkand','avbrott']).map(function(s){ return '<button onclick="changeDealStatus('+d.id+',"'+s+'")" style="padding:5px 12px;border-radius:8px;font-size:11px;font-weight:600;border:2px solid '+(d.status===s?DEAL_STATUS_COLORS[s]:'#e5e7eb')+';background:'+(d.status===s?DEAL_STATUS_COLORS[s]:'#fff')+';color:'+(d.status===s?'#fff':'#64748b')+';cursor:pointer;transition:all .15s" onmouseover="this.style.borderColor=\''+DEAL_STATUS_COLORS[s]+'\'" onmouseout="this.style.borderColor=\''+(d.status===s?DEAL_STATUS_COLORS[s]:'#e5e7eb')+'\'">'+(DEAL_STATUS_LABELS[s]||s)+'</button>'; }).join('')
+'</div></div>'
+'</div>' // end content area
+'</div></div>'; // end overlay
document.body.insertAdjacentHTML('beforeend', html);
} catch(e) { console.error('showDealDetail error:', e); }
}
async function changeDealStatus(dealId, newStatus) {
const user = JSON.parse(localStorage.getItem('solarUser')||'{}');
try {
const r = await fetch('api/deals.php?action=status', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ deal_id: dealId, status: newStatus, staff_id: user.id||0 })
});
const data = await r.json();
if (data.success) {
const overlay = document.getElementById('dealDetailOverlay');
if (overlay) overlay.remove();
showDealDetail(dealId);
loadDeals();
}
} catch(e) { console.error('changeDealStatus error:', e); }
}
/* ===== UNDERENTREPRENÖRER (UE) ===== */
var _ueData = null;
async function loadUEPage() {
try {
var r = await fetch('api/deals.php?action=contractors&t='+Date.now());
_ueData = await r.json();
var specs = {};
_ueData.forEach(function(c){ if(c.specialty) specs[c.specialty] = (specs[c.specialty]||0)+1; });
var specSel = document.getElementById('ueSpecFilter');
if (specSel) {
specSel.innerHTML = '<option value="">Alla specialiteter</option>' + Object.keys(specs).sort().map(function(s){ return '<option value="'+s+'">'+s+' ('+specs[s]+')</option>'; }).join('');
}
var totalContacts = _ueData.reduce(function(a,c){ return a+c.contacts.length; },0);
var totalActive = _ueData.reduce(function(a,c){ return a+(c.stats?c.stats.active_projects:0); },0);
var cntEl = document.getElementById('ueCount');
if(cntEl) cntEl.textContent = '('+_ueData.length+' f\u00f6retag, '+totalContacts+' kontakter, '+totalActive+' p\u00e5g\u00e5ende projekt)';
filterUE();
} catch(e) { console.error('loadUEPage error:', e); }
}
function filterUE() {
if(!_ueData) return;
var search = (document.getElementById('ueSearch')||{}).value||'';
search = search.toLowerCase();
var spec = (document.getElementById('ueSpecFilter')||{}).value||'';
var filtered = _ueData.filter(function(c){
if(spec && c.specialty !== spec) return false;
if(!search) return true;
if(c.name.toLowerCase().indexOf(search) >= 0) return true;
if((c.specialty||'').toLowerCase().indexOf(search) >= 0) return true;
return c.contacts.some(function(ct){
return (ct.contact_name||'').toLowerCase().indexOf(search) >= 0
|| (ct.phone||'').indexOf(search) >= 0
|| (ct.email||'').toLowerCase().indexOf(search) >= 0;
});
});
renderUECards(filtered);
}
function renderUECards(companies) {
var container = document.getElementById('ueContainer');
if(!container) return;
if(!companies.length) { container.innerHTML = '<div style="text-align:center;color:#94a3b8;padding:40px;font-size:14px">Inga entrepren\u00f6rer hittades</div>'; return; }
container.innerHTML = '<div style="display:flex;flex-direction:column;gap:12px">'
+ companies.map(function(c, idx){
var specColor = '#64748b';
if((c.specialty||'').indexOf('elektriker')>=0) specColor = '#eab308';
else if((c.specialty||'').indexOf('tak')>=0 || (c.specialty||'').indexOf('sol')>=0) specColor = '#f97316';
else if((c.specialty||'').indexOf('f\u00f6nster')>=0) specColor = '#3b82f6';
else if((c.specialty||'').indexOf('pl\u00e5t')>=0) specColor = '#6b7280';
else if((c.specialty||'').indexOf('st\u00e4llning')>=0) specColor = '#8b5cf6';
var st = c.stats || {};
var hasProjects = st.total_projects > 0;
var safeName = c.name.replace(/'/g,"\\'").replace(/"/g,'"');
return '<div id="ue-card-'+idx+'" style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)">'
// Header - klickbar
+'<div style="padding:14px 20px;display:flex;align-items:center;justify-content:space-between;cursor:pointer" onclick="toggleUECard('+idx+',\''+safeName+'\')">'
+'<div style="flex:1;min-width:0">'
+'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">'
+'<div style="width:40px;height:40px;border-radius:10px;background:'+specColor+'15;color:'+specColor+';display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:800;flex-shrink:0">'+c.name.charAt(0)+'</div>'
+'<div>'
+'<div style="font-size:16px;font-weight:700;color:#1a1a1a">'+c.name+'</div>'
+'<div style="display:flex;align-items:center;gap:8px;margin-top:2px">'
+(c.specialty?'<span style="font-size:11px;font-weight:600;padding:2px 10px;border-radius:10px;background:'+specColor+'15;color:'+specColor+';border:1px solid '+specColor+'30">'+c.specialty+'</span>':'')
+'<span style="font-size:11px;color:#64748b">'+c.contacts.length+' kontakter</span>'
+'</div></div></div>'
+'<div style="display:flex;align-items:center;gap:8px">'
+(hasProjects?'<span style="font-size:11px;font-weight:700;padding:3px 10px;border-radius:10px;background:#16a34a;color:#fff">'+st.active_projects+' p\u00e5g\u00e5ende</span>':'')
+(hasProjects?'<span style="font-size:10px;color:#64748b">'+st.total_projects+' tot \u00b7 '+st.completed_projects+' klara \u00b7 '+st.cancelled_projects+' avbr</span>':'<span style="font-size:10px;color:#94a3b8">Inga projekt</span>')
+'</div></div>'
+'<svg id="ue-arrow-'+idx+'" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" style="flex-shrink:0;transition:transform .2s"><polyline points="6 9 12 15 18 9"/></svg>'
+'</div>'
// Expanderbar body
+'<div id="ue-body-'+idx+'" style="max-height:0;overflow:hidden;transition:max-height .4s ease">'
+'<div style="border-top:1px solid #e5e7eb">'
// Kontakter
+'<div style="padding:12px 20px">'
+'<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Kontakter</div>'
+'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:8px">'
+ c.contacts.map(function(ct){
return '<div style="display:flex;align-items:center;gap:10px;padding:8px 12px;background:#f8f9fa;border-radius:8px">'
+'<div style="width:32px;height:32px;border-radius:50%;background:#e2e8f0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;color:#64748b;flex-shrink:0">'+((ct.contact_name||'?').charAt(0))+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:600;color:#334155">'+(ct.contact_name||'Ok\u00e4nd')+'</div>'
+(ct.contact_title?'<div style="font-size:11px;color:#94a3b8">'+ct.contact_title+'</div>':'')
+'</div>'
+'<div style="display:flex;gap:4px;flex-shrink:0">'
+(ct.phone?'<a href="tel:'+ct.phone+'" onclick="event.stopPropagation()" style="display:flex;align-items:center;gap:3px;padding:3px 8px;border-radius:6px;background:#f0fdf4;color:#16a34a;text-decoration:none;font-size:11px;font-weight:600;border:1px solid #86efac"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>'+ct.phone+'</a>':'')
+(ct.email?'<a href="mailto:'+ct.email+'" onclick="event.stopPropagation()" style="display:flex;align-items:center;gap:3px;padding:3px 8px;border-radius:6px;background:#eff6ff;color:#3b82f6;text-decoration:none;font-size:11px;font-weight:600;border:1px solid #93c5fd"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>'+ct.email+'</a>':'')
+'</div></div>';
}).join('')
+'</div></div>'
// Projekt-sektion (laddas vid expand)
+'<div id="ue-projects-'+idx+'" style="padding:0 20px 16px">'
+'<div style="text-align:center;padding:20px;color:#94a3b8;font-size:13px">Laddar projekt...</div>'
+'</div>'
+'</div></div></div>';
}).join('')
+'</div>';
}
var _ueExpandedProjects = {};
async function toggleUECard(idx, ueName) {
var body = document.getElementById('ue-body-'+idx);
var arrow = document.getElementById('ue-arrow-'+idx);
if (!body) return;
var isOpen = body.style.maxHeight && body.style.maxHeight !== '0px';
if (isOpen) {
body.style.maxHeight = '0px';
if(arrow) arrow.style.transform = 'rotate(0deg)';
} else {
body.style.maxHeight = '2000px';
if(arrow) arrow.style.transform = 'rotate(180deg)';
// Ladda projekt om inte redan laddat
if (!_ueExpandedProjects[ueName]) {
_ueExpandedProjects[ueName] = true;
loadUECardProjects(idx, ueName);
}
}
}
async function loadUECardProjects(idx, ueName) {
var container = document.getElementById('ue-projects-'+idx);
if (!container) return;
try {
var r = await fetch('api/deals.php?action=ue_projects&ue='+encodeURIComponent(ueName)+'&t='+Date.now());
var projects = await r.json();
if (projects.error) { container.innerHTML = '<div style="color:#ef4444;padding:8px;font-size:13px">'+projects.error+'</div>'; return; }
if (!projects.length) { container.innerHTML = '<div style="padding:12px 0;color:#94a3b8;font-size:13px">Inga projekt hittade f\u00f6r denna UE</div>'; return; }
var activeStatuses = ['Att best\u00e4lla','Att boka','Att projektera','Bokat','Leveransbevakning','Ny order','Projektering bokad','P\u00e5g\u00e5ende','Support / Fels\u00f6kning'];
var active = projects.filter(function(p){ return activeStatuses.indexOf(p.group_title)>=0; });
var completed = projects.filter(function(p){ return p.group_title==='F\u00e4rdigst\u00e4llt'; });
var cancelled = projects.filter(function(p){ return ['Avbrott','Avbrott f\u00e4rdigst\u00e4llt','Avbrott ny order / projektering','\u00c5nger'].indexOf(p.group_title)>=0; });
var other = projects.filter(function(p){ return active.indexOf(p)<0 && completed.indexOf(p)<0 && cancelled.indexOf(p)<0; });
if(other.length) active = active.concat(other);
function statusColor(gt) {
if(activeStatuses.indexOf(gt)>=0) return '#16a34a';
if(gt==='F\u00e4rdigst\u00e4llt') return '#3b82f6';
return '#ef4444';
}
function projSection(rows, label, color, defaultOpen) {
if(!rows.length) return '';
var totalVal = rows.reduce(function(a,p){ return a+parseFloat(p.ordervarde_ink_moms||0); },0);
return '<div style="margin-bottom:12px">'
+'<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer" onclick="var t=this.nextElementSibling;t.style.display=t.style.display===\'none\'?\'block\':\'none\';this.querySelector(\'svg\').style.transform=t.style.display===\'none\'?\'rotate(-90deg)\':\'rotate(0deg)\'">'
+'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="'+color+'" stroke-width="2.5" style="transition:transform .2s;transform:'+(defaultOpen?'rotate(0deg)':'rotate(-90deg)')+'"><polyline points="6 9 12 15 18 9"/></svg>'
+'<span style="font-size:12px;font-weight:700;color:'+color+'">'+label+'</span>'
+'<span style="font-size:11px;font-weight:600;background:'+color+';color:#fff;padding:1px 8px;border-radius:10px">'+rows.length+'</span>'
+(totalVal?'<span style="font-size:11px;color:#64748b;margin-left:auto">'+fmtKr(totalVal)+'</span>':'')
+'</div>'
+'<div style="display:'+(defaultOpen?'block':'none')+'">'
+'<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f8f9fa">'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Aff\u00e4rsnr</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Kund</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Adress</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Status</th>'
+'<th style="padding:6px 8px;text-align:right;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Orderv\u00e4rde</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Region</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb;font-weight:600;color:#64748b">Datum</th>'
+'</tr></thead><tbody>'
+rows.map(function(p){
var sc = statusColor(p.group_title);
return '<tr style="cursor:pointer;transition:background .1s" onmouseover="this.style.background=\'#f8fafc\'" onmouseout="this.style.background=\'#fff\'" onclick="'+(p.deal_id?'showDealDetail('+p.deal_id+')':'')+'">'
+'<td style="padding:6px 8px;font-weight:600;color:#024550;border-bottom:1px solid #f1f5f9">'+(p.deal_number||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9">'+(p.customer_name||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(p.property_address||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9"><span style="font-size:10px;padding:2px 8px;border-radius:8px;color:#fff;background:'+sc+';white-space:nowrap">'+(p.group_title||'-')+'</span></td>'
+'<td style="padding:6px 8px;text-align:right;border-bottom:1px solid #f1f5f9;font-weight:600">'+(p.ordervarde_ink_moms?fmtKr(parseFloat(p.ordervarde_ink_moms)):'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9">'+(p.region||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;color:#64748b">'+(p.datum_salj||'-')+'</td>'
+'</tr>';
}).join('')
+'</tbody></table></div></div>';
}
container.innerHTML = '<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;padding-top:4px;border-top:1px solid #f1f5f9">Projekt ('+projects.length+')</div>'
+ projSection(active, 'P\u00e5g\u00e5ende', '#16a34a', true)
+ projSection(completed, 'F\u00e4rdigst\u00e4llda', '#3b82f6', false)
+ projSection(cancelled, 'Avbrutna', '#ef4444', false);
// Uppdatera max-height efter innehåll laddats
var body = document.getElementById('ue-body-'+idx);
if(body) body.style.maxHeight = body.scrollHeight + 'px';
} catch(e) {
container.innerHTML = '<div style="color:#ef4444;padding:8px;font-size:13px">Fel vid laddning: '+e.message+'</div>';
}
}
// ============ INKÖP & LEVERANTÖRER ============
var _inkopOrders = [];
var _inkopSuppliers = [];
const INKOP_STATUS_LABELS = {draft:'Utkast',ordered:'Beställd',confirmed:'Bekräftad',shipped:'Skickad',delivered:'Levererad',cancelled:'Avbruten'};
const INKOP_STATUS_COLORS = {draft:'#94a3b8',ordered:'#3b82f6',confirmed:'#8b5cf6',shipped:'#f97316',delivered:'#16a34a',cancelled:'#ef4444'};
var _inkopCurrentTab = 'deals';
var _inkopDealsDT = null;
var _inkopOrdersDT = null;
var _inkopDealsData = [];
var _inkopSelectedDeals = {};
function switchInkopTab(tab) {
_inkopCurrentTab = tab;
var tab1 = document.getElementById('inkopTab1');
var tab2 = document.getElementById('inkopTab2');
var deals = document.getElementById('inkopDealsTab');
var orders = document.getElementById('inkopOrdersTab');
if(tab === 'deals') {
tab1.style.borderBottomColor = '#024550'; tab1.style.color = '#024550'; tab1.style.fontWeight = '700';
tab2.style.borderBottomColor = 'transparent'; tab2.style.color = '#94a3b8'; tab2.style.fontWeight = '600';
deals.style.display = ''; orders.style.display = 'none';
loadInkopDeals();
} else {
tab2.style.borderBottomColor = '#024550'; tab2.style.color = '#024550'; tab2.style.fontWeight = '700';
tab1.style.borderBottomColor = 'transparent'; tab1.style.color = '#94a3b8'; tab1.style.fontWeight = '600';
orders.style.display = ''; deals.style.display = 'none';
loadInkopOrders();
}
}
async function loadInkopPage() {
try {
var suppR = await fetch('api/suppliers.php?action=list&t='+Date.now());
_inkopSuppliers = await suppR.json();
// Populate supplier filter on orders tab
var sf = document.getElementById('inkopOrderSupplier');
if(sf) sf.innerHTML = '<option value="">Alla leverantörer</option>' + _inkopSuppliers.map(function(s){ return '<option value="'+s.id+'">'+s.name+'</option>'; }).join('');
switchInkopTab(_inkopCurrentTab);
} catch(e) { console.error('loadInkopPage error:', e); }
}
async function loadInkopDeals() {
try {
var pt = (document.getElementById('inkopProductType')||{}).value||'';
var ds = (document.getElementById('inkopDealStatus')||{}).value||'';
var pf = (document.getElementById('inkopPoFilter')||{}).value||'';
var url = 'api/suppliers.php?action=deals_for_purchase&t='+Date.now();
if(pt) url += '&product_type='+encodeURIComponent(pt);
if(ds) url += '&deal_status='+encodeURIComponent(ds);
if(pf) url += '&po_filter='+encodeURIComponent(pf);
var r = await fetch(url);
_inkopDealsData = await r.json();
_inkopSelectedDeals = {};
renderInkopDeals();
renderInkopDealsStats();
} catch(e) { console.error('loadInkopDeals error:', e); }
}
function renderInkopDealsStats() {
var el = document.getElementById('inkopDealsStats');
if(!el) return;
var total = _inkopDealsData.length;
var withPo = _inkopDealsData.filter(function(d){ return parseInt(d.po_count||0) > 0; }).length;
var withoutPo = total - withPo;
var totalVal = _inkopDealsData.reduce(function(a,d){ return a + parseFloat(d.ordervarde_ink_moms||0); },0);
el.innerHTML = '<div style="background:linear-gradient(135deg,#024550,#036b78);border-radius:12px;padding:14px;color:#fff"><div style="font-size:10px;opacity:.7;text-transform:uppercase;letter-spacing:.5px">Totalt affärer</div><div style="font-size:20px;font-weight:800;margin-top:4px">'+total+'</div></div>'
+'<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Utan inköpsorder</div><div style="font-size:20px;font-weight:800;color:#92400e;margin-top:4px">'+withoutPo+'</div></div>'
+'<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Med inköpsorder</div><div style="font-size:20px;font-weight:800;color:#16a34a;margin-top:4px">'+withPo+'</div></div>'
+'<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Ordervärde</div><div style="font-size:20px;font-weight:800;color:#334155;margin-top:4px">'+fmtKr(totalVal)+'</div></div>';
}
function renderInkopDeals() {
if(_inkopDealsDT) { _inkopDealsDT.destroy(); _inkopDealsDT = null; }
var tb = document.getElementById('inkopDealsBody');
if(!tb) return;
tb.innerHTML = _inkopDealsData.map(function(d){
var types = (d.product_types||'').split(',').map(function(t){ return PRODUCT_LABELS[t]||t; }).join(', ');
var dsc = DEAL_STATUS_COLORS[d.deal_status]||'#94a3b8';
var pos = d.purchase_orders||[];
var poHtml = pos.map(function(po){
var psc = INKOP_STATUS_COLORS[po.status]||'#16a34a';
return '<span class="po-badge" data-poid="'+po.id+'" style="cursor:pointer;font-size:10px;padding:2px 8px;border-radius:8px;color:#fff;background:'+psc+';font-weight:600;white-space:nowrap;display:inline-block;margin:1px">'+(po.supplier_name||po.order_number||'PO')+'</span>';
}).join(' ');
poHtml += ' <a href="javascript:void(0)" class="po-create-btn" data-dealid="'+d.deal_id+'" style="padding:3px 10px;border-radius:8px;border:1px solid #024550;background:#fff;color:#024550;font-size:10px;font-weight:600;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-block">+ Inköp</a>';
// Parse product antal from product_details
var antalInfo = '';
if(d.product_details) {
d.product_details.split('|').forEach(function(pd){
var parts = pd.split(':');
var antal = parseInt(parts[1])||0;
var modell = parts[2]||'';
if(antal > 0) antalInfo += (antalInfo?', ':'') + antal + ' ' + (PRODUCT_LABELS[parts[0]]||parts[0]) + (modell?' ('+modell+')':'');
});
}
var measBadge = '';
if(d.measurement_count > 0) measBadge = '<span style="font-size:9px;padding:2px 6px;border-radius:6px;background:#dcfce7;color:#16a34a;font-weight:600;margin-left:4px">Inmätt</span>';
var chk = '<input type="checkbox" class="deal-chk" data-dealid="'+d.deal_id+'" '+((_inkopSelectedDeals[d.deal_id])?'checked ':'')+' style="width:16px;height:16px;cursor:pointer">';
return '<tr>'
+'<td style="text-align:center;width:30px">'+chk+'</td>'
+'<td style="font-weight:600;color:#024550;white-space:nowrap">'+(d.deal_number||'-')+'</td>'
+'<td style="white-space:nowrap">'+(d.customer_name||'-')+'</td>'
+'<td style="font-size:12px;color:#64748b;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+(d.property_address||d.lev_adress||'-')+'</td>'
+'<td style="font-size:11px;white-space:nowrap">'+(antalInfo||types)+measBadge+'</td>'
+'<td style="color:#64748b;white-space:nowrap">'+(d.datum_salj||'-')+'</td>'
+'<td><span style="font-size:11px;padding:3px 10px;border-radius:10px;color:#fff;background:'+dsc+';font-weight:600;white-space:nowrap">'+(DEAL_STATUS_LABELS[d.deal_status]||d.deal_status)+'</span></td>'
+'<td style="text-align:right;font-weight:600;white-space:nowrap">'+(d.ordervarde_ink_moms?fmtKr(parseFloat(d.ordervarde_ink_moms)):'-')+'</td>'
+'<td style="white-space:nowrap">'+poHtml+'</td>'
+'</tr>';
}).join('');
_inkopDealsDT = $('#inkopDealsDT').DataTable({
paging: true, pageLength: 50, order: [[5,'asc']],
language: { search:'Sök:', lengthMenu:'Visa _MENU_', info:'_START_-_END_ av _TOTAL_', paginate:{previous:'Förra',next:'Nästa'}, zeroRecords:'Inga affärer hittades' },
columnDefs: [{ targets:[0], orderable:false, width:'30px' }, { targets:[7], className:'dt-right' }, { targets:[8], orderable:false, width:'200px' }]
});
// Event delegation for DataTable buttons
$('#inkopDealsDT').off('click','.po-create-btn').on('click', '.po-create-btn', function(e){
e.preventDefault(); e.stopPropagation();
var did = $(this).data('dealid');
var d = _inkopDealsData.find(function(x){ return x.deal_id == did; });
if(d) showCreatePoForDeal(d.deal_id, encodeURIComponent(d.customer_name||''), encodeURIComponent(d.deal_number||''));
return false;
});
$('#inkopDealsDT').off('click','.po-badge').on('click', '.po-badge', function(e){
e.preventDefault(); e.stopPropagation();
showOrderDetail($(this).data('poid'));
return false;
});
$('#inkopDealsDT').off('click','.deal-chk').on('click', '.deal-chk', function(e){
e.stopPropagation();
toggleDealSelect($(this).data('dealid'));
});
updateBatchBar();
}
function toggleDealSelect(dealId) {
if(_inkopSelectedDeals[dealId]) delete _inkopSelectedDeals[dealId];
else _inkopSelectedDeals[dealId] = true;
updateBatchBar();
}
function toggleAllDealSelect(checked) {
_inkopSelectedDeals = {};
if(checked) {
_inkopDealsData.forEach(function(d){ if(!d.po_id) _inkopSelectedDeals[d.deal_id] = true; });
}
renderInkopDeals();
}
function updateBatchBar() {
var existing = document.getElementById('inkopBatchBar');
var count = Object.keys(_inkopSelectedDeals).length;
if(count === 0) { if(existing) existing.remove(); return; }
var html = '<div id="inkopBatchBar" style="position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#024550;color:#fff;padding:12px 24px;border-radius:14px;box-shadow:0 8px 30px rgba(0,0,0,.3);display:flex;align-items:center;gap:16px;z-index:9999;font-size:14px">'
+'<span style="font-weight:700">'+count+' affärer valda</span>'
+'<button type="button" onclick="createBatchPo()" style="padding:8px 20px;border-radius:8px;border:none;background:#fff;color:#024550;font-size:13px;font-weight:700;cursor:pointer">Skapa batch-order</button>'
+'<button type="button" onclick="_inkopSelectedDeals={};renderInkopDeals()" style="background:none;border:1px solid rgba(255,255,255,.3);padding:6px 14px;border-radius:8px;color:#fff;font-size:12px;cursor:pointer">Avmarkera</button>'
+'</div>';
if(existing) existing.remove();
document.body.insertAdjacentHTML('beforeend', html);
}
var _poFormDealId = null;
var _poFormData = {};
var _poFormPositions = [];
var _hedlundsArticles = [];
async function showCreatePoForDeal(dealId, encName, encDeal) {
_poFormDealId = dealId;
// Remove old overlay if exists
var old = document.getElementById('poFormOverlay');
if(old) old.remove();
// Load Hedlunds articles if not cached
if(!_hedlundsArticles.length) {
try {
var hedSup = (_inkopSuppliers||[]).find(function(s){ return s.name && s.name.toLowerCase().indexOf('hedlund')>=0; });
if(hedSup) {
var hr = await fetch('api/suppliers.php?action=articles&supplier_id='+hedSup.id+'&t='+Date.now());
var hd = await hr.json();
_hedlundsArticles = hd.articles || hd || [];
}
} catch(e){ console.warn('Could not load Hedlunds articles',e); }
}
// Load tillbehör formulas if not cached
if(!_tillbehorFormulas.length) {
try {
var tfr = await fetch('api/suppliers.php?action=tillbehor_formulas&t='+Date.now());
_tillbehorFormulas = await tfr.json();
} catch(e){ console.warn('Could not load tillbehor formulas',e); }
}
// Get deal details
var d = _inkopDealsData.find(function(x){ return x.deal_id == dealId; });
var name = d ? d.customer_name : decodeURIComponent(encName);
var dealNum = d ? d.deal_number : decodeURIComponent(encDeal);
var types = d ? (d.product_types||'').split(',').map(function(t){ return PRODUCT_LABELS[t]||t; }).join(', ') : '';
var addr = d ? (d.property_address||d.lev_adress||'') : '';
var suppOpts = (_inkopSuppliers||[]).map(function(s){ return '<option value="'+s.id+'">'+s.name+(s.category?' ('+s.category+')':'')+'</option>'; }).join('');
// Load existing POs for this deal
var existingPOs = (d && d.purchase_orders) ? d.purchase_orders : [];
// Load saved measurements for this deal
var savedMeasurements = [];
try {
var mr = await fetch('api/suppliers.php?action=get_measurements&deal_id='+dealId+'&t='+Date.now());
var md = await mr.json();
savedMeasurements = md.measurements || [];
} catch(e){ console.warn('Could not load measurements',e); }
// Parse window info from local product_details
var mondayWindowInfo = {pvc:0, tra:0, traalu:0, total:0, details:''};
if(d && d.product_details) {
d.product_details.split('|').forEach(function(pd){
var parts = pd.split(':');
if(parts[0] === 'fonster') {
var antal = parseInt(parts[1])||0;
var modell = parts[2]||'';
mondayWindowInfo.total = antal;
mondayWindowInfo.details = modell;
// Parse "14 Trä/Alu" or "6 PVC" or "4 Trä, 6 PVC"
(modell||'').split(',').forEach(function(m){
m = m.trim();
var match = m.match(/(\d+)\s+(.*)/);
if(match) {
var n = parseInt(match[1]);
var t = match[2].trim().toLowerCase();
if(t.indexOf('pvc')>=0) mondayWindowInfo.pvc += n;
else if(t.indexOf('trä/alu')>=0 || t.indexOf('tra/alu')>=0) mondayWindowInfo.traalu += n;
else if(t.indexOf('trä')>=0 || t.indexOf('tra')>=0) mondayWindowInfo.tra += n;
}
});
}
});
}
_poFormPositions = [];
_poHedlundsRows = [];
if(savedMeasurements.length) {
savedMeasurements.forEach(function(m,i){
_poFormPositions.push({pos:m.pos||i+1, rum:m.rum||'', vaning:m.vaning||'', product:m.product||'', width:m.width||'', height:m.height||'', pcs:m.pcs||1, model:m.model||'', color_in:m.color_in||'', color_out:m.color_out||'', profile:m.profile||'', type:m.type||'', side:m.side||'', view:m.view_dir||'', glass:m.glass||'', sunprotection:m.sunprotection||'', mullion:m.mullion||'', ventilation:m.ventilation||''});
});
var startPad = _poFormPositions.length;
for(var i=startPad;i<Math.max(20,startPad+5);i++) _poFormPositions.push({pos:i+1, rum:'',vaning:'',product:'',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''});
} else if(mondayWindowInfo.total > 0) {
// Pre-fill from Monday window counts
var pos = 1;
for(var i=0;i<mondayWindowInfo.pvc;i++) { _poFormPositions.push({pos:pos++, rum:'',vaning:'',product:'Fönster',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'REHAU Euro 70 Slim',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''}); }
for(var i=0;i<mondayWindowInfo.tra;i++) { _poFormPositions.push({pos:pos++, rum:'',vaning:'',product:'Fönster',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'Nordic Design Plus',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''}); }
for(var i=0;i<mondayWindowInfo.traalu;i++) { _poFormPositions.push({pos:pos++, rum:'',vaning:'',product:'Fönster',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'Brilliant Profile',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''}); }
// Pad extra rows
for(var i=0;i<5;i++) _poFormPositions.push({pos:pos++, rum:'',vaning:'',product:'',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''});
} else {
for(var i=0;i<20;i++) _poFormPositions.push({pos:i+1, rum:'',vaning:'',product:'',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''});
}
var sectionStyle = 'background:#fff;border-radius:12px;padding:20px 24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid #e5e7eb';
var labelStyle = 'font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px;text-transform:uppercase;letter-spacing:.5px';
var inputStyle = 'width:100%;padding:7px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box';
var headingStyle = 'font-size:15px;font-weight:700;color:#024550;margin:0 0 12px;display:flex;align-items:center;gap:8px';
// Build existing orders tab content
var existingHtml = '';
if(existingPOs.length) {
existingHtml = '<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'">Befintliga inköpsordrar ('+existingPOs.length+')</h3>'
+'<table style="width:100%;border-collapse:collapse;font-size:13px">'
+'<thead><tr style="background:#f1f5f9"><th style="padding:8px;border:1px solid #e2e8f0;text-align:left">Order</th><th style="padding:8px;border:1px solid #e2e8f0;text-align:left">Leverantör</th><th style="padding:8px;border:1px solid #e2e8f0;text-align:left">Status</th><th style="padding:8px;border:1px solid #e2e8f0;text-align:left">Datum</th><th style="padding:8px;border:1px solid #e2e8f0"></th></tr></thead><tbody>';
existingPOs.forEach(function(po){
var sc = INKOP_STATUS_COLORS[po.status]||'#94a3b8';
existingHtml += '<tr>'
+'<td style="padding:8px;border:1px solid #e2e8f0;font-weight:600">'+(po.order_number||'-')+'</td>'
+'<td style="padding:8px;border:1px solid #e2e8f0">'+(po.supplier_name||'-')+'</td>'
+'<td style="padding:8px;border:1px solid #e2e8f0"><span style="padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600;color:#fff;background:'+sc+'">'+(INKOP_STATUS_LABELS[po.status]||po.status)+'</span></td>'
+'<td style="padding:8px;border:1px solid #e2e8f0">'+(po.order_date||'-')+'</td>'
+'<td style="padding:8px;border:1px solid #e2e8f0"><button type="button" onclick="closePoForm();showOrderDetail('+po.id+')" style="padding:4px 12px;border-radius:6px;border:1px solid #e5e7eb;background:#fff;font-size:11px;cursor:pointer">Visa</button></td>'
+'</tr>';
});
existingHtml += '</tbody></table></div>';
} else {
existingHtml = '<div style="'+sectionStyle+';text-align:center;padding:40px;color:#94a3b8"><p style="font-size:14px;margin:0">Inga befintliga inköpsordrar för denna affär</p></div>';
}
var tabActive = 'padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:700;border:1px solid #e5e7eb;border-bottom:2px solid #fff;background:#fff;color:#024550;cursor:pointer;margin-bottom:-1px';
var tabInactive = 'padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;border:1px solid transparent;background:transparent;color:#94a3b8;cursor:pointer';
var html = ''
// Header
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">'
+'<div>'
+'<h1 class="page-title" style="margin-bottom:2px">Materialorder - Fönster</h1>'
+'<p class="page-subtitle" style="margin:0">'+dealNum+' - '+name+'</p>'
+'</div>'
+'<div style="display:flex;gap:8px">'
+'<button type="button" onclick="saveMeasurements(false)" style="padding:10px 24px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:#16a34a;color:#fff;cursor:pointer">Spara inmätning</button>'
+'<button type="button" onclick="savePoForm()" style="padding:10px 24px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:linear-gradient(135deg,#024550,#036b78);color:#fff;cursor:pointer">Skapa inköpsorder</button>'
+'<button type="button" onclick="closePoForm()" style="padding:10px 20px;border-radius:10px;font-size:13px;font-weight:600;border:1px solid #e5e7eb;background:#fff;color:#334155;cursor:pointer">Tillbaka</button>'
+'</div></div>'
// Tabs
+'<div style="display:flex;gap:0;border-bottom:1px solid #e5e7eb;margin-bottom:16px">'
+'<button type="button" id="poTabNew" onclick="switchPoTab(\'new\')" style="'+tabActive+'">Ny inmätning</button>'
+'<button type="button" id="poTabExisting" onclick="switchPoTab(\'existing\')" style="'+tabInactive+'">Befintliga ordrar'+(existingPOs.length?' ('+existingPOs.length+')':'')+'</button>'
+'</div>'
// Existing orders tab
+'<div id="poTabContentExisting" style="display:none">'+existingHtml+'</div>'
// New measurement tab
+'<div id="poTabContentNew">'
// Order summary from Monday
+(mondayWindowInfo.total > 0 ?
'<div style="'+sectionStyle+';background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-color:#bae6fd">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0284c7" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg><span style="color:#0284c7">Orderinfo</span></h3>'
+'<div style="display:flex;gap:24px;flex-wrap:wrap">'
+(mondayWindowInfo.pvc ? '<div style="background:#fff;border-radius:8px;padding:10px 16px;border:1px solid #e0f2fe"><div style="font-size:11px;color:#64748b;text-transform:uppercase">PVC Fönster/Dörrar</div><div style="font-size:22px;font-weight:800;color:#0284c7">'+mondayWindowInfo.pvc+'</div></div>' : '')
+(mondayWindowInfo.tra ? '<div style="background:#fff;border-radius:8px;padding:10px 16px;border:1px solid #e0f2fe"><div style="font-size:11px;color:#64748b;text-transform:uppercase">Trä Fönster</div><div style="font-size:22px;font-weight:800;color:#0284c7">'+mondayWindowInfo.tra+'</div></div>' : '')
+(mondayWindowInfo.traalu ? '<div style="background:#fff;border-radius:8px;padding:10px 16px;border:1px solid #e0f2fe"><div style="font-size:11px;color:#64748b;text-transform:uppercase">Trä/Alu Fönster</div><div style="font-size:22px;font-weight:800;color:#0284c7">'+mondayWindowInfo.traalu+'</div></div>' : '')
+'<div style="background:#fff;border-radius:8px;padding:10px 16px;border:1px solid #e0f2fe"><div style="font-size:11px;color:#64748b;text-transform:uppercase">Totalt</div><div style="font-size:22px;font-weight:800;color:#024550">'+mondayWindowInfo.total+' st</div></div>'
+'</div>'
+(savedMeasurements.length ? '<div style="margin-top:8px;font-size:12px;color:#16a34a;font-weight:600">✓ Inmätning sparad ('+savedMeasurements.length+' positioner)</div>' : '<div style="margin-top:8px;font-size:12px;color:#f59e0b;font-weight:600">⚠ Inmätning ej påbörjad — fyll i mått nedan</div>')
+'</div>' : '')
// Block 1: KUNDUPPGIFTER
+'<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>Kunduppgifter</h3>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">'
+'<div><label style="'+labelStyle+'">Namn</label><input id="poKundNamn" value="'+escHtml(name)+'" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Adress</label><input id="poKundAdress" value="'+escHtml(addr)+'" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Postadress</label><input id="poKundPost" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Stad</label><input id="poKundStad" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Telefon</label><input id="poKundTel" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Ordernummer</label><input id="poKundOrder" value="'+escHtml(dealNum)+'" style="'+inputStyle+'" readonly></div>'
+'<div><label style="'+labelStyle+'">Montering</label><input id="poKundMontering" type="date" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Byggnadsår</label><input id="poKundByggnar" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Fastighetsbeteckning</label><input id="poKundFast" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Husets fasad</label><input id="poKundFasad" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Säljare</label><input id="poKundSaljare" value="'+(d&&d.saljare_name?escHtml(d.saljare_name):'')+'" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Kapmån (mm)</label><input id="poKundKapman" value="20" type="number" style="'+inputStyle+'"></div>'
+'</div>'
+'<div style="margin-top:8px"><label style="display:flex;align-items:center;gap:6px;font-size:12px;color:#64748b;cursor:pointer"><input type="checkbox" id="poKundByggavfall"> Frakt av byggavfall</label></div>'
+'</div>'
// Inner tabs: Inmätning | Tillbehör | Leverantör
+'<div style="display:flex;gap:0;margin-bottom:0">'
+'<button type="button" id="poInnerTab1" onclick="switchPoInnerTab(1)" style="padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:700;border:1px solid #e5e7eb;border-bottom:2px solid #fff;background:#fff;color:#024550;cursor:pointer;margin-bottom:-1px">Inmätning</button>'
+'<button type="button" id="poInnerTab2" onclick="switchPoInnerTab(2)" style="padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;border:1px solid transparent;background:transparent;color:#94a3b8;cursor:pointer">Tillbehör</button>'
+'<button type="button" id="poInnerTab3" onclick="switchPoInnerTab(3)" style="padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;border:1px solid transparent;background:transparent;color:#94a3b8;cursor:pointer">Leverantörsorder</button>'
+'</div>'
+'<div style="border-top:1px solid #e5e7eb"></div>'
// === INNER TAB 1: INMÄTNING ===
+'<div id="poInnerContent1" style="margin-top:16px">'
+'<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>Inmätning</h3>'
+'<div style="display:flex;gap:16px;margin-bottom:10px">'
+'<div style="font-size:12px;color:#64748b">Antal Fönster: <strong id="poCountFonster">0</strong></div>'
+'<div style="font-size:12px;color:#64748b">Fönsterdörrar: <strong id="poCountFDorr">0</strong></div>'
+'<div style="font-size:12px;color:#64748b">Ytterdörrar: <strong id="poCountYDorr">0</strong></div>'
+'<div style="font-size:12px;color:#64748b">Övrig info: <input id="poOvrigInfo" placeholder="Skriv text här..." style="padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;width:300px"></div>'
+'</div>'
+'<div style="overflow-x:auto">'
+'<table id="poInmatningTable" style="width:100%;border-collapse:collapse;font-size:11px;min-width:1200px">'
+'<thead><tr style="background:#f1f5f9">'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:35px">Pos</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:80px">Rum</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:50px">Vån</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:90px">Produkt</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:60px">Bredd</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:60px">Höjd</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:40px">Antal</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:80px">Modell</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:70px">Färg in</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:70px">Färg ut</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:70px">Profil</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:60px">Typ</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:50px">Sida</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:50px">Vy</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:80px">Glas</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:80px">Solskydd</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:60px">Spröjs</th>'
+'<th style="padding:6px 4px;border:1px solid #e2e8f0;width:60px">Vent.</th>'
+'</tr></thead>'
+'<tbody id="poInmatningBody"></tbody>'
+'</table></div>'
+'<button type="button" onclick="addPoPositions(10)" style="margin-top:8px;padding:6px 16px;border-radius:8px;border:1px solid #e5e7eb;background:#f8fafc;font-size:12px;cursor:pointer;color:#64748b">+ 10 rader</button>'
+'</div></div>'
// === INNER TAB 2: TILLBEHÖR ===
+'<div id="poInnerContent2" style="display:none;margin-top:16px">'
+'<div style="'+sectionStyle+'">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
+'<h3 style="'+headingStyle+';margin:0"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>Hedlunds - Tillbehör</h3>'
+'<button type="button" onclick="calcTillbehorFromInmatning()" style="padding:6px 16px;border-radius:8px;border:1px solid #024550;background:#fff;color:#024550;font-size:12px;font-weight:600;cursor:pointer">Beräkna från inmätning</button>'
+'</div>'
+'<div id="tillbehorSummary" style="display:flex;gap:16px;margin-bottom:12px;padding:10px 14px;background:#f0f9ff;border-radius:8px;font-size:12px;color:#0284c7"></div>'
+'<div style="overflow-x:auto">'
+'<table style="width:100%;border-collapse:collapse;font-size:11px">'
+'<thead><tr style="background:#f1f5f9">'
+'<th style="padding:6px;border:1px solid #e2e8f0;text-align:left">Kategori</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;text-align:left">Artikel</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;width:60px">Antal</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;width:80px">Längd (mm)</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;width:100px">Spår/övrigt</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;width:80px;text-align:right">Pris/m</th>'
+'<th style="padding:6px;border:1px solid #e2e8f0;width:80px;text-align:right">Summa</th>'
+'</tr></thead>'
+'<tbody id="poHedlundsBody"></tbody>'
+'<tfoot><tr style="background:#f8f9fa;font-weight:700"><td colspan="6" style="padding:6px;border:1px solid #e2e8f0;text-align:right">Totalt Hedlunds:</td><td id="poHedlundsTotal" style="padding:6px;border:1px solid #e2e8f0;text-align:right">0 kr</td></tr></tfoot>'
+'</table></div>'
+'<button type="button" onclick="addHedlundsRow()" style="margin-top:8px;padding:6px 16px;border-radius:8px;border:1px solid #e5e7eb;background:#f8fafc;font-size:12px;cursor:pointer;color:#64748b">+ Lägg till artikel</button>'
+'</div></div>'
// === INNER TAB 3: LEVERANTÖRSORDER ===
+'<div id="poInnerContent3" style="display:none;margin-top:16px">'
+'<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>Leverantörsorder</h3>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px">'
+'<div><label style="'+labelStyle+'">Leverantör</label>'
+'<select id="poLeverantor" style="'+inputStyle+'">'
+'<option value="">Välj leverantör...</option>'+suppOpts+'</select></div>'
+'<div><label style="'+labelStyle+'">Orderdatum</label><input type="date" id="poOrderDatum" value="'+new Date().toISOString().split('T')[0]+'" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Förväntad leverans</label><input type="date" id="poLevDatum" style="'+inputStyle+'"></div>'
+'</div>'
+'<div style="font-size:12px;color:#94a3b8;padding:16px;text-align:center;background:#f8f9fa;border-radius:8px">Fönsterorderns rader genereras automatiskt från inmätningen</div>'
+'</div></div>'
// Block 5: MONTÖR
+'<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>Montör</h3>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+'<div><label style="'+labelStyle+'">Inmätt av</label><input id="poInmattAv" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">Telefon mättekniker</label><input id="poTelMattekniker" style="'+inputStyle+'"></div>'
+'</div>'
+'<div style="font-size:12px;color:#94a3b8;padding:12px;text-align:center;background:#f8f9fa;border-radius:8px;margin-top:8px">Monteringsbladet genereras automatiskt från inmätningen</div>'
+'</div>'
// Block 6: KONTROLL
+'<div style="'+sectionStyle+'">'
+'<h3 style="'+headingStyle+'"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#024550" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>Kontroll</h3>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">'
+'<div><label style="'+labelStyle+'">Typ av beställning</label>'
+'<select id="poKontrollTyp" style="'+inputStyle+'"><option>Nybeställning</option><option>Tilläggsbeställning</option><option>Reklamation</option></select></div>'
+'<div><label style="'+labelStyle+'">Beställningsdag</label><input type="date" id="poKontrollBestDag" value="'+new Date().toISOString().split('T')[0]+'" style="'+inputStyle+'"></div>'
+'<div><label style="'+labelStyle+'">BigBag</label><select id="poKontrollBigBag" style="'+inputStyle+'"><option value="0">Nej</option><option value="1">Ja</option></select></div>'
+'</div>'
+'<div style="margin-top:12px"><label style="'+labelStyle+'">Noteringar</label>'
+'<textarea id="poKontrollNotes" rows="3" style="'+inputStyle+';resize:vertical"></textarea></div>'
+'</div>'
// Save bar
+'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px;margin-bottom:32px">'
+'<button type="button" onclick="closePoForm()" style="padding:10px 24px;border-radius:10px;font-size:13px;font-weight:600;border:1px solid #e5e7eb;background:#fff;color:#334155;cursor:pointer">Tillbaka</button>'
+'<button type="button" onclick="saveMeasurements(false)" style="padding:10px 24px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:#16a34a;color:#fff;cursor:pointer">Spara inmätning</button>'
+'<button type="button" onclick="savePoForm()" style="padding:10px 28px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:linear-gradient(135deg,#024550,#036b78);color:#fff;cursor:pointer">Skapa inköpsorder</button>'
+'</div>'
+'</div>'; // close poTabContentNew
var overlay = document.createElement('div');
overlay.id = 'poFormOverlay';
overlay.style.cssText = 'position:fixed;inset:0;z-index:10000;background:#f8f9fa;overflow-y:auto';
overlay.innerHTML = '<div style="max-width:1400px;margin:0 auto;padding:24px">'+html+'</div>';
document.body.appendChild(overlay);
renderPoInmatning();
renderPoHedlunds();
}
var _tillbehorFormulas = [];
function switchPoInnerTab(n) {
for(var i=1;i<=3;i++){
var btn = document.getElementById('poInnerTab'+i);
var content = document.getElementById('poInnerContent'+i);
if(!btn||!content) continue;
if(i===n){
btn.style.cssText='padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:700;border:1px solid #e5e7eb;border-bottom:2px solid #fff;background:#fff;color:#024550;cursor:pointer;margin-bottom:-1px';
content.style.display='';
} else {
btn.style.cssText='padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;border:1px solid transparent;background:transparent;color:#94a3b8;cursor:pointer';
content.style.display='none';
}
}
if(n===2) renderPoHedlunds();
}
function calcTillbehorFromInmatning() {
// Gather window data from inmätning
var windows = _poFormPositions.filter(function(p){ return p.product && (parseInt(p.width)>0 || parseInt(p.height)>0); });
if(!windows.length) { alert('Inga fönster med mått att beräkna från.'); return; }
var summaryEl = document.getElementById('tillbehorSummary');
var totalPcs = 0;
windows.forEach(function(w){ totalPcs += (parseInt(w.pcs)||1); });
if(summaryEl) summaryEl.innerHTML = '<strong>'+totalPcs+' fönster/dörrar</strong> med mått → beräknar tillbehör...';
_poHedlundsRows = [];
// Process each formula (sorted by sort_order from API)
_tillbehorFormulas.forEach(function(f){
if(!f.active) return;
var totalQty = 0;
var lengths = [];
var isDoorOnly = (f.category === 'Trösklar');
var cutType = f.cut_type || 'none'; // gerad, rak, none
windows.forEach(function(w){
var pcs = parseInt(w.pcs)||1;
var ww = parseInt(w.width)||0;
var hh = parseInt(w.height)||0;
var prod = (w.product||'').toLowerCase();
// Trösklar only for doors
if(isDoorOnly && prod.indexOf('dörr')<0) return;
var margin = parseInt(f.margin_per_side)||0;
var extraMm = parseInt(f.extra_mm)||0;
var mult = parseFloat(f.multiplier)||1;
var len = 0;
if(f.base_field === 'width') {
len = ww + margin*2 + extraMm;
} else if(f.base_field === 'height') {
len = hh + margin*2 + extraMm;
} else if(f.base_field === 'circumference') {
len = (ww + hh)*2 + extraMm;
}
// multiplier = antal bitar per fönster för denna formelrad
var qtyPerWindow = Math.round(mult);
for(var q=0;q<qtyPerWindow;q++){
for(var p=0;p<pcs;p++){
totalQty++;
lengths.push(len);
}
}
});
if(totalQty > 0) {
var bestArticle = '';
var bestPrice = 0;
if(f.default_article) {
bestArticle = f.default_article;
var found = _hedlundsArticles.find(function(a){ return a.article_name === f.default_article; });
if(found) bestPrice = parseFloat(found.unit_price)||0;
}
// Average length per piece
var totalLen = 0;
lengths.forEach(function(l){ totalLen += l; });
var avgLen = Math.round(totalLen / totalQty);
// Build extra info with cut type
var cutLabel = cutType==='gerad'?'Gerad (45°)':cutType==='rak'?'Rak (list mot list)':'';
var extraInfo = f.label;
if(cutLabel) extraInfo += ' — '+cutLabel;
_poHedlundsRows.push({
category: f.category,
article: bestArticle,
quantity: totalQty,
length: avgLen,
extra: extraInfo,
price_per_m: bestPrice,
cut_type: cutType,
formula_id: f.id
});
}
});
// Add empty rows for categories not covered
['Foderkloss','Konsoll','Övrigt'].forEach(function(cat){
var exists = _poHedlundsRows.some(function(r){ return r.category === cat; });
if(!exists) _poHedlundsRows.push({category:cat, article:'', quantity:'', length:'', extra:'', price_per_m:0});
});
renderPoHedlunds();
calcHedlundsTotal();
if(summaryEl) {
var totalMeter = 0;
_poHedlundsRows.forEach(function(r){ if(r.quantity && r.length) totalMeter += parseInt(r.quantity)*parseInt(r.length)/1000; });
summaryEl.innerHTML = '<strong>'+totalPcs+' fönster/dörrar</strong> → <strong>'+_poHedlundsRows.length+' tillbehörsrader</strong> ('+totalMeter.toFixed(1)+' löpmeter)';
}
}
function switchPoTab(tab) {
var tabNewBtn = document.getElementById('poTabNew');
var tabExBtn = document.getElementById('poTabExisting');
var contentNew = document.getElementById('poTabContentNew');
var contentEx = document.getElementById('poTabContentExisting');
var tabActive = 'padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:700;border:1px solid #e5e7eb;border-bottom:2px solid #fff;background:#fff;color:#024550;cursor:pointer;margin-bottom:-1px';
var tabInactive = 'padding:10px 20px;border-radius:10px 10px 0 0;font-size:13px;font-weight:600;border:1px solid transparent;background:transparent;color:#94a3b8;cursor:pointer';
if(tab === 'new') {
tabNewBtn.style.cssText = tabActive;
tabExBtn.style.cssText = tabInactive;
contentNew.style.display = '';
contentEx.style.display = 'none';
} else {
tabNewBtn.style.cssText = tabInactive;
tabExBtn.style.cssText = tabActive;
contentNew.style.display = 'none';
contentEx.style.display = '';
}
}
function escHtml(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
function _selOpts(options, selected) {
return '<option value="">--</option>'+options.map(function(o){
if(o.indexOf('---')===0) return '<option disabled style="font-weight:700;color:#024550;background:#f1f5f9">'+o.replace(/^---/,'')+'</option>';
return '<option value="'+o+'"'+(o===selected?' selected':'')+'>'+o+'</option>';
}).join('');
}
var FONSTER_MODELS = ['---FÖNSTER / FD','Slim 2-glass','Slim 3-glass','Brilliant 2-glass','Brilliant 3-glass','Sweden 2-glass','Sweden 3-glass','Price 2-glas','SP fönster','---STANDARD','Tänndalen','Åre','Duved','Sälen','Vemdalen','Hemavan','Riksgränsen','---PREMIUM','Sundsvall','Falun','Rättvik','Helsingborg','Gävle','Rimini','Piombino','---PARDÖRR','Vemdalen II','---SIDOLJUS','KSI1','KSI2'];
var FONSTER_COLORS = ['---SLIM/FÖNSTER','White','Braun Genarbt','Mooreiche 2 Genarbt','Eiche Dunkel 1 Genarbt','Mahagoni Genarbt','Golden oak Genarbt','Dunkelgrün Genarbt','Grau Genarbt','Anthrazit Grau Genarbt','Anthrazit Grau Glatt','Schoko Braun Genarbt','Black Cherry Genarbt','Nussbaum Genarbt','Macore Genarbt','Sienna PN Genarbt','Sienna PR Genarbt','Cherry Blossom Genarbt','Soft Cherry Genarbt','Rustic Cherry Genarbt','Winchester XA Genarbt','Bergkiefer Genarbt','Irish Oak Genarbt','Eiche Hell 1 Genarbt','Oregon Genarbt','Cremeweiss Genarbt','Weiss Genarbt','Weinrot Genarbt','Dunkelrot Genarbt','Rot Genarbt','Mossgrün Genarbt','Grün Genarbt','Stahlblau Genarbt','Brilliantblau Genarbt','Schiefergrau Glatt','Basaltgrau Genarbt','Basaltgrau Glatt','Quarzgrau Genarbt','Quarzgrau Glatt','Signalgrau Glatt','Grau Glatt','Achatgrau Genarbt','Lichtgrau Genarbt','Crown Platin Genarbt','Anthrazitgrau Gebürstet','Aluminium Gebürstet','Alux Anthrazit','Alux DB 703','---YTTERDÖRR','Vit NCS S0502-Y','Blå NCS S6020-B','Grön NCS S5020-G70Y','Röd NCS S4050-R','Gul NCS S2040-Y20R','Mörk Grå NCS S6000-N','Ljus grå NCS S1502-Y50R','Kaski svart','Kaski mörkgrå','Kaski ljusgrå','Kaski vit','Kaski brun','Kaski furu mörk','Kaski furu ljus'];
var FONSTER_PROFILES = ['REHAU Euro 70 Slim','Brilliant Profile','Nordic Design Plus'];
var FONSTER_GLASS = ['Cotswold','Antisun clear','Rain + saftyglass','bioclean','säkerhetsglas','säkerhetsglas 2 sidor','6 mm','8 mm','regn','regn + säkerhetsglas','Hardened glass inside','Hardened glass outside','Hardened glass in and outside','Laminated glass inside','Laminated glass outside','Laminated glass in and outside','antisun klart'];
var FONSTER_TYPES = ['---FÖNSTER','In','Out','Fixed','Fixed Fakeframe','In 2-air','In 3-air','In fully glazed','Out fully glazed','---YTTERDÖRR','Utåt','Utåt pardörr'];
var FONSTER_SIDES = ['---FÖNSTER','Left','Right','Bottomhung','Tophung','Bottomhung right','Bottomhung left','Left, Right','Left, Left Right','Left, Right, Right','---FÖNSTERDÖRR','Primary Door left','Primary Door Right','---SVENSKA','vänster','höger','Underkant','Överkant','Underkant höger','Underkant vänster','vänster, höger','vänster, vänster, höger','vänster, höger, höger','---PARYTTERDÖRR','Vänster gångdörr','Höger gångdörr'];
var FONSTER_VIEWS = ['From Inside','From Outside','inifrån','utifrån'];
var FONSTER_SUNPROTECTION = ['---Mörkläggande Rullgardiner','ZP 160','ZP 161','ZP 1601','ZP 351','ZP 354','ZP 358','---Ljusgenomsläppande Rullgardiner','ZP 135','ZP 01','---Plisséer','PLI 2301','PLI 2007','PLI 2404','PLI 4006','PLI 4001','PLI 2303','PLI 3303','PLI 3304','---Persienner','Persienne White','Persienne Grey'];
var FONSTER_VENT = [];
function renderPoInmatning() {
var tb = document.getElementById('poInmatningBody');
if(!tb) return;
var cellStyle = 'padding:2px;border:1px solid #e2e8f0';
var inpStyle = 'width:100%;padding:4px;border:none;font-size:11px;font-family:inherit;box-sizing:border-box;background:transparent';
var selStyle = inpStyle;
tb.innerHTML = _poFormPositions.map(function(p,i){
return '<tr>'
+'<td style="'+cellStyle+';text-align:center;color:#94a3b8;font-weight:600">'+p.pos+'</td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="rum" value="'+escHtml(p.rum)+'" style="'+inpStyle+'"></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="vaning" value="'+escHtml(p.vaning)+'" style="'+inpStyle+';width:40px"></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="product" style="'+selStyle+'"><option value="">--</option><option value="Fönster"'+(p.product==='Fönster'?' selected':'')+'>Fönster</option><option value="Fönsterdörr"'+(p.product==='Fönsterdörr'?' selected':'')+'>Fönsterdörr</option><option value="Parfönsterdörr"'+(p.product==='Parfönsterdörr'?' selected':'')+'>Parfönsterdörr</option><option value="Ytterdörr"'+(p.product==='Ytterdörr'?' selected':'')+'>Ytterdörr</option><option value="Parytterdörr"'+(p.product==='Parytterdörr'?' selected':'')+'>Parytterdörr</option></select></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="width" type="number" value="'+(p.width||'')+'" style="'+inpStyle+';width:55px"></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="height" type="number" value="'+(p.height||'')+'" style="'+inpStyle+';width:55px"></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="pcs" type="number" value="'+(p.pcs||'')+'" min="1" style="'+inpStyle+';width:35px"></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="model" style="'+selStyle+'">'+_selOpts(FONSTER_MODELS,p.model)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="color_in" style="'+selStyle+'">'+_selOpts(FONSTER_COLORS,p.color_in)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="color_out" style="'+selStyle+'">'+_selOpts(FONSTER_COLORS,p.color_out)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="profile" style="'+selStyle+'">'+_selOpts(FONSTER_PROFILES,p.profile)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="type" style="'+selStyle+'">'+_selOpts(FONSTER_TYPES,p.type)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="side" style="'+selStyle+'">'+_selOpts(FONSTER_SIDES,p.side)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="view" style="'+selStyle+'">'+_selOpts(FONSTER_VIEWS,p.view)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="glass" style="'+selStyle+'">'+_selOpts(FONSTER_GLASS,p.glass)+'</select></td>'
+'<td style="'+cellStyle+'"><select data-pos="'+i+'" data-field="sunprotection" style="'+selStyle+'">'+_selOpts(FONSTER_SUNPROTECTION,p.sunprotection)+'</select></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="mullion" value="'+escHtml(p.mullion)+'" style="'+inpStyle+';width:50px"></td>'
+'<td style="'+cellStyle+'"><input data-pos="'+i+'" data-field="ventilation" value="'+escHtml(p.ventilation)+'" style="'+inpStyle+';width:50px"></td>'
+'</tr>';
}).join('');
// Fields where "apply to all" makes sense
var applyAllFields = ['width','height','model','color_in','color_out','profile','type','side','view','glass','sunprotection'];
// Bind input changes
tb.querySelectorAll('input,select').forEach(function(inp){
inp.addEventListener('change', function(){
var idx = parseInt(this.dataset.pos);
var field = this.dataset.field;
var val = this.value;
// Auto-convert dm to mm for width/height (values ≤99 are dm, multiply by 100)
if((field === 'width' || field === 'height') && val && !isNaN(val)) {
var num = parseFloat(val);
if(num > 0 && num < 100) { val = String(Math.round(num * 100)); this.value = val; }
}
if(idx >= 0 && field) _poFormPositions[idx][field] = val;
updatePoCounts();
// If it's an apply-all field and multiple rows have product, show Ja/Nej dialog
if(val && applyAllFields.indexOf(field) >= 0) {
var filledRows = _poFormPositions.filter(function(p){ return p.product; });
if(filledRows.length > 1) {
var fieldLabels = {width:'Bredd',height:'Höjd',model:'Modell',color_in:'Färg in',color_out:'Färg ut',profile:'Profil',type:'Typ',side:'Sida',view:'Vy',glass:'Glas',sunprotection:'Solskydd'};
var label = fieldLabels[field]||field;
// Custom Ja/Nej dialog
var old2 = document.getElementById('applyAllDialog'); if(old2) old2.remove();
var dlg = document.createElement('div');
dlg.id = 'applyAllDialog';
dlg.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:20000;display:flex;align-items:center;justify-content:center';
dlg.innerHTML = '<div style="background:#fff;border-radius:14px;padding:24px 28px;box-shadow:0 20px 60px rgba(0,0,0,.25);max-width:400px;width:90%">'
+'<div style="font-size:15px;font-weight:700;color:#024550;margin-bottom:6px">'+label+' = "'+escHtml(val)+'"</div>'
+'<div style="font-size:13px;color:#64748b;margin-bottom:16px">Sätta samma på alla '+filledRows.length+' rader med produkt?</div>'
+'<div style="display:flex;gap:10px;justify-content:flex-end">'
+'<button id="applyAllNo" style="padding:8px 24px;border-radius:10px;font-size:13px;font-weight:600;border:1px solid #e5e7eb;background:#fff;color:#334155;cursor:pointer">Nej</button>'
+'<button id="applyAllYes" style="padding:8px 24px;border-radius:10px;font-size:13px;font-weight:700;border:none;background:linear-gradient(135deg,#024550,#036b78);color:#fff;cursor:pointer">Ja</button>'
+'</div></div>';
document.body.appendChild(dlg);
(function(f,v){
document.getElementById('applyAllYes').onclick = function(){
_poFormPositions.forEach(function(p){ if(p.product) p[f] = v; });
dlg.remove();
renderPoInmatning();
};
document.getElementById('applyAllNo').onclick = function(){ dlg.remove(); };
})(field, val);
}
}
});
});
updatePoCounts();
}
function updatePoCounts() {
var fc=0,fd=0,yd=0;
_poFormPositions.forEach(function(p){
if(!p.product) return;
var pcs = parseInt(p.pcs)||1;
if(p.product==='Fönster') fc+=pcs;
else if(p.product==='Fönsterdörr'||p.product==='Parfönsterdörr') fd+=pcs;
else if(p.product==='Ytterdörr'||p.product==='Parytterdörr') yd+=pcs;
});
var el1 = document.getElementById('poCountFonster'); if(el1) el1.textContent = fc;
var el2 = document.getElementById('poCountFDorr'); if(el2) el2.textContent = fd;
var el3 = document.getElementById('poCountYDorr'); if(el3) el3.textContent = yd;
}
function addPoPositions(n) {
var start = _poFormPositions.length;
for(var i=0;i<n;i++) _poFormPositions.push({pos:start+i+1, rum:'',vaning:'',product:'',width:'',height:'',pcs:1,model:'',color_in:'',color_out:'',profile:'',type:'',side:'',view:'',glass:'',sunprotection:'',mullion:'',ventilation:''});
renderPoInmatning();
}
var _poHedlundsRows = [];
function _buildArticleSelect(rowIdx, category, selectedArticle) {
// Filter articles matching this row's category (fuzzy match)
var catLower = (category||'').toLowerCase();
var matching = _hedlundsArticles.filter(function(a){
var ac = (a.category||'').toLowerCase();
return ac.indexOf(catLower)>=0 || catLower.indexOf(ac)>=0;
});
// If no match, show all articles grouped by category
if(!matching.length) matching = _hedlundsArticles;
var opts = '<option value="">-- Välj artikel --</option>';
var lastCat = '';
matching.forEach(function(a){
if(a.category !== lastCat) {
if(lastCat) opts += '</optgroup>';
opts += '<optgroup label="'+escHtml(a.category)+'">';
lastCat = a.category;
}
var sel = (a.article_name === selectedArticle) ? ' selected' : '';
var priceInfo = a.unit_price ? ' ('+a.unit_price+' kr/m)' : '';
opts += '<option value="'+escHtml(a.article_name)+'" data-price="'+(a.unit_price||0)+'">'+escHtml(a.article_name)+priceInfo+'</option>';
});
if(lastCat) opts += '</optgroup>';
return '<select data-hi="'+rowIdx+'" data-hf="article" style="width:100%;padding:4px;border:none;font-size:11px;font-family:inherit;box-sizing:border-box;background:transparent">'+opts+'</select>';
}
function renderPoHedlunds() {
if(!_poHedlundsRows.length) {
['Fönsterbänk','Invändigt foder','Invändigt smyg','Trösklar','Utv smyg & foder','Foderkloss','Konsoll'].forEach(function(cat){
_poHedlundsRows.push({category:cat, article:'', article_id:null, quantity:'', length:'', extra:'', price_per_m:0});
});
}
var tb = document.getElementById('poHedlundsBody');
if(!tb) return;
var inpStyle = 'width:100%;padding:4px;border:none;font-size:11px;font-family:inherit;box-sizing:border-box;background:transparent';
tb.innerHTML = _poHedlundsRows.map(function(r,i){
var cutBadge = '';
if(r.cut_type === 'gerad') cutBadge = '<span style="display:inline-block;margin-left:4px;padding:1px 6px;border-radius:4px;background:#fef3c7;color:#92400e;font-size:9px;font-weight:600">GERAD 45°</span>';
else if(r.cut_type === 'rak') cutBadge = '<span style="display:inline-block;margin-left:4px;padding:1px 6px;border-radius:4px;background:#dbeafe;color:#1e40af;font-size:9px;font-weight:600">RAK</span>';
return '<tr>'
+'<td style="padding:4px 6px;border:1px solid #e2e8f0;font-weight:600;font-size:11px;color:#334155;white-space:nowrap">'+escHtml(r.category)+cutBadge+'</td>'
+'<td style="padding:2px;border:1px solid #e2e8f0;min-width:250px">'+_buildArticleSelect(i, r.category, r.article)+'</td>'
+'<td style="padding:2px;border:1px solid #e2e8f0"><input data-hi="'+i+'" data-hf="quantity" type="number" value="'+(r.quantity||'')+'" style="'+inpStyle+';width:55px" placeholder="0"></td>'
+'<td style="padding:2px;border:1px solid #e2e8f0"><input data-hi="'+i+'" data-hf="length" type="number" value="'+(r.length||'')+'" style="'+inpStyle+';width:75px" placeholder="mm"></td>'
+'<td style="padding:2px;border:1px solid #e2e8f0"><input data-hi="'+i+'" data-hf="extra" value="'+escHtml(r.extra)+'" style="'+inpStyle+'" placeholder="Spår etc."></td>'
+'<td style="padding:4px 6px;border:1px solid #e2e8f0;text-align:right;font-size:11px;color:#64748b" id="hedPrice_'+i+'">'+(r.price_per_m?r.price_per_m+' kr/m':'')+'</td>'
+'<td style="padding:4px 6px;border:1px solid #e2e8f0;text-align:right;font-size:11px;font-weight:600" id="hedSum_'+i+'">'+((r.quantity&&r.length&&r.price_per_m)?fmtKr(r.quantity*r.length/1000*r.price_per_m):'-')+'</td>'
+'</tr>';
}).join('');
// Bind article select change - auto-fill price
tb.querySelectorAll('select[data-hf="article"]').forEach(function(sel){
sel.addEventListener('change', function(){
var idx = parseInt(this.dataset.hi);
_poHedlundsRows[idx].article = this.value;
var opt = this.options[this.selectedIndex];
var price = parseFloat(opt.dataset.price)||0;
_poHedlundsRows[idx].price_per_m = price;
var priceEl = document.getElementById('hedPrice_'+idx);
if(priceEl) priceEl.textContent = price ? price+' kr/m' : '';
calcHedlundsTotal();
});
});
tb.querySelectorAll('input').forEach(function(inp){
inp.addEventListener('change', function(){
var idx = parseInt(this.dataset.hi);
var f = this.dataset.hf;
if(idx >= 0 && f) _poHedlundsRows[idx][f] = this.value;
calcHedlundsTotal();
});
});
}
function addHedlundsRow() {
_poHedlundsRows.push({category:'Övrigt', article:'', quantity:'', length:'', extra:'', price_per_m:0});
renderPoHedlunds();
}
function calcHedlundsTotal() {
var total = 0;
_poHedlundsRows.forEach(function(r,i){
var rowSum = 0;
if(r.quantity && r.length && r.price_per_m) rowSum = parseInt(r.quantity) * parseInt(r.length)/1000 * parseFloat(r.price_per_m);
total += rowSum;
var sumEl = document.getElementById('hedSum_'+i);
if(sumEl) sumEl.textContent = rowSum ? fmtKr(rowSum) : '-';
});
var el = document.getElementById('poHedlundsTotal');
if(el) el.textContent = fmtKr(total);
}
/* ========================================
* FÖNSTER KALKYL KONFIGURATOR
* ======================================== */
// === FÖNSTER KONFIGURATOR (Säljar-kalkyl) ===
var _fkSelectedProduct = null;
var _fkSelectedTillbehor = []; // [{id, name, price, qty}]
var _fkQuoteId = null;
var _fkDeductionType = 'rot';
var _fkOwnerCount = 1;
var _fkFinYears = 15;
var _fkFinRate = 0.049;
var _fkProducts = [];
var _fkFoderTyp = 'gerad'; // gerad | kloss | ingen
var _fkSmygTyp = 'ja'; // ja | nej
var _fkPositions = [{pos:1, rum:'', produkt:'', bredd:1200, hojd:1200, antal:1, glas:'3-glas', notering:''}];
var _fkTillbCategories = [
{key:'sprojs', label:'Spröjs', items:[]},
{key:'persienner', label:'Persienner', items:[]},
{key:'persienner', label:'Persienner', items:[]},
{key:'solskydd', label:'Solskydd', items:[]},
{key:'beslag', label:'Beslag', items:[]},
{key:'ovrigt', label:'Övrigt', items:[]}
];
var _fkCurrentTillbCat = 'sprojs';
async function initFonsterConfig() {
try {
var res = await fetch('/api/products.php?cat=fonster');
var data = await res.json();
_fkProducts = data.products || [];
} catch(e) { _fkProducts = []; }
renderFkProductCards();
// Load tillbehör products
try {
var res2 = await fetch('/api/products.php?cat=tillbehor');
var data2 = await res2.json();
var tillbProducts = data2.products || [];
// Also load door products
var res3 = await fetch('/api/products.php?cat=dorrar');
var data3 = await res3.json();
var dorrProducts = data3.products || [];
// Combine into _fkProducts for reference
_fkProducts = _fkProducts.concat(dorrProducts);
// Build door product cards too
buildFkTillbCategories(tillbProducts);
} catch(e){}
renderFkTillbMenu();
updateFkCalc();
}
function renderFkProductCards() {
var container = document.getElementById('fkProductCards');
if(!container) return;
var html = '';
_fkProducts.forEach(function(p){
var sel = _fkSelectedProduct && _fkSelectedProduct.id === p.id;
var borderColor = sel ? '#3b82f6' : '#e5e7eb';
var bg = sel ? '#f0f9ff' : '#fff';
var imgHtml = p.img ? '<img src="'+p.img+'" style="width:48px;height:48px;object-fit:cover;border-radius:8px;flex-shrink:0" onerror="this.style.display=\'none\'">' : '<div style="width:48px;height:48px;background:#f1f5f9;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" style="width:24px;height:24px;fill:none;stroke:#94a3b8;stroke-width:1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></svg></div>';
var radioHtml = sel ? '<div style="width:20px;height:20px;border:2px solid #3b82f6;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0"><div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div></div>' : '<div style="width:20px;height:20px;border:2px solid #d1d5db;border-radius:50%;flex-shrink:0"></div>';
html += '<div data-product-id="'+p.id+'" onclick="selectFkProduct(\''+p.id+'\')" style="padding:14px 16px;border:2px solid '+borderColor+';border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:'+bg+';transition:all .15s">';
html += radioHtml + imgHtml;
html += '<div style="flex:1"><div style="font-weight:600;font-size:14px">'+p.name+'</div>';
if(p.description) html += '<div style="font-size:11px;color:#64748b;margin-top:2px">'+p.description+'</div>';
html += '</div>';
html += '<div style="font-weight:700;font-size:14px;color:#1a1a1a">'+fmt(parseFloat(p.price)||0)+'</div>';
html += '</div>';
});
if(!html) html = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Inga fönsterprodukter i katalogen. Lägg till via Produkter-fliken.</div>';
container.innerHTML = html;
}
function selectFkProduct(id) {
_fkSelectedProduct = _fkProducts.find(function(p){ return p.id === id; }) || null;
renderFkProductCards();
updateFkCalc();
}
function buildFkTillbCategories(products) {
// Reset items
_fkTillbCategories.forEach(function(c){ c.items = []; });
products.forEach(function(p) {
var name = (p.name || '').toLowerCase();
// Foder/smyg/bänk har egna steg - skippa dem i tillbehör
if(name.indexOf('foder')>=0 || name.indexOf('smyg')>=0 || name.indexOf('bänk')>=0 || name.indexOf('bank')>=0) { return; }
if(name.indexOf('spröjs')>=0 || name.indexOf('sprojs')>=0) { _fkTillbCategories[0].items.push(p); }
else if(name.indexOf('persienn')>=0) { _fkTillbCategories[1].items.push(p); }
else if(name.indexOf('solskydd')>=0 || name.indexOf('rull')>=0) { _fkTillbCategories[2].items.push(p); }
else if(name.indexOf('beslag')>=0 || name.indexOf('handtag')>=0) { _fkTillbCategories[3].items.push(p); }
else { _fkTillbCategories[4].items.push(p); }
});
// If all categories empty, add placeholder items
if(_fkTillbCategories.every(function(c){ return c.items.length===0; })) {
var defaultItems = [
{cat:'sprojs', items:[{id:'TB01',name:'Spröjs standard',price:450,description:'Per fönster'},{id:'TB02',name:'Mellanglasspröjs',price:650,description:'Per fönster'}]},
{cat:'foder', items:[{id:'TB03',name:'Fönsterfoder komplett',price:890,description:'Per fönster'},{id:'TB04',name:'Smygbräda',price:320,description:'Per fönster'}]},
{cat:'fonsterbank', items:[{id:'TB05',name:'Fönsterbänk trä',price:550,description:'Per fönster'},{id:'TB06',name:'Fönsterbänk granit',price:1850,description:'Per fönster'}]},
{cat:'persienner', items:[{id:'TB07',name:'Persienn Basic vit',price:2474,description:'Per fönster'},{id:'TB08',name:'Persienn Basic svart',price:2594,description:'Per fönster'}]},
{cat:'solskydd', items:[{id:'TB09',name:'Rullgardin enkel',price:1200,description:'Per fönster'},{id:'TB10',name:'Solfilm',price:800,description:'Per fönster'}]},
{cat:'beslag', items:[{id:'TB11',name:'Handtag standard',price:250,description:'Per fönster'},{id:'TB12',name:'Låsbeslag',price:380,description:'Per fönster'}]},
{cat:'ovrigt', items:[{id:'TB13',name:'Monteringskit',price:450,description:'Per fönster'},{id:'TB14',name:'Isoleringskit',price:320,description:'Per fönster'}]}
];
defaultItems.forEach(function(di) {
var cat = _fkTillbCategories.find(function(c){ return c.key === di.cat; });
if(cat) cat.items = di.items;
});
}
}
function renderFkTillbMenu() {
var menuEl = document.getElementById('fkTillbCatMenu');
var prodsEl = document.getElementById('fkTillbProducts');
if(!menuEl || !prodsEl) return;
// Menu
var menuHtml = '';
_fkTillbCategories.forEach(function(c) {
var active = c.key === _fkCurrentTillbCat;
var style = active ? 'background:#024550;color:#fff;font-weight:600;' : 'background:#f8fafc;color:#334155;';
var count = c.items.length;
menuHtml += '<button onclick="switchFkTillbCat(\''+c.key+'\')" style="padding:8px 12px;border:none;border-radius:6px;font-size:12px;cursor:pointer;text-align:left;font-family:inherit;'+style+(active?'border-left:3px solid #f59e0b;':'border-left:3px solid transparent;')+'">'+c.label+' <span style="opacity:.6;font-size:10px">('+count+')</span></button>';
});
menuEl.innerHTML = menuHtml;
// Products
var cat = _fkTillbCategories.find(function(c){ return c.key === _fkCurrentTillbCat; });
var prodsHtml = '';
if(cat && cat.items.length) {
cat.items.forEach(function(item) {
var isSelected = _fkSelectedTillbehor.some(function(t){ return t.id === item.id; });
var borderCol = isSelected ? '#a855f7' : '#e5e7eb';
var bgCol = isSelected ? '#faf5ff' : '#fff';
var imgHtml = item.img ? '<img src="'+item.img+'" style="width:100%;height:80px;object-fit:cover;border-radius:6px;margin-bottom:6px" onerror="this.style.display=\'none\'">' : '<div style="width:100%;height:80px;background:#f8fafc;border-radius:6px;margin-bottom:6px;display:flex;align-items:center;justify-content:center"><svg viewBox="0 0 24 24" style="width:28px;height:28px;fill:none;stroke:#cbd5e1;stroke-width:1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="12" y1="3" x2="12" y2="21"/></svg></div>';
prodsHtml += '<div onclick="toggleFkTillbehor(\''+item.id+'\',\''+item.name.replace(/'/g,"\\'")+'\','+parseFloat(item.price||0)+')" style="border:2px solid '+borderCol+';border-radius:10px;padding:10px;cursor:pointer;background:'+bgCol+';transition:all .15s">';
prodsHtml += imgHtml;
prodsHtml += '<div style="font-size:12px;font-weight:600;line-height:1.3">'+item.name+'</div>';
prodsHtml += '<div style="font-size:12px;font-weight:700;color:#024550;margin-top:4px">'+fmt(parseFloat(item.price)||0)+'</div>';
if(isSelected) prodsHtml += '<div style="margin-top:4px;font-size:10px;color:#a855f7;font-weight:600">✓ Vald</div>';
prodsHtml += '</div>';
});
} else {
prodsHtml = '<div style="padding:30px;text-align:center;color:#94a3b8;font-size:13px;grid-column:1/-1">Inga produkter i denna kategori</div>';
}
prodsEl.innerHTML = prodsHtml;
// Selected list
renderFkSelectedTillbehor();
}
function switchFkTillbCat(key) {
_fkCurrentTillbCat = key;
renderFkTillbMenu();
}
function toggleFkTillbehor(id, name, price) {
var idx = _fkSelectedTillbehor.findIndex(function(t){ return t.id === id; });
if(idx >= 0) {
_fkSelectedTillbehor.splice(idx, 1);
} else {
_fkSelectedTillbehor.push({id:id, name:name, price:price, qty:1});
}
renderFkTillbMenu();
updateFkCalc();
}
function updateFkTillbQty(id, val) {
var item = _fkSelectedTillbehor.find(function(t){ return t.id === id; });
if(item) { item.qty = Math.max(1, parseInt(val)||1); }
renderFkSelectedTillbehor();
updateFkCalc();
}
function removeFkTillbehor(id) {
_fkSelectedTillbehor = _fkSelectedTillbehor.filter(function(t){ return t.id !== id; });
renderFkTillbMenu();
updateFkCalc();
}
function renderFkSelectedTillbehor() {
var wrap = document.getElementById('fkTillbSelected');
var list = document.getElementById('fkTillbSelectedList');
if(!wrap || !list) return;
if(_fkSelectedTillbehor.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = 'block';
var html = '';
_fkSelectedTillbehor.forEach(function(t) {
html += '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#faf5ff;border:1px solid #e9d5ff;border-radius:8px">';
html += '<div style="flex:1;font-size:12px;font-weight:600;color:#6b21a8">'+t.name+'</div>';
html += '<div style="font-size:11px;color:#64748b">'+fmt(t.price)+'/st</div>';
html += '<input type="number" value="'+t.qty+'" min="1" onchange="updateFkTillbQty(\''+t.id+'\',this.value)" style="width:50px;padding:4px 6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;text-align:center;font-family:inherit">';
html += '<div style="font-size:12px;font-weight:700;min-width:70px;text-align:right">'+fmt(t.price * t.qty)+'</div>';
html += '<button onclick="removeFkTillbehor(\''+t.id+'\')" style="background:none;border:none;cursor:pointer;color:#ef4444;font-size:16px;padding:2px 4px">×</button>';
html += '</div>';
});
list.innerHTML = html;
}
function setFkFoder(typ) {
_fkFoderTyp = typ;
var cards = document.querySelectorAll('#fkFoderCards label');
cards.forEach(function(lbl) {
var radio = lbl.querySelector('input[type=radio]');
if(radio && radio.value === typ) {
lbl.style.borderColor = '#a855f7'; lbl.style.background = '#faf5ff';
} else {
lbl.style.borderColor = '#e5e7eb'; lbl.style.background = '#fff';
}
});
updateFkCalc();
}
function setFkSmyg(typ) {
_fkSmygTyp = typ;
var cards = document.querySelectorAll('#fkSmygCards label');
cards.forEach(function(lbl) {
var radio = lbl.querySelector('input[type=radio]');
if(radio && radio.value === typ) {
lbl.style.borderColor = '#a855f7'; lbl.style.background = '#faf5ff';
} else {
lbl.style.borderColor = '#e5e7eb'; lbl.style.background = '#fff';
}
});
updateFkCalc();
}
function setFkDeduction(type) {
_fkDeductionType = type;
document.querySelectorAll('.fk-deduct-btn').forEach(function(btn) {
var dt = btn.getAttribute('data-dt');
if(dt === type) { btn.style.background = '#059669'; btn.style.color = '#fff'; btn.style.borderColor = '#059669'; }
else { btn.style.background = '#fff'; btn.style.color = '#059669'; btn.style.borderColor = '#bbf7d0'; }
});
updateFkCalc();
}
function setFkOwners(n) {
_fkOwnerCount = n;
document.querySelectorAll('.fk-owner-btn').forEach(function(btn) {
var oc = parseInt(btn.getAttribute('data-oc'));
if(oc === n) { btn.style.background = '#059669'; btn.style.color = '#fff'; btn.style.borderColor = '#059669'; }
else { btn.style.background = '#fff'; btn.style.color = '#059669'; btn.style.borderColor = '#bbf7d0'; }
});
updateFkCalc();
}
function setFkFinYears(yr) {
_fkFinYears = yr;
document.querySelectorAll('.fk-fin-btn').forEach(function(btn) {
var y = parseInt(btn.getAttribute('data-yr'));
if(y === yr) { btn.style.background = '#3b82f6'; btn.style.color = '#fff'; btn.style.borderColor = '#3b82f6'; }
else { btn.style.background = '#fff'; btn.style.color = '#3b82f6'; btn.style.borderColor = '#bfdbfe'; }
});
var info = document.getElementById('fkFinInfo');
if(info) info.textContent = '('+yr+' \u00e5r, '+((_fkFinRate*100).toFixed(1))+'%)';
updateFkCalc();
}
function updateFkCalc() {
var antal = parseInt(el('fkAntal')?.value) || 0;
var antalDisp = document.getElementById('fkAntalVal');
if(antalDisp) antalDisp.textContent = antal;
var pricePerUnit = _fkSelectedProduct ? parseFloat(_fkSelectedProduct.price) || 0 : 0;
var fonsterCost = antal * pricePerUnit;
var tillbCost = 0;
_fkSelectedTillbehor.forEach(function(t) { tillbCost += (t.price * t.qty); });
// Multiply tillbehör that are per-window by antal
// If qty matches default (1), assume it's per-window
_fkSelectedTillbehor.forEach(function(t) {
// tillbCost already includes qty, but for per-window items we multiply by antal
// Actually just use qty as-is - user sets total quantity manually
});
var montTimmar = parseFloat(el('fkMontTimmar')?.value) || 0;
var montPris = parseFloat(el('fkMontPris')?.value) || 0;
var laborCost = montTimmar * montPris;
var frakt = parseFloat(el('fkFrakt')?.value) || 0;
var marginPct = parseInt(el('fkMarginSlider')?.value) || 0;
var pctDisp = document.getElementById('fkMarginPct');
if(pctDisp) pctDisp.textContent = marginPct + '%';
var subtotal = fonsterCost + tillbCost + laborCost + frakt;
var marginKr = Math.round(subtotal * marginPct / 100);
var totalBeforeDeduct = subtotal + marginKr;
// Deduction
var deductKr = 0;
var maxPerPerson = 50000;
var maxDeduct = maxPerPerson * _fkOwnerCount;
if(_fkDeductionType === 'rot') {
deductKr = Math.min(Math.round(laborCost * 0.30), maxDeduct);
var lbl = document.getElementById('fkDeductLabel');
if(lbl) lbl.textContent = 'ROT-AVDRAG (30% av arbete)';
} else if(_fkDeductionType === 'green') {
deductKr = Math.min(Math.round(totalBeforeDeduct * 0.20), maxDeduct);
var lbl = document.getElementById('fkDeductLabel');
if(lbl) lbl.textContent = 'GR\u00d6NT TEKNIK (20% av allt)';
} else {
var lbl = document.getElementById('fkDeductLabel');
if(lbl) lbl.textContent = 'INGET AVDRAG';
}
var total = totalBeforeDeduct - deductKr;
// Monthly
var r = _fkFinRate / 12;
var n = _fkFinYears * 12;
var monthly = total > 0 && r > 0 ? Math.round(total * r * Math.pow(1+r, n) / (Math.pow(1+r, n) - 1)) : 0;
// Update DOM
var desc = document.getElementById('fkPrDesc');
if(desc) desc.textContent = _fkSelectedProduct ? '('+antal+' x '+_fkSelectedProduct.name+')' : '';
if(el('fkPrFonster')) el('fkPrFonster').textContent = fmt(fonsterCost);
if(el('fkPrTillbehor')) el('fkPrTillbehor').textContent = fmt(tillbCost);
if(el('fkPrMontering')) el('fkPrMontering').textContent = fmt(laborCost);
if(el('fkPrFrakt')) el('fkPrFrakt').textContent = fmt(frakt);
if(el('fkPrSubtotal')) el('fkPrSubtotal').textContent = fmt(totalBeforeDeduct);
if(el('fkMarginKr')) el('fkMarginKr').textContent = fmt(marginKr);
if(el('fkDeductKr')) el('fkDeductKr').textContent = '-' + fmt(deductKr);
if(el('fkPrTotal')) el('fkPrTotal').textContent = fmt(total);
if(el('fkPrMonthly')) el('fkPrMonthly').textContent = fmt(monthly) + '/m\u00e5n';
}
function goToFkAffar() {
alert('Affär-flöde för fönster kommer snart');
}
async function saveFkQuote(status) {
var antal = parseInt(el('fkAntal')?.value) || 0;
var configData = {
product: _fkSelectedProduct,
antal: antal,
glas: el('fkGlas')?.value || '',
profil: el('fkProfil')?.value || '',
bredd: parseInt(el('fkBredd')?.value) || 0,
hojd: parseInt(el('fkHojd')?.value) || 0,
farg_in: el('fkFargIn')?.value || '',
farg_ut: el('fkFargUt')?.value || '',
foder_typ: _fkFoderTyp,
smyg: _fkSmygTyp,
foder_farg_in: el('fkFoderFargIn')?.value || '',
foder_farg_ut: el('fkFoderFargUt')?.value || '',
fonsterbank: document.querySelector('input[name=fkBank]:checked')?.value || 'tra',
tillbehor: _fkSelectedTillbehor,
montering_timmar: parseFloat(el('fkMontTimmar')?.value) || 0,
montering_pris: parseFloat(el('fkMontPris')?.value) || 0,
frakt: parseFloat(el('fkFrakt')?.value) || 0,
margin_percent: parseInt(el('fkMarginSlider')?.value) || 0,
deduction_type: _fkDeductionType,
owner_count: _fkOwnerCount
};
var pricePerUnit = _fkSelectedProduct ? parseFloat(_fkSelectedProduct.price) || 0 : 0;
var fonsterCost = antal * pricePerUnit;
var tillbCost = 0;
_fkSelectedTillbehor.forEach(function(t){ tillbCost += t.price * t.qty; });
var laborCost = configData.montering_timmar * configData.montering_pris;
var subtotal = fonsterCost + tillbCost + laborCost + configData.frakt;
var marginKr = Math.round(subtotal * configData.margin_percent / 100);
var total = subtotal + marginKr;
var desc = [];
if(_fkSelectedProduct) desc.push(_fkSelectedProduct.name + ' x ' + antal);
if(configData.glas) desc.push(configData.glas);
if(configData.profil) desc.push(configData.profil);
var payload = {
category: 'fonster',
customer_name: pendingKalkylCustomer?.name || '',
customer_address: pendingKalkylCustomer?.address || '',
customer_email: pendingKalkylCustomer?.email || '',
customer_phone: pendingKalkylCustomer?.phone || '',
config_data: JSON.stringify(configData),
material_cost: fonsterCost + tillbCost,
labor_cost: laborCost,
subtotal: subtotal,
margin_percent: configData.margin_percent,
deal_total: total,
total_price: total,
panel_name: desc.join(', '),
panel_count: antal,
status: status || 'utkast',
created_by: gStaffId || null,
notes: ''
};
if(_fkQuoteId) payload.id = _fkQuoteId;
try {
var res = await fetch('/api/quotes.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
var data = await res.json();
if(data.error) { alert('Fel: '+data.error); return; }
_fkQuoteId = data.id || _fkQuoteId;
alert(status==='offert'?'Offert skapad!':'Kalkyl sparad!');
} catch(e) { alert('Fel: '+e.message); }
}
// =============================================
// === GENERISK KALKYL-RAMVERK (alla nya) ===
// =============================================
const CFG_FINANCE_RATE = 0.049;
function cfgMonthly(total, years) {
if(!total||!years) return 0;
const r=CFG_FINANCE_RATE/12, n=years*12;
return Math.round(total*r*Math.pow(1+r,n)/(Math.pow(1+r,n)-1));
}
function cfgDeduct(subtotal, laborRatio, type, owners) {
const maxPerOwner=50000, maxTotal=owners*maxPerOwner;
if(type==='green') return Math.min(Math.round(subtotal*0.20), maxTotal);
if(type==='rot') return Math.min(Math.round(subtotal*laborRatio*0.30), maxTotal);
return 0;
}
function cfgSetBtns(prefix, cls, activeVal) {
document.querySelectorAll('.'+cls).forEach(b=>{
const v=b.dataset.dt||b.dataset.oc||b.dataset.yr;
if(v===String(activeVal)){b.style.background=prefix==='fin'?'#3b82f6':'#059669';b.style.color='#fff';b.style.borderColor=prefix==='fin'?'#3b82f6':'#059669';}
else{b.style.background='#fff';b.style.color=prefix==='fin'?'#3b82f6':'#059669';b.style.borderColor=prefix==='fin'?'#bfdbfe':'#bbf7d0';}
});
}
// === BATTERI (FRISTÅENDE) KONFIGURATOR ===
let _batBrand=null, _batSize=null, _batSizePrice=0, _batAddons={}, _batDeduction='green', _batOwners=1, _batFinYears=15;
const BAT_BRANDS=[
{id:'saj',name:'SAJ HS3',desc:'Med VR',sizes:[{kwh:5,price:55000},{kwh:10,price:85000},{kwh:15,price:115000}]},
{id:'sigenstor',name:'SigenStor',desc:'Med VR',sizes:[{kwh:5,price:52000},{kwh:10,price:80000},{kwh:15,price:110000}]},
{id:'emaldo',name:'Emaldo',desc:'Utan VR',sizes:[{kwh:5,price:45000},{kwh:10,price:70000},{kwh:15,price:95000}]}
];
const BAT_ADDONS=[
{id:'vr',name:'VR (virtuell reservkraft)',price:5000,green:true},
{id:'smartmeter',name:'Smart Meter',price:3000,green:true},
{id:'extra_cable',name:'Extra kablage',price:2500,green:false}
];
function initBatConfig(){
renderBatBrands();
renderBatAddons();
updateBatCalc();
}
function renderBatBrands(){
const c=document.getElementById('batBrandCards');if(!c)return;
c.innerHTML=BAT_BRANDS.map(b=>`
<div onclick="selectBatBrand('${b.id}')" style="padding:14px 16px;border:2px solid ${_batBrand===b.id?'#3b82f6':'#e5e7eb'};border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:${_batBrand===b.id?'#f0f9ff':'#fff'}">
<div style="width:20px;height:20px;border:2px solid ${_batBrand===b.id?'#3b82f6':'#d1d5db'};border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0">${_batBrand===b.id?'<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>':''}</div>
<div style="flex:1"><div style="font-weight:600;font-size:14px">${b.name}</div><div style="font-size:11px;color:#64748b">${b.desc}</div></div>
</div>`).join('');
}
function selectBatBrand(id){_batBrand=id;_batSize=null;_batSizePrice=0;renderBatBrands();renderBatSizes();updateBatCalc();}
function renderBatSizes(){
const c=document.getElementById('batSizeCards');if(!c)return;
const brand=BAT_BRANDS.find(b=>b.id===_batBrand);
if(!brand){c.innerHTML='<div style="color:#94a3b8;text-align:center;padding:20px">Välj ett märke först</div>';return;}
c.innerHTML=brand.sizes.map(s=>`
<div onclick="selectBatSize(${s.kwh},${s.price})" style="padding:14px 16px;border:2px solid ${_batSize===s.kwh?'#3b82f6':'#e5e7eb'};border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:${_batSize===s.kwh?'#f0f9ff':'#fff'}">
<div style="width:20px;height:20px;border:2px solid ${_batSize===s.kwh?'#3b82f6':'#d1d5db'};border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0">${_batSize===s.kwh?'<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>':''}</div>
<div style="flex:1"><div style="font-weight:600;font-size:14px">${s.kwh} kWh</div></div>
<div style="font-weight:700;font-size:14px;color:#1a1a1a">${fmt(s.price)}</div>
</div>`).join('');
}
function selectBatSize(kwh,price){_batSize=kwh;_batSizePrice=price;renderBatSizes();updateBatCalc();}
function renderBatAddons(){
const c=document.getElementById('batAddonCards');if(!c)return;
c.innerHTML=BAT_ADDONS.map(a=>`
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;border:1.5px solid ${_batAddons[a.id]?'#a855f7':'#e5e7eb'};border-radius:10px;background:${_batAddons[a.id]?'#faf5ff':'#fff'}">
<input type="checkbox" ${_batAddons[a.id]?'checked':''} onchange="toggleBatAddon('${a.id}',${a.price},this.checked)" style="accent-color:#a855f7;width:18px;height:18px">
<div style="flex:1"><div style="font-weight:600;font-size:13px">${a.name}</div>${a.green?'<div style="font-size:10px;color:#059669">✓ Grön teknik-berättigad</div>':''}</div>
<div style="font-weight:700;font-size:13px">${fmt(a.price)}</div>
</div>`).join('');
}
function toggleBatAddon(id,price,checked){if(checked)_batAddons[id]=price;else delete _batAddons[id];renderBatAddons();updateBatCalc();}
function setBatDeduction(t){_batDeduction=t;cfgSetBtns('deduct','bat-deduct-btn',t);updateBatCalc();}
function setBatOwners(n){_batOwners=n;cfgSetBtns('owner','bat-owner-btn',n);updateBatCalc();}
function setBatFinYears(y){_batFinYears=y;cfgSetBtns('fin','bat-fin-btn',y);updateBatCalc();}
function updateBatCalc(){
const addonsTotal=Object.values(_batAddons).reduce((s,v)=>s+v,0);
const subtotal=_batSizePrice+addonsTotal;
const deduct=cfgDeduct(subtotal,0.35,_batDeduction,_batOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_batFinYears);
document.getElementById('batPrBattery').textContent=fmt(_batSizePrice);
document.getElementById('batPrAddons').textContent=fmt(addonsTotal);
document.getElementById('batPrSubtotal').textContent=fmt(subtotal);
document.getElementById('batDeductLabel').textContent=_batDeduction==='green'?'GRÖNT TEKNIK-AVDRAG':_batDeduction==='rot'?'ROT-AVDRAG':'INGET AVDRAG';
document.getElementById('batDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('batPrTotal').textContent=fmt(total);
document.getElementById('batFinInfo').textContent='('+_batFinYears+' år, 4.9%)';
document.getElementById('batPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === LADDBOX KONFIGURATOR ===
let _lbSelected=null, _lbPrice=0, _lbDeduction='green', _lbOwners=1, _lbFinYears=15;
const LB_CHARGERS=[
{id:'easee',brand:'Easee',model:'Home',price:12500,desc:'22kW, WiFi, lastbalansering',green:true},
{id:'zaptec',brand:'Zaptec',model:'Go',price:9900,desc:'22kW, kompakt design',green:true},
{id:'charge_amps',brand:'Charge Amps',model:'Halo',price:14900,desc:'22kW, premium, RFID',green:true},
{id:'garo',brand:'GARO',model:'Entity Pro',price:11500,desc:'22kW, robust, utomhus',green:true}
];
function initLbConfig(){renderLbChargers();updateLbCalc();}
function renderLbChargers(){
const c=document.getElementById('lbChargerCards');if(!c)return;
c.innerHTML=LB_CHARGERS.map(ch=>`
<div onclick="selectLbCharger('${ch.id}',${ch.price})" style="padding:16px;border:2px solid ${_lbSelected===ch.id?'#3b82f6':'#e5e7eb'};border-radius:10px;cursor:pointer;background:${_lbSelected===ch.id?'#f0f9ff':'#fff'}">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-weight:600;font-size:15px">${ch.brand} ${ch.model}</div>
<div style="font-size:12px;color:#64748b;margin-top:2px">${ch.desc}</div>
${ch.green?'<div style="font-size:10px;color:#059669;margin-top:4px">✓ Berättigar till grön teknik-avdrag</div>':''}
</div>
<div style="text-align:right">
<div style="font-size:18px;font-weight:700">${fmt(ch.price)}</div>
${_lbSelected===ch.id?'<div style="color:#3b82f6;font-size:18px;margin-top:2px">✓</div>':''}
</div>
</div>
</div>`).join('');
}
function selectLbCharger(id,price){if(_lbSelected===id){_lbSelected=null;_lbPrice=0;}else{_lbSelected=id;_lbPrice=price;}renderLbChargers();updateLbCalc();}
function setLbDeduction(t){_lbDeduction=t;cfgSetBtns('deduct','lb-deduct-btn',t);updateLbCalc();}
function setLbOwners(n){_lbOwners=n;cfgSetBtns('owner','lb-owner-btn',n);updateLbCalc();}
function setLbFinYears(y){_lbFinYears=y;cfgSetBtns('fin','lb-fin-btn',y);updateLbCalc();}
function updateLbCalc(){
const subtotal=_lbPrice;
const deduct=cfgDeduct(subtotal,0,_lbDeduction,_lbOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_lbFinYears);
document.getElementById('lbPrCharger').textContent=fmt(subtotal);
document.getElementById('lbPrSubtotal').textContent=fmt(subtotal);
document.getElementById('lbDeductLabel').textContent=_lbDeduction==='green'?'GRÖNT TEKNIK-AVDRAG':'INGET AVDRAG';
document.getElementById('lbDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('lbPrTotal').textContent=fmt(total);
document.getElementById('lbFinInfo').textContent='('+_lbFinYears+' år, 4.9%)';
document.getElementById('lbPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === TAKTVÄTT KONFIGURATOR ===
let _ttSelectedType=null, _ttPricePerSqm=0, _ttDeduction='rot', _ttOwners=1, _ttFinYears=15;
const TT_TYPES=[
{id:'standard',name:'Standardtvätt',price_sqm:45,desc:'Högtryckstvätt + mossbehandling'},
{id:'premium',name:'Premiumtvätt',price_sqm:65,desc:'Tvätt + impregnering + mossbehandling'},
{id:'gentle',name:'Skonsam tvätt',price_sqm:55,desc:'Lågtryck för känsliga tak'}
];
function initTtConfig(){renderTtTypes();updateTtCalc();}
function renderTtTypes(){
const c=document.getElementById('ttTypeCards');if(!c)return;
c.innerHTML=TT_TYPES.map(t=>`
<div onclick="selectTtType('${t.id}',${t.price_sqm})" style="padding:14px 16px;border:2px solid ${_ttSelectedType===t.id?'#3b82f6':'#e5e7eb'};border-radius:10px;cursor:pointer;display:flex;align-items:center;gap:12px;background:${_ttSelectedType===t.id?'#f0f9ff':'#fff'}">
<div style="width:20px;height:20px;border:2px solid ${_ttSelectedType===t.id?'#3b82f6':'#d1d5db'};border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0">${_ttSelectedType===t.id?'<div style="width:10px;height:10px;background:#3b82f6;border-radius:50%"></div>':''}</div>
<div style="flex:1"><div style="font-weight:600;font-size:14px">${t.name}</div><div style="font-size:11px;color:#64748b">${t.desc}</div></div>
<div style="font-weight:700;font-size:14px;color:#1a1a1a">${t.price_sqm} kr/m²</div>
</div>`).join('');
}
function selectTtType(id,priceSqm){_ttSelectedType=id;_ttPricePerSqm=priceSqm;renderTtTypes();updateTtCalc();}
function setTtDeduction(t){_ttDeduction=t;cfgSetBtns('deduct','tt-deduct-btn',t);updateTtCalc();}
function setTtOwners(n){_ttOwners=n;cfgSetBtns('owner','tt-owner-btn',n);updateTtCalc();}
function setTtFinYears(y){_ttFinYears=y;cfgSetBtns('fin','tt-fin-btn',y);updateTtCalc();}
function updateTtCalc(){
const area=parseInt(document.getElementById('ttArea')?.value)||0;
const subtotal=area*_ttPricePerSqm;
const deduct=cfgDeduct(subtotal,1.0,_ttDeduction,_ttOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_ttFinYears);
document.getElementById('ttPrDesc').textContent=area?area+' m² × '+_ttPricePerSqm+' kr':'';
document.getElementById('ttPrWash').textContent=fmt(subtotal);
document.getElementById('ttPrSubtotal').textContent=fmt(subtotal);
document.getElementById('ttDeductLabel').textContent=_ttDeduction==='rot'?'ROT-AVDRAG':'INGET AVDRAG';
document.getElementById('ttDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('ttPrTotal').textContent=fmt(total);
document.getElementById('ttFinInfo').textContent='('+_ttFinYears+' år, 4.9%)';
document.getElementById('ttPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === VÄRMEPUMP KONFIGURATOR ===
let _vpSelected=null, _vpPrice=0, _vpInstall=0, _vpDeduction='green', _vpOwners=1, _vpFinYears=15;
const VP_PUMPS=[
{id:'mitsu_25',brand:'Mitsubishi',model:'MSZ-LN25',kw:2.5,price:18900,install:8500,green:true,desc:'Värmefaktor 4.2'},
{id:'mitsu_35',brand:'Mitsubishi',model:'MSZ-LN35',kw:3.5,price:22500,install:8500,green:true,desc:'Värmefaktor 4.0'},
{id:'daikin_25',brand:'Daikin',model:'Stylish 25',kw:2.5,price:17500,install:8500,green:true,desc:'Värmefaktor 4.1'},
{id:'daikin_35',brand:'Daikin',model:'Stylish 35',kw:3.5,price:21000,install:8500,green:true,desc:'Värmefaktor 3.9'},
{id:'samsung_25',brand:'Samsung',model:'WindFree 25',kw:2.5,price:15900,install:8500,green:true,desc:'Vindstill teknik'}
];
function initVpConfig(){renderVpPumps();updateVpCalc();}
function renderVpPumps(){
const c=document.getElementById('vpPumpCards');if(!c)return;
c.innerHTML=VP_PUMPS.map(p=>`
<div onclick="selectVpPump('${p.id}',${p.price},${p.install})" style="padding:16px;border:2px solid ${_vpSelected===p.id?'#3b82f6':'#e5e7eb'};border-radius:10px;cursor:pointer;background:${_vpSelected===p.id?'#f0f9ff':'#fff'}">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<div style="font-weight:600;font-size:15px">${p.brand} ${p.model}</div>
<div style="font-size:12px;color:#64748b;margin-top:2px">${p.kw} kW — ${p.desc}</div>
${p.green?'<div style="font-size:10px;color:#059669;margin-top:4px">✓ Berättigar till grön teknik-avdrag</div>':''}
</div>
<div style="text-align:right">
<div style="font-size:18px;font-weight:700">${fmt(p.price)}</div>
<div style="font-size:11px;color:#64748b">+ ${fmt(p.install)} installation</div>
${_vpSelected===p.id?'<div style="color:#3b82f6;font-size:18px;margin-top:2px">✓</div>':''}
</div>
</div>
</div>`).join('');
}
function selectVpPump(id,price,install){if(_vpSelected===id){_vpSelected=null;_vpPrice=0;_vpInstall=0;}else{_vpSelected=id;_vpPrice=price;_vpInstall=install;}renderVpPumps();updateVpCalc();}
function setVpDeduction(t){_vpDeduction=t;cfgSetBtns('deduct','vp-deduct-btn',t);updateVpCalc();}
function setVpOwners(n){_vpOwners=n;cfgSetBtns('owner','vp-owner-btn',n);updateVpCalc();}
function setVpFinYears(y){_vpFinYears=y;cfgSetBtns('fin','vp-fin-btn',y);updateVpCalc();}
function updateVpCalc(){
const subtotal=_vpPrice+_vpInstall;
const laborRatio=_vpInstall/Math.max(subtotal,1);
const deduct=cfgDeduct(subtotal,laborRatio,_vpDeduction,_vpOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_vpFinYears);
document.getElementById('vpPrPump').textContent=fmt(_vpPrice);
document.getElementById('vpPrInstall').textContent=fmt(_vpInstall);
document.getElementById('vpPrSubtotal').textContent=fmt(subtotal);
document.getElementById('vpDeductLabel').textContent=_vpDeduction==='green'?'GRÖNT TEKNIK-AVDRAG':_vpDeduction==='rot'?'ROT-AVDRAG':'INGET AVDRAG';
document.getElementById('vpDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('vpPrTotal').textContent=fmt(total);
document.getElementById('vpFinInfo').textContent='('+_vpFinYears+' år, 4.9%)';
document.getElementById('vpPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === TAK KONFIGURATOR ===
let _takYtor=[], _takTillbehor={}, _takDeduction='rot', _takOwners=1, _takFinYears=15;
const TAK_TYPER=[{v:'Sadeltak',l:'Sadeltak'},{v:'Pulpettak',l:'Pulpettak'},{v:'Mansardtak',l:'Mansardtak'},{v:'Platt tak',l:'Platt tak'}];
const TAK_MATERIAL=[
{v:'Betongpannor',l:'Betongpannor',price_sqm:850},
{v:'Tegelpannor',l:'Tegelpannor',price_sqm:1200},
{v:'Plåttak',l:'Plåttak',price_sqm:650},
{v:'Papptak',l:'Papptak',price_sqm:450}
];
const TAK_FARGER=['Svart','Tegelröd','Mörkgrå','Brun','Grön'];
const TAK_TILLBEHOR=[
{id:'takstege',name:'Takstege',price:3500,unit:'st'},
{id:'snoglidar',name:'Snöglidare',price:180,unit:'löpmeter'},
{id:'ventilation',name:'Ventilationshuv',price:2800,unit:'st'},
{id:'takfönster',name:'Takfönster VELUX',price:8900,unit:'st'},
{id:'hangranna',name:'Hängrännor',price:320,unit:'löpmeter'},
{id:'stuprör',name:'Stuprör',price:280,unit:'löpmeter'}
];
function initTakConfig(){renderTakYtor();renderTakTillbehor();updateTakCalc();}
let _takNextId=1;
function addTakYta(){_takYtor.push({id:_takNextId++,area:0,type:'',material:'',color:''});renderTakYtor();updateTakCalc();}
function removeTakYta(id){_takYtor=_takYtor.filter(y=>y.id!==id);renderTakYtor();updateTakCalc();}
function updateTakYta(id,field,val){const y=_takYtor.find(y=>y.id===id);if(y){y[field]=field==='area'?parseInt(val)||0:val;}updateTakCalc();}
function renderTakYtor(){
const c=document.getElementById('takYtorContainer');if(!c)return;
if(!_takYtor.length){c.innerHTML='<div style="text-align:center;padding:30px;border:2px dashed #e5e7eb;border-radius:10px;color:#94a3b8"><p style="font-size:14px;font-weight:600;margin:0 0 8px">Inga takytor tillagda ännu</p><button onclick="addTakYta()" style="padding:8px 16px;background:#f0f9ff;border:1.5px solid #3b82f6;border-radius:8px;color:#3b82f6;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till första takytan</button></div>';return;}
c.innerHTML=_takYtor.map((y,i)=>`
<div style="border:1px solid #e5e7eb;border-radius:10px;padding:16px;background:#f8fafc">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-weight:600;font-size:14px">Takyta ${i+1}</span>
<button onclick="removeTakYta(${y.id})" style="background:none;border:none;cursor:pointer;color:#ef4444;font-size:16px">✕</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Takyta (m²)</label><input type="number" value="${y.area||''}" placeholder="100" oninput="updateTakYta(${y.id},'area',this.value)" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>
<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Taktyp</label><select onchange="updateTakYta(${y.id},'type',this.value)" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff"><option value="">Välj...</option>${TAK_TYPER.map(t=>'<option value="'+t.v+'"'+(y.type===t.v?' selected':'')+'>'+t.l+'</option>').join('')}</select></div>
<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Material</label><select onchange="updateTakYta(${y.id},'material',this.value)" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff"><option value="">Välj...</option>${TAK_MATERIAL.map(m=>'<option value="'+m.v+'"'+(y.material===m.v?' selected':'')+'>'+m.l+' — '+m.price_sqm+' kr/m²</option>').join('')}</select></div>
<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Färg</label><select onchange="updateTakYta(${y.id},'color',this.value)" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;background:#fff"><option value="">Välj...</option>${TAK_FARGER.map(f=>'<option value="'+f+'"'+(y.color===f?' selected':'')+'>'+f+'</option>').join('')}</select></div>
</div>
</div>`).join('');
}
function renderTakTillbehor(){
const c=document.getElementById('takTillbehorCards');if(!c)return;
c.innerHTML=TAK_TILLBEHOR.map(t=>`
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;border:1.5px solid ${_takTillbehor[t.id]?'#3b82f6':'#e5e7eb'};border-radius:10px;background:${_takTillbehor[t.id]?'#f0f9ff':'#fff'}">
<input type="checkbox" ${_takTillbehor[t.id]?'checked':''} onchange="toggleTakTillb('${t.id}',this.checked)" style="accent-color:#3b82f6;width:18px;height:18px">
<div style="flex:1"><div style="font-weight:600;font-size:13px">${t.name}</div><div style="font-size:10px;color:#64748b">${fmt(t.price)}/${t.unit}</div></div>
${_takTillbehor[t.id]?'<input type="number" min="1" value="'+(_takTillbehor[t.id]?.qty||1)+'" oninput="updateTakTillbQty(\''+t.id+'\',this.value)" style="width:60px;padding:6px;border:1.5px solid #e5e7eb;border-radius:6px;font-size:13px;text-align:center;font-family:inherit">':''}
</div>`).join('');
}
function toggleTakTillb(id,checked){
if(checked){const t=TAK_TILLBEHOR.find(x=>x.id===id);_takTillbehor[id]={price:t.price,qty:1};}else delete _takTillbehor[id];
renderTakTillbehor();updateTakCalc();
}
function updateTakTillbQty(id,val){if(_takTillbehor[id])_takTillbehor[id].qty=parseInt(val)||1;updateTakCalc();}
function setTakDeduction(t){_takDeduction=t;cfgSetBtns('deduct','tak-deduct-btn',t);updateTakCalc();}
function setTakOwners(n){_takOwners=n;cfgSetBtns('owner','tak-owner-btn',n);updateTakCalc();}
function setTakFinYears(y){_takFinYears=y;cfgSetBtns('fin','tak-fin-btn',y);updateTakCalc();}
function updateTakCalc(){
let ytorTotal=0;
_takYtor.forEach(y=>{const m=TAK_MATERIAL.find(x=>x.v===y.material);if(m&&y.area)ytorTotal+=y.area*m.price_sqm;});
let tillbTotal=0;
Object.values(_takTillbehor).forEach(t=>{tillbTotal+=t.price*(t.qty||1);});
const subtotal=ytorTotal+tillbTotal;
const deduct=cfgDeduct(subtotal,0.50,_takDeduction,_takOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_takFinYears);
document.getElementById('takPrYtor').textContent=fmt(ytorTotal);
document.getElementById('takPrTillb').textContent=fmt(tillbTotal);
document.getElementById('takPrSubtotal').textContent=fmt(subtotal);
document.getElementById('takDeductLabel').textContent=_takDeduction==='rot'?'ROT-AVDRAG':'INGET AVDRAG';
document.getElementById('takDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('takPrTotal').textContent=fmt(total);
document.getElementById('takFinInfo').textContent='('+_takFinYears+' år, 4.9%)';
document.getElementById('takPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === ISOLERING KONFIGURATOR ===
let _isoDeduction='rot', _isoOwners=1, _isoFinYears=15;
function initIsoConfig(){updateIsoCalc();}
function setIsoDeduction(t){_isoDeduction=t;cfgSetBtns('deduct','iso-deduct-btn',t);updateIsoCalc();}
function setIsoOwners(n){_isoOwners=n;cfgSetBtns('owner','iso-owner-btn',n);updateIsoCalc();}
function setIsoFinYears(y){_isoFinYears=y;cfgSetBtns('fin','iso-fin-btn',y);updateIsoCalc();}
function updateIsoCalc(){
const area=parseInt(document.getElementById('isoArea')?.value)||0;
const type=document.getElementById('isoType')?.value||'';
const thickness=parseInt(document.getElementById('isoThickness')?.value)||0;
const price=parseInt(document.getElementById('isoPrice')?.value)||0;
const subtotal=price;
const deduct=cfgDeduct(subtotal,0.50,_isoDeduction,_isoOwners);
const total=subtotal-deduct;
const monthly=cfgMonthly(total,_isoFinYears);
document.getElementById('isoPrDesc').textContent=area?area+' m², '+(type||'?')+', '+thickness+'mm':'';
document.getElementById('isoPrMaterial').textContent=fmt(subtotal);
document.getElementById('isoPrSubtotal').textContent=fmt(subtotal);
document.getElementById('isoDeductLabel').textContent=_isoDeduction==='rot'?'ROT-AVDRAG':'INGET AVDRAG';
document.getElementById('isoDeductKr').textContent=deduct?'-'+fmt(deduct):'0 kr';
document.getElementById('isoPrTotal').textContent=fmt(total);
document.getElementById('isoFinInfo').textContent='('+_isoFinYears+' år, 4.9%)';
document.getElementById('isoPrMonthly').textContent=fmt(monthly)+'/mån';
}
// === GENERISK SPARA/AFFÄR FÖR NYA KONFIGURATORER ===
async function saveCfgQuote(category) {
const configData = collectCfgData(category);
const payload = {
category: category,
config_data: JSON.stringify(configData),
total_price: configData.total,
panel_name: configData.description || category,
panel_count: 0,
status: 'utkast',
created_by: gStaffId || null,
customer_name: pendingKalkylCustomer?.name || null,
customer_address: pendingKalkylCustomer?.address || null,
customer_email: pendingKalkylCustomer?.email || null,
customer_phone: pendingKalkylCustomer?.phone || null,
customer_personnummer: (pendingKalkylCustomer?.owner1 && pendingKalkylCustomer.owner1.pnr) || null,
owner_count: pendingKalkylCustomer?.ownerType === '2' ? 2 : (pendingKalkylCustomer?.ownerType === 'brf' ? 0 : 1),
prospect_data: pendingKalkylCustomer?.solarData || null
};
if(currentQuoteId) payload.id = currentQuoteId;
try {
const res = await fetch('/api/quotes.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const data = await res.json();
if(data.error){alert('Fel: '+data.error);return;}
if(data.id) currentQuoteId = data.id;
var pIdx2 = (pendingKalkylCustomer||{}).prospectIdx;
if(pIdx2 >= 0 && faltProspects[pIdx2]){
if(faltProspects[pIdx2].status !== 'offert') faltProspects[pIdx2].status = 'kalkyl';
if(!faltProspects[pIdx2].quoteIds) faltProspects[pIdx2].quoteIds = faltProspects[pIdx2].quoteId ? [faltProspects[pIdx2].quoteId] : [];
if(!faltProspects[pIdx2].quoteIds.includes(currentQuoteId)) faltProspects[pIdx2].quoteIds.push(currentQuoteId);
faltProspects[pIdx2].quoteId = currentQuoteId;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
}
alert('Kalkyl sparad!');
} catch(e){alert('Fel: '+e.message);}
}
function goToCfgAffar(category){alert('Affär-vy för '+category+' — kommer snart');}
function collectCfgData(cat) {
switch(cat) {
case 'batteri': {
const brand=BAT_BRANDS.find(b=>b.id===_batBrand);
return {description:(brand?brand.name:'')+' '+(_batSize||'')+'kWh',total:_batSizePrice+Object.values(_batAddons).reduce((s,v)=>s+v,0),battery_brand:_batBrand,battery_kwh:_batSize,battery_price:_batSizePrice,addons:_batAddons,deduction:_batDeduction,owners:_batOwners};
}
case 'laddbox': {
const ch=LB_CHARGERS.find(c=>c.id===_lbSelected);
return {description:ch?ch.brand+' '+ch.model:'',total:_lbPrice,charger_id:_lbSelected,charger_price:_lbPrice,deduction:_lbDeduction,owners:_lbOwners};
}
case 'taktvatt': {
const area=parseInt(document.getElementById('ttArea')?.value)||0;
const typ=TT_TYPES.find(t=>t.id===_ttSelectedType);
return {description:(typ?typ.name:'')+' '+area+'m²',total:area*_ttPricePerSqm,type_id:_ttSelectedType,price_sqm:_ttPricePerSqm,area:area,deduction:_ttDeduction,owners:_ttOwners};
}
case 'varmepump': {
const p=VP_PUMPS.find(x=>x.id===_vpSelected);
return {description:p?p.brand+' '+p.model:'',total:_vpPrice+_vpInstall,pump_id:_vpSelected,pump_price:_vpPrice,install_price:_vpInstall,deduction:_vpDeduction,owners:_vpOwners};
}
case 'tak':
return {description:_takYtor.length+' takyta(or)',total:0,ytor:_takYtor,tillbehor:_takTillbehor,deduction:_takDeduction,owners:_takOwners};
case 'isolering': {
const area=parseInt(document.getElementById('isoArea')?.value)||0;
const price=parseInt(document.getElementById('isoPrice')?.value)||0;
return {description:(document.getElementById('isoType')?.value||'Isolering')+' '+area+'m²',total:price,area:area,type:document.getElementById('isoType')?.value,thickness:parseInt(document.getElementById('isoThickness')?.value)||0,price:price,deduction:_isoDeduction,owners:_isoOwners};
}
default: return {description:cat,total:0};
}
}
function closePoForm() {
var ov = document.getElementById('poFormOverlay');
if(ov) ov.remove();
loadInkopDeals();
}
async function saveMeasurements(silent) {
// Always save measurements to deal_measurements
try {
var r = await fetch('api/suppliers.php?action=save_measurements', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
deal_id: _poFormDealId,
measurements: _poFormPositions
})
});
var res = await r.json();
if(!silent) {
if(res.success) alert('Inmätning sparad! ('+res.saved+' positioner)');
else alert(res.error||'Fel vid sparning');
}
return res.success;
} catch(e) {
if(!silent) alert('Fel: '+e.message);
return false;
}
}
async function savePoForm() {
var suppId = document.getElementById('poLeverantor').value;
if(!suppId) { alert('Välj leverantör under Leverantörsorder'); return; }
// Save measurements first
await saveMeasurements(true);
// Collect positions with data
var items = [];
_poFormPositions.forEach(function(p){
if(!p.product || !p.width || !p.height) return;
items.push({
article_name: p.product + ' ' + p.width + 'x' + p.height,
category: p.product,
quantity: parseInt(p.pcs)||1,
unit: 'st',
width: parseInt(p.width)||null,
height: parseInt(p.height)||null,
notes: [p.model, p.color_in, p.color_out, p.glass, p.profile].filter(Boolean).join(', ')
});
});
// Collect Hedlunds items
_poHedlundsRows.forEach(function(r){
if(!r.article || !r.quantity) return;
items.push({
article_name: r.article,
category: r.category,
quantity: parseInt(r.quantity)||1,
unit: 'st',
length: parseInt(r.length)||null,
notes: r.extra||null,
unit_price: r.price_per_m ? (parseInt(r.length||0)/1000 * parseFloat(r.price_per_m)) : 0
});
});
var notes = [
'Kund: ' + (document.getElementById('poKundNamn').value||''),
'Adress: ' + (document.getElementById('poKundAdress').value||''),
'Montering: ' + (document.getElementById('poKundMontering').value||''),
'Byggnadsår: ' + (document.getElementById('poKundByggnar').value||''),
document.getElementById('poKontrollNotes').value||''
].filter(Boolean).join('\n');
try {
var r = await fetch('api/suppliers.php?action=create_order', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
deal_id: _poFormDealId,
supplier_id: parseInt(suppId),
order_date: document.getElementById('poOrderDatum').value,
expected_delivery: document.getElementById('poLevDatum').value||null,
notes: notes,
status: 'draft',
items: items
})
});
var res = await r.json();
if(res.success) {
closePoForm();
showOrderDetail(res.id);
} else { alert(res.error||'Fel'); }
} catch(e) { alert('Fel: '+e.message); }
}
async function createBatchPo() {
var dealIds = Object.keys(_inkopSelectedDeals).map(Number);
if(!dealIds.length) return;
var suppOpts = (_inkopSuppliers||[]).map(function(s){ return '<option value="'+s.id+'">'+s.name+(s.category?' ('+s.category+')':'')+'</option>'; }).join('');
var dealInfo = dealIds.map(function(id){ var d = _inkopDealsData.find(function(x){return x.deal_id==id;}); return d ? (d.deal_number||'?')+' - '+(d.customer_name||'?') : '?'; }).join('<br>');
var html = '<div style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:40px 20px;overflow-y:auto" onclick="if(event.target===this)this.remove()" id="batchPoOverlay">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:600px;box-shadow:0 20px 60px rgba(0,0,0,.2);padding:24px">'
+'<h2 style="font-size:18px;font-weight:700;margin:0 0 4px">Batch-inköpsorder</h2>'
+'<div style="font-size:13px;color:#64748b;margin-bottom:12px">'+dealIds.length+' affärer valda - varje affär får en egen inköpsorder</div>'
+'<div style="background:#f8f9fa;border-radius:8px;padding:10px 14px;font-size:12px;margin-bottom:16px;max-height:150px;overflow-y:auto;line-height:1.6">'+dealInfo+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Leverantör (alla)</label>'
+'<select id="batchSupplier" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px">'
+'<option value="">Välj leverantör...</option>'+suppOpts+'</select></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Orderdatum</label>'
+'<input type="date" id="batchDate" value="'+new Date().toISOString().split('T')[0]+'" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Förväntad leverans</label>'
+'<input type="date" id="batchDelivery" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'</div>'
+'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">'
+'<button onclick="document.getElementById(\'batchPoOverlay\').remove()" style="padding:8px 20px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;font-size:13px;cursor:pointer">Avbryt</button>'
+'<button id="batchCreateBtn" onclick="executeBatchPo()" style="padding:8px 20px;border-radius:8px;border:none;background:#024550;color:#fff;font-size:13px;font-weight:600;cursor:pointer">Skapa '+dealIds.length+' ordrar</button>'
+'</div></div></div>';
document.body.insertAdjacentHTML('beforeend', html);
}
async function executeBatchPo() {
var suppId = document.getElementById('batchSupplier').value;
if(!suppId) { alert('Välj leverantör'); return; }
var btn = document.getElementById('batchCreateBtn');
if(btn) { btn.disabled = true; btn.textContent = 'Skapar...'; }
var dealIds = Object.keys(_inkopSelectedDeals).map(Number);
var created = 0;
for(var i = 0; i < dealIds.length; i++) {
try {
var r = await fetch('api/suppliers.php?action=create_order', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
deal_id: dealIds[i],
supplier_id: parseInt(suppId),
order_date: document.getElementById('batchDate').value,
expected_delivery: document.getElementById('batchDelivery').value || null,
status: 'draft'
})
});
var res = await r.json();
if(res.success) created++;
} catch(e){}
}
var ov = document.getElementById('batchPoOverlay');
if(ov) ov.remove();
var bb = document.getElementById('inkopBatchBar');
if(bb) bb.remove();
_inkopSelectedDeals = {};
loadInkopDeals();
alert(created + ' inköpsordrar skapade!');
}
// ===== Inköpsordrar (Tab 2) =====
async function loadInkopOrders() {
try {
var r = await fetch('api/suppliers.php?action=orders&t='+Date.now());
_inkopOrders = await r.json();
renderInkopOrdersTab();
} catch(e) { console.error('loadInkopOrders error:', e); }
}
function filterInkopOrders() { loadInkopOrders(); }
function renderInkopOrdersTab() {
if(_inkopOrdersDT) { _inkopOrdersDT.destroy(); _inkopOrdersDT = null; }
var status = (document.getElementById('inkopOrderStatus')||{}).value||'';
var supplier = (document.getElementById('inkopOrderSupplier')||{}).value||'';
var filtered = _inkopOrders.filter(function(o){
if(status && o.status !== status) return false;
if(supplier && String(o.supplier_id) !== supplier) return false;
return true;
});
// Stats
var stats = {draft:0, ordered:0, confirmed:0, shipped:0, delivered:0, cancelled:0};
var totalVal = 0;
_inkopOrders.forEach(function(o){
stats[o.status] = (stats[o.status]||0) + 1;
if(o.status !== 'cancelled' && o.status !== 'draft') totalVal += parseFloat(o.total_amount||0);
});
var statsEl = document.getElementById('inkopOrderStats');
if(statsEl) statsEl.innerHTML = '<div style="background:linear-gradient(135deg,#024550,#036b78);border-radius:12px;padding:14px;color:#fff"><div style="font-size:10px;opacity:.7;text-transform:uppercase;letter-spacing:.5px">Totalt beställt</div><div style="font-size:20px;font-weight:800;margin-top:4px">'+fmtKr(totalVal)+'</div></div>'
+'<div style="background:#eff6ff;border:1px solid #93c5fd;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Aktiva</div><div style="font-size:20px;font-weight:800;color:#3b82f6;margin-top:4px">'+(stats.ordered+stats.confirmed+stats.shipped)+'</div></div>'
+'<div style="background:#f0fdf4;border:1px solid #86efac;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Levererade</div><div style="font-size:20px;font-weight:800;color:#16a34a;margin-top:4px">'+stats.delivered+'</div></div>'
+'<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px;padding:14px"><div style="font-size:10px;color:#64748b;text-transform:uppercase">Totalt</div><div style="font-size:20px;font-weight:800;color:#334155;margin-top:4px">'+_inkopOrders.length+'</div></div>';
var tb = document.getElementById('inkopOrdersBody');
if(!tb) return;
tb.innerHTML = filtered.map(function(o){
var sc = INKOP_STATUS_COLORS[o.status]||'#94a3b8';
return '<tr style="cursor:pointer" onclick="showOrderDetail('+o.id+')">'
+'<td style="font-weight:600;color:#024550">'+(o.order_number||'-')+'</td>'
+'<td>'+(o.supplier_name||'-')+'</td>'
+'<td>'+(o.customer_name||'')+(o.deal_number?' <span style="font-size:11px;color:#94a3b8">('+o.deal_number+')</span>':'')+'</td>'
+'<td style="color:#64748b">'+(o.order_date||'-')+'</td>'
+'<td style="color:#64748b">'+(o.expected_delivery||'-')+'</td>'
+'<td><span style="font-size:11px;padding:3px 10px;border-radius:10px;color:#fff;background:'+sc+';font-weight:600">'+(INKOP_STATUS_LABELS[o.status]||o.status)+'</span></td>'
+'<td style="text-align:right;font-weight:600">'+(o.total_amount?fmtKr(parseFloat(o.total_amount)):'-')+'</td>'
+'<td style="text-align:center">'+(o.item_count||0)+'</td>'
+'</tr>';
}).join('');
_inkopOrdersDT = $('#inkopOrdersDT').DataTable({
paging: true, pageLength: 50, order: [[3,'desc']],
language: { search:'Sök:', lengthMenu:'Visa _MENU_', info:'_START_-_END_ av _TOTAL_', paginate:{previous:'Förra',next:'Nästa'}, zeroRecords:'Inga inköpsordrar hittades' },
columnDefs: [{ targets:[6], className:'dt-right' }]
});
}
async function showOrderDetail(orderId) {
try {
var r = await fetch('api/suppliers.php?action=order&id='+orderId+'&t='+Date.now());
var o = await r.json();
if(o.error) { alert(o.error); return; }
var sc = INKOP_STATUS_COLORS[o.status]||'#94a3b8';
var itemsTotal = (o.items||[]).reduce(function(a,i){ return a+parseFloat(i.total_price||0); },0);
var html = '<div style="position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto;backdrop-filter:blur(2px)" onclick="if(event.target===this)this.remove()" id="orderDetailOverlay">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:900px;box-shadow:0 25px 80px rgba(0,0,0,.25)">'
+'<div style="padding:20px 28px;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center">'
+'<div>'
+'<h2 style="font-size:20px;font-weight:800;margin:0">'+(o.order_number||'Order')+'</h2>'
+'<div style="font-size:13px;color:#64748b;margin-top:2px">'+(o.supplier_name||'')+' \u2014 '+(o.customer_name||'Ingen kund')+(o.deal_number?' ('+o.deal_number+')':'')+'</div>'
+'</div>'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<span style="padding:4px 14px;border-radius:20px;font-size:12px;font-weight:600;color:#fff;background:'+sc+'">'+(INKOP_STATUS_LABELS[o.status]||o.status)+'</span>'
+'<button onclick="document.getElementById(\'orderDetailOverlay\').remove()" style="background:none;border:none;font-size:28px;cursor:pointer;color:#94a3b8">×</button>'
+'</div></div>'
+'<div style="padding:16px 28px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px;border-bottom:1px solid #f1f5f9">'
+'<div><div style="font-size:10px;color:#94a3b8;text-transform:uppercase">Orderdatum</div><div style="font-size:14px;font-weight:600">'+(o.order_date||'-')+'</div></div>'
+'<div><div style="font-size:10px;color:#94a3b8;text-transform:uppercase">Förv. leverans</div><div style="font-size:14px;font-weight:600">'+(o.expected_delivery||'-')+'</div></div>'
+'<div><div style="font-size:10px;color:#94a3b8;text-transform:uppercase">Levererad</div><div style="font-size:14px;font-weight:600">'+(o.actual_delivery||'-')+'</div></div>'
+'<div><div style="font-size:10px;color:#94a3b8;text-transform:uppercase">Totalbelopp</div><div style="font-size:18px;font-weight:800;color:#024550">'+(itemsTotal?fmtKr(itemsTotal):'-')+'</div></div>'
+'</div>'
+'<div style="padding:12px 28px;border-bottom:1px solid #f1f5f9;display:flex;gap:6px;flex-wrap:wrap">'
+['draft','ordered','confirmed','shipped','delivered'].map(function(s){
var active = o.status === s;
return '<button onclick="changeOrderStatus('+o.id+',\''+s+'\')" style="padding:5px 14px;border-radius:8px;font-size:11px;font-weight:600;border:1px solid '+(active?INKOP_STATUS_COLORS[s]:'#e5e7eb')+';background:'+(active?INKOP_STATUS_COLORS[s]:'#fff')+';color:'+(active?'#fff':'#334155')+';cursor:pointer">'+INKOP_STATUS_LABELS[s]+'</button>';
}).join('')
+'</div>'
+'<div style="padding:20px 28px">'
+'<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Orderrader ('+(o.items||[]).length+')</div>'
+((o.items||[]).length ? '<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f8f9fa">'
+'<th style="padding:8px;text-align:left;border-bottom:1px solid #e5e7eb">Artikel</th>'
+'<th style="padding:8px;text-align:left;border-bottom:1px solid #e5e7eb">Kategori</th>'
+'<th style="padding:8px;text-align:right;border-bottom:1px solid #e5e7eb">Antal</th>'
+'<th style="padding:8px;text-align:left;border-bottom:1px solid #e5e7eb">Enhet</th>'
+'<th style="padding:8px;text-align:right;border-bottom:1px solid #e5e7eb">À-pris</th>'
+'<th style="padding:8px;text-align:right;border-bottom:1px solid #e5e7eb">Summa</th>'
+'<th style="padding:8px;text-align:left;border-bottom:1px solid #e5e7eb">Mått</th>'
+'</tr></thead><tbody>'
+(o.items||[]).map(function(i){
var dims = [i.width?i.width+'mm':'', i.height?'h:'+i.height+'mm':'', i.length?'l:'+i.length+'mm':''].filter(Boolean).join(' ');
return '<tr><td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;font-weight:600">'+(i.article_name||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;color:#64748b">'+(i.category||'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;text-align:right;font-weight:600">'+(i.quantity||0)+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9">'+(i.unit||'st')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;text-align:right">'+(i.unit_price?fmtKr(parseFloat(i.unit_price)):'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;text-align:right;font-weight:600">'+(i.total_price?fmtKr(parseFloat(i.total_price)):'-')+'</td>'
+'<td style="padding:6px 8px;border-bottom:1px solid #f1f5f9;color:#94a3b8;font-size:11px">'+(dims||'-')+'</td></tr>';
}).join('')
+'</tbody></table>' : '<div style="color:#94a3b8;text-align:center;padding:20px">Inga rader ännu</div>')
+(o.notes?'<div style="margin-top:16px;padding:12px;background:#f8f9fa;border-radius:8px;font-size:12px;color:#64748b"><strong>Noteringar:</strong> '+o.notes+'</div>':'')
+'</div></div></div>';
document.body.insertAdjacentHTML('beforeend', html);
} catch(e) { console.error('showOrderDetail error:', e); }
}
async function changeOrderStatus(orderId, newStatus) {
try {
await fetch('api/suppliers.php?action=order_status', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({id: orderId, status: newStatus})
});
var ov = document.getElementById('orderDetailOverlay');
if(ov) ov.remove();
showOrderDetail(orderId);
if(_inkopCurrentTab === 'orders') loadInkopOrders();
else loadInkopDeals();
} catch(e) { console.error(e); }
}
// ============ LEVERANTÖRER ============
var _leverantorData = [];
async function loadLeverantorerPage() {
try {
var r = await fetch('api/suppliers.php?action=list&t='+Date.now());
_leverantorData = await r.json();
renderLeverantorer(_leverantorData);
} catch(e) { console.error('loadLeverantorerPage error:', e); }
}
function renderLeverantorer(suppliers) {
var container = document.getElementById('leverantorContainer');
if(!container) return;
if(!suppliers.length) { container.innerHTML = '<div style="text-align:center;color:#94a3b8;padding:40px;font-size:14px">Inga leverantörer</div>'; return; }
container.innerHTML = '<div style="display:flex;flex-direction:column;gap:12px">'
+suppliers.map(function(s, idx){
var catColor = '#64748b';
if((s.category||'').indexOf('Fönster')>=0 || (s.category||'').indexOf('Dörr')>=0) catColor = '#3b82f6';
else if((s.category||'').indexOf('Tillbehör')>=0) catColor = '#f97316';
else if((s.category||'').indexOf('Sol')>=0) catColor = '#eab308';
else if((s.category||'').indexOf('Tak')>=0) catColor = '#6b7280';
return '<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.06)">'
+'<div style="padding:16px 20px;display:flex;align-items:center;justify-content:space-between;cursor:pointer" onclick="toggleLevCard('+idx+','+s.id+')">'
+'<div style="display:flex;align-items:center;gap:12px">'
+'<div style="width:44px;height:44px;border-radius:10px;background:'+catColor+'15;color:'+catColor+';display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:800;flex-shrink:0">'+s.name.charAt(0)+'</div>'
+'<div>'
+'<div style="font-size:16px;font-weight:700;color:#1a1a1a">'+s.name+'</div>'
+'<div style="display:flex;align-items:center;gap:8px;margin-top:2px">'
+(s.category?'<span style="font-size:11px;font-weight:600;padding:2px 10px;border-radius:10px;background:'+catColor+'15;color:'+catColor+';border:1px solid '+catColor+'30">'+s.category+'</span>':'')
+'<span style="font-size:11px;color:#64748b">'+(s.article_count||0)+' artiklar</span>'
+'<span style="font-size:11px;color:#64748b">'+(s.order_count||0)+' ordrar</span>'
+(s.active_orders>0?'<span style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;background:#3b82f6;color:#fff">'+s.active_orders+' aktiva</span>':'')
+'</div></div></div>'
+'<div style="display:flex;align-items:center;gap:8px">'
+(s.total_ordered>0?'<span style="font-size:13px;font-weight:700;color:#024550">'+fmtKr(parseFloat(s.total_ordered))+'</span>':'')
+'<svg id="lev-arrow-'+idx+'" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2" style="flex-shrink:0;transition:transform .2s"><polyline points="6 9 12 15 18 9"/></svg>'
+'</div></div>'
+'<div id="lev-body-'+idx+'" style="max-height:0;overflow:hidden;transition:max-height .4s ease">'
+'<div style="border-top:1px solid #e5e7eb;padding:16px 20px">'
+'<div id="lev-content-'+idx+'" style="text-align:center;padding:16px;color:#94a3b8;font-size:13px">Laddar...</div>'
+'</div></div></div>';
}).join('')
+'</div>';
}
var _levExpanded = {};
async function toggleLevCard(idx, supplierId) {
var body = document.getElementById('lev-body-'+idx);
var arrow = document.getElementById('lev-arrow-'+idx);
if(!body) return;
var isOpen = body.style.maxHeight && body.style.maxHeight !== '0px';
if(isOpen) {
body.style.maxHeight = '0px';
if(arrow) arrow.style.transform = 'rotate(0deg)';
} else {
body.style.maxHeight = 'none';
if(arrow) arrow.style.transform = 'rotate(180deg)';
if(!_levExpanded[supplierId]) {
_levExpanded[supplierId] = true;
loadLevDetails(idx, supplierId);
}
}
}
async function loadLevDetails(idx, supplierId) {
var container = document.getElementById('lev-content-'+idx);
if(!container) return;
try {
var r = await fetch('api/suppliers.php?action=get&id='+supplierId+'&t='+Date.now());
var s = await r.json();
if(s.error) { container.innerHTML = '<div style="color:#ef4444">'+s.error+'</div>'; return; }
// Contact info
var contactHtml = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;margin-bottom:16px">';
if(s.contact_name) contactHtml += '<div style="padding:8px 12px;background:#f8f9fa;border-radius:8px;font-size:12px"><span style="color:#64748b">Kontakt:</span> <strong>'+s.contact_name+'</strong></div>';
if(s.phone) contactHtml += '<div style="padding:8px 12px;background:#f8f9fa;border-radius:8px;font-size:12px"><a href="tel:'+s.phone+'" style="color:#16a34a;text-decoration:none;font-weight:600">'+s.phone+'</a></div>';
if(s.email) contactHtml += '<div style="padding:8px 12px;background:#f8f9fa;border-radius:8px;font-size:12px"><a href="mailto:'+s.email+'" style="color:#3b82f6;text-decoration:none;font-weight:600">'+s.email+'</a></div>';
if(s.notes) contactHtml += '<div style="padding:8px 12px;background:#f8f9fa;border-radius:8px;font-size:12px;grid-column:1/-1"><span style="color:#64748b">Info:</span> '+s.notes+'</div>';
contactHtml += '</div>';
// Articles by category
var cats = {};
(s.articles||[]).forEach(function(a){ if(!cats[a.category]) cats[a.category]=[]; cats[a.category].push(a); });
var artHtml = '';
if(Object.keys(cats).length) {
artHtml = '<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Artikelkatalog ('+(s.articles||[]).length+')</div>';
Object.keys(cats).sort().forEach(function(cat){
var items = cats[cat];
artHtml += '<div style="margin-bottom:8px">'
+'<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;cursor:pointer" onclick="var t=this.nextElementSibling;t.style.display=t.style.display===\'none\'?\'block\':\'none\';this.querySelector(\'svg\').style.transform=t.style.display===\'none\'?\'rotate(-90deg)\':\'rotate(0deg)\'">'
+'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2.5" style="transition:transform .2s"><polyline points="6 9 12 15 18 9"/></svg>'
+'<span style="font-size:11px;font-weight:600;color:#334155">'+cat+'</span>'
+'<span style="font-size:10px;color:#94a3b8">('+items.length+')</span></div>'
+'<div style="display:none;max-height:400px;overflow-y:auto"><table style="width:100%;border-collapse:collapse;font-size:11px"><thead><tr style="background:#f8f9fa;position:sticky;top:0;z-index:1">'
+'<th style="padding:4px 6px;text-align:left;border-bottom:1px solid #e5e7eb">Artikel</th>'
+'<th style="padding:4px 6px;text-align:right;border-bottom:1px solid #e5e7eb">Bredd</th>'
+'<th style="padding:4px 6px;text-align:right;border-bottom:1px solid #e5e7eb">Pris</th>'
+'<th style="padding:4px 6px;text-align:left;border-bottom:1px solid #e5e7eb">Längd</th>'
+'</tr></thead><tbody>'
+items.map(function(a){
return '<tr><td style="padding:3px 6px;border-bottom:1px solid #f1f5f9">'+a.article_name+'</td>'
+'<td style="padding:3px 6px;border-bottom:1px solid #f1f5f9;text-align:right">'+(a.width?a.width+' mm':'-')+'</td>'
+'<td style="padding:3px 6px;border-bottom:1px solid #f1f5f9;text-align:right;font-weight:600">'+(a.unit_price?a.unit_price+' kr/m':'-')+'</td>'
+'<td style="padding:3px 6px;border-bottom:1px solid #f1f5f9;color:#64748b">'+(a.min_length&&a.max_length?a.min_length+'-'+a.max_length+' mm':(a.max_length?a.max_length+' mm':'-'))+'</td></tr>';
}).join('')
+'</tbody></table></div></div>';
});
}
// Recent orders
var ordHtml = '';
if((s.orders||[]).length) {
ordHtml = '<div style="font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin:16px 0 8px">Senaste ordrar</div>'
+'<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f8f9fa">'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb">Ordernr</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb">Kund</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb">Datum</th>'
+'<th style="padding:6px 8px;text-align:left;border-bottom:1px solid #e5e7eb">Status</th>'
+'<th style="padding:6px 8px;text-align:right;border-bottom:1px solid #e5e7eb">Belopp</th>'
+'</tr></thead><tbody>'
+s.orders.map(function(o){
var sc = INKOP_STATUS_COLORS[o.status]||'#94a3b8';
return '<tr style="cursor:pointer" onclick="showOrderDetail('+o.id+')">'
+'<td style="padding:5px 8px;border-bottom:1px solid #f1f5f9;font-weight:600;color:#024550">'+(o.order_number||'-')+'</td>'
+'<td style="padding:5px 8px;border-bottom:1px solid #f1f5f9">'+(o.customer_name||'-')+'</td>'
+'<td style="padding:5px 8px;border-bottom:1px solid #f1f5f9;color:#64748b">'+(o.order_date||'-')+'</td>'
+'<td style="padding:5px 8px;border-bottom:1px solid #f1f5f9"><span style="font-size:10px;padding:2px 8px;border-radius:8px;color:#fff;background:'+sc+'">'+INKOP_STATUS_LABELS[o.status]+'</span></td>'
+'<td style="padding:5px 8px;border-bottom:1px solid #f1f5f9;text-align:right;font-weight:600">'+(o.total_amount?fmtKr(parseFloat(o.total_amount)):'-')+'</td></tr>';
}).join('')
+'</tbody></table>';
}
container.innerHTML = contactHtml + artHtml + ordHtml;
// Update max-height
var body = document.getElementById('lev-body-'+idx);
if(body) body.style.maxHeight = body.scrollHeight + 'px';
} catch(e) {
container.innerHTML = '<div style="color:#ef4444">Fel: '+e.message+'</div>';
}
}
function showNewSupplierModal() {
var html = '<div style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:40px 20px;overflow-y:auto" onclick="if(event.target===this)this.remove()" id="newSupplierOverlay">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:500px;box-shadow:0 20px 60px rgba(0,0,0,.2);padding:24px">'
+'<h2 style="font-size:18px;font-weight:700;margin:0 0 16px">Ny leverantör</h2>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+'<div style="grid-column:1/-1"><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Företagsnamn *</label>'
+'<input type="text" id="newSuppName" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Kategori</label>'
+'<select id="newSuppCategory" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px">'
+'<option value="">Välj...</option><option value="Fönster & Dörrar">Fönster & Dörrar</option><option value="Tillbehör">Tillbehör</option><option value="Solpaneler">Solpaneler</option><option value="Batterier">Batterier</option><option value="Tak">Tak</option><option value="Värmepumpar">Värmepumpar</option><option value="Övrigt">Övrigt</option>'
+'</select></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Kontaktperson</label>'
+'<input type="text" id="newSuppContact" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Telefon</label>'
+'<input type="text" id="newSuppPhone" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">E-post</label>'
+'<input type="text" id="newSuppEmail" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;box-sizing:border-box"></div>'
+'</div>'
+'<div style="margin-top:12px"><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Noteringar</label>'
+'<textarea id="newSuppNotes" rows="2" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;resize:vertical;box-sizing:border-box"></textarea></div>'
+'<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px">'
+'<button onclick="document.getElementById(\'newSupplierOverlay\').remove()" style="padding:8px 20px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;font-size:13px;cursor:pointer">Avbryt</button>'
+'<button onclick="createSupplier()" style="padding:8px 20px;border-radius:8px;border:none;background:#024550;color:#fff;font-size:13px;font-weight:600;cursor:pointer">Skapa</button>'
+'</div></div></div>';
document.body.insertAdjacentHTML('beforeend', html);
}
async function createSupplier() {
var name = document.getElementById('newSuppName').value;
if(!name) { alert('Namn krävs'); return; }
try {
var r = await fetch('api/suppliers.php?action=save_supplier', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
name: name,
category: document.getElementById('newSuppCategory').value || null,
contact_name: document.getElementById('newSuppContact').value || null,
phone: document.getElementById('newSuppPhone').value || null,
email: document.getElementById('newSuppEmail').value || null,
notes: document.getElementById('newSuppNotes').value || null
})
});
var res = await r.json();
if(res.success) {
document.getElementById('newSupplierOverlay').remove();
loadLeverantorerPage();
} else { alert(res.error||'Fel'); }
} catch(e) { alert('Fel: '+e.message); }
}
function showNewDealModal() {
const staffOpts = (window.allAdminUsers||[]).filter(u=>u.role==='saljare'||u.role==='admin').map(u=>'<option value="'+u.id+'">'+u.name+'</option>').join('');
const html = '<div style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10000;display:flex;align-items:flex-start;justify-content:center;padding:40px 20px;overflow-y:auto" onclick="if(event.target===this)this.remove()" id="newDealOverlay">'
+'<div style="background:#fff;border-radius:16px;width:100%;max-width:600px;box-shadow:0 20px 60px rgba(0,0,0,.2);padding:24px">'
+'<h2 style="font-size:18px;font-weight:700;margin:0 0 16px">Ny affär</h2>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Kundnamn *</label><input id="ndName" type="text" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Personnummer</label><input id="ndPnr" type="text" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Telefon</label><input id="ndPhone" type="tel" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">E-post</label><input id="ndEmail" type="email" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div style="grid-column:1/-1"><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Adress *</label><input id="ndAddress" type="text" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Ort</label><input id="ndCity" type="text" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Fastighetsbeteckning</label><input id="ndFastighet" type="text" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Ordervärde ink moms</label><input id="ndValue" type="number" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Marginal ink moms</label><input id="ndMarginal" type="number" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Säljare 1</label><select id="ndSalj1" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"><option value="">Välj...</option>'+staffOpts+'</select></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Datum</label><input id="ndDate" type="date" value="'+new Date().toISOString().split('T')[0]+'" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div style="grid-column:1/-1"><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Vad är sålt?</label>'
+'<div style="display:flex;flex-wrap:wrap;gap:6px" id="ndProducts">'
+Object.entries(PRODUCT_LABELS).map(([k,v])=>'<label style="display:flex;align-items:center;gap:4px;font-size:12px;padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer"><input type="checkbox" value="'+k+'">'+v+'</label>').join('')
+'</div></div>'
+'<div style="grid-column:1/-1"><label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Övrig info</label><textarea id="ndInfo" rows="2" style="width:100%;padding:8px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box;resize:vertical"></textarea></div>'
+'</div>'
+'<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">'
+'<button onclick="document.getElementById(\'newDealOverlay\').remove()" style="padding:8px 16px;border:1.5px solid #e5e7eb;border-radius:8px;background:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Avbryt</button>'
+'<button onclick="submitNewDeal()" style="padding:8px 20px;border:none;border-radius:8px;background:#024550;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Skapa affär</button>'
+'</div></div></div>';
document.body.insertAdjacentHTML('beforeend', html);
}
async function submitNewDeal() {
const user = JSON.parse(localStorage.getItem('solarUser')||'{}');
const name = document.getElementById('ndName').value.trim();
const address = document.getElementById('ndAddress').value.trim();
if (!name) { alert('Kundnamn krävs'); return; }
const checkedProducts = [...document.querySelectorAll('#ndProducts input:checked')].map(cb => ({product_type: cb.value}));
const body = {
customer_name: name,
personnummer: document.getElementById('ndPnr').value.trim(),
phone: document.getElementById('ndPhone').value.trim(),
email: document.getElementById('ndEmail').value.trim(),
address: address,
city: document.getElementById('ndCity').value.trim(),
fastighetsbeteckning: document.getElementById('ndFastighet').value.trim(),
ordervarde_ink_moms: document.getElementById('ndValue').value || null,
total_marginal_ink_moms: document.getElementById('ndMarginal').value || null,
saljare1_id: document.getElementById('ndSalj1').value || null,
datum_salj: document.getElementById('ndDate').value || null,
ovrig_info: document.getElementById('ndInfo').value.trim(),
products: checkedProducts,
staff_id: user.id || null,
status: 'offert'
};
try {
const r = await fetch('api/deals.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const data = await r.json();
if (data.success) {
document.getElementById('newDealOverlay').remove();
loadDeals();
} else {
alert(data.error || 'Kunde inte skapa affär');
}
} catch(e) { alert('Fel: '+e.message); }
}
/* === KALENDER === */
let calYear=2026,calMonth=1;
const calDummyEvents={
'2026-02-18':['Inmätning - Andersson','Offertpresentation - Svensson','Intern planering'],
'2026-02-19':['Besiktning - Berg Fastigheter'],
'2026-02-20':['Kundmöte - Karlsson','Produktträning Optima'],
'2026-02-21':['Montering - Nilsson Renovering'],
'2026-02-24':['Offertarbete'],
'2026-02-25':['Kundbesök - Holm Bygg'],
'2026-02-27':['Inmätning - Pettersson & Söner']
};
const monthNames=['Januari','Februari','Mars','April','Maj','Juni','Juli','Augusti','September','Oktober','November','December'];
const dayNames=['Mån','Tis','Ons','Tor','Fre','Lör','Sön'];
function renderCalendar(){
const grid=document.getElementById('calGrid');
if(!grid)return;
document.getElementById('calMonthTitle').textContent=monthNames[calMonth]+' '+calYear;
let html=dayNames.map(d=>'<div class="cal-day-header">'+d+'</div>').join('');
const firstDay=new Date(calYear,calMonth,1).getDay();
const daysInMonth=new Date(calYear,calMonth+1,0).getDate();
const daysInPrev=new Date(calYear,calMonth,0).getDate();
const startDay=firstDay===0?6:firstDay-1;
for(let i=startDay-1;i>=0;i--){html+='<div class="cal-day other-month">'+(daysInPrev-i)+'</div>'}
const today=new Date();
for(let d=1;d<=daysInMonth;d++){
const dateStr=calYear+'-'+String(calMonth+1).padStart(2,'0')+'-'+String(d).padStart(2,'0');
const isToday=today.getDate()===d&&today.getMonth()===calMonth&&today.getFullYear()===calYear;
const hasEvent=calDummyEvents[dateStr];
html+='<div class="cal-day'+(isToday?' today':'')+(hasEvent?' has-event':'')+'">'+d+'</div>';
}
const totalCells=startDay+daysInMonth;
const remaining=totalCells%7===0?0:7-(totalCells%7);
for(let i=1;i<=remaining;i++){html+='<div class="cal-day other-month">'+i+'</div>'}
grid.innerHTML=html;
}
function calPrev(){calMonth--;if(calMonth<0){calMonth=11;calYear--}renderCalendar()}
function calNext(){calMonth++;if(calMonth>11){calMonth=0;calYear++}renderCalendar()}
/* Init pages when shown */
document.querySelectorAll('.nav-item').forEach(item=>{
item.addEventListener('click',function(){
if(this.dataset.page==='projekt'){ loadDeals(); }
if(this.dataset.page==='projektering'){ loadDealPipeline(); }
if(this.dataset.page==='kunder'){ loadCustomers(); }
if(this.dataset.page==='personal'){ loadStaff(); }
if(this.dataset.page==='faltsalj'){
setTimeout(()=>{initFaltMap();if(gMap)google.maps.event.trigger(gMap,'resize')},200);
}
if(this.dataset.page==='kalender'){
setTimeout(renderCalendar,50);
}
});
});
/* === EVA AI CHAT === */
const EVA_KEY='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';
const evaHistory=[];
function ekoTab(btn,panelId){
document.querySelectorAll('.eko-tab').forEach(t=>t.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.eko-panel').forEach(p=>p.classList.remove('active'));
document.getElementById(panelId).classList.add('active');
}
function getEvaSystemPrompt(customers){
customers = customers || [];
return `Du är Eva Andersson, en proaktiv medarbetare på Solar Energy Group. Du jobbar med ekonomi och försäljning.
Du svarar alltid på svenska, kort och koncist. Du är vänlig, professionell och handlingskraftig.
VIKTIGT BETEENDE:
- Du säger ALDRIG att du inte kan göra något. Du är alltid lösningsorienterad.
- Om någon ber dig maila eller kontakta en kund - fråga ALLTID först: "Är det något mer du vill att jag ska inkludera i mailet?" innan du skickar. Vänta på svar.
- Om någon ber dig göra något praktiskt - bekräfta att du tar hand om det. "Fixat!", "Jag ordnar det direkt!", "Klart, du får en uppdatering när jag vet mer."
- Var som en duktig kollega som alltid tar tag i saker direkt.
- Du representerar företaget och kan agera å deras vägnar.
Du har tillgång till följande företagsdata:
KUNDER OCH STATUS:
${customers.map(c=>c.id+': '+c.name+' ('+c.city+') - Status: '+(c.status==='order'?'Order':c.status==='offert'?'Offert':c.status==='lost'?'Ej order':'Lead')+' - Belopp: '+c.amount.toLocaleString('sv-SE')+' kr').join('\n')}
SAMMANFATTNING KUNDER:
- Order: ${customers.filter(c=>c.status==='order').length} st, totalt ${customers.filter(c=>c.status==='order').reduce((s,c)=>s+c.amount,0).toLocaleString('sv-SE')} kr
- Offert: ${customers.filter(c=>c.status==='offert').length} st, totalt ${customers.filter(c=>c.status==='offert').reduce((s,c)=>s+c.amount,0).toLocaleString('sv-SE')} kr
- Ej order (förlorade): ${customers.filter(c=>c.status==='lost').length} st, totalt ${customers.filter(c=>c.status==='lost').reduce((s,c)=>s+c.amount,0).toLocaleString('sv-SE')} kr
- Leads: ${customers.filter(c=>c.status==='lead').length} st, totalt ${customers.filter(c=>c.status==='lead').reduce((s,c)=>s+c.amount,0).toLocaleString('sv-SE')} kr
PÅGÅENDE PROJEKT:
- PRJ-2024-001: Andersson Bygg AB - 12 fönster + 2 balkongdörrar - Montering 100% klar, Besiktning godkänd
- PRJ-2024-002: Svensson Fastigheter - 8 fönster villa - Montering 75%, väntar besiktning
- PRJ-2024-003: Johansson & Co - Kontorsfönster 24 st - Montering 30%, pågår
- PRJ-2024-004: Nilsson Renovering - 6 fönster + entredörr - Inmätning pågår, leverans packad, montering ej påbörjad
- PRJ-2024-005: Karlsson Entreprenad - Flerfamiljshus 48 fönster - Offert skickad, väntar svar
- PRJ-2024-006: Berg Fastigheter AB - BRF renovering 32 fönster - Utkast, ej skickad
ORDRAR:
- ORD-2024-001: Andersson Bygg, 24 500 kr, Levererad
- ORD-2024-002: Svensson Fastigheter, 48 200 kr, Under produktion
- ORD-2024-003: Johansson & Co, 12 800 kr, Väntar godkännande
- ORD-2024-004: Nilsson Renovering, 67 300 kr, Levererad
- ORD-2024-005: Karlsson Entreprenad, 35 900 kr, Under produktion
KALKYLER:
- KAL-2026-012: Andersson Bygg, Fönster, 185 400 kr - Godkänd
- KAL-2026-011: Svensson Fastigheter, Fönster, 92 500 kr - Skickad
- KAL-2026-010: Karlsson Entreprenad, Solceller, 248 000 kr - Utkast
- KAL-2026-009: Berg Fastigheter, Tak, 320 000 kr - Skickad
- KAL-2026-008: Nilsson Renovering, Dörrar, 67 300 kr - Godkänd
SÄLJARE:
- Erik Lundström: 8 affärer, 485 000 kr (bäst denna månad)
- Maria Andersson: 5 affärer, 312 000 kr
- Johan Bergman: 4 affärer, 248 000 kr
- Sofia Nilsson: 3 affärer, 187 000 kr
EKONOMI FEBRUARI 2026:
- Omsättning: 1 232 000 kr (mål: 1 500 000 kr, 82% uppnått)
- Antal stängda affärer: 20
- Genomsnittligt ordervärde: 61 600 kr
- Offert-till-order konvertering: 68%
- Leveransprecision: 94% (mål 97%)
Problem/Noteringar:
- Nilsson Renovering (PRJ-2024-004): Specialbeslag saknas, försenar montering med ca 1 vecka
- Karlsson Entreprenad (PRJ-2024-005): Stor offert 248 000 kr väntar svar sedan 2 veckor
- Leveransprecision under mål (94% vs 97%) pga materialbrist hos leverantör Optima
- 4 förlorade affärer totalt 528 700 kr - vanligaste orsak: pris (3 av 4)
Datum idag: 18 februari 2026.`;
}
function getTimeStr(){const d=new Date();return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')}
function addEvaMsg(text,isUser){
const msgs=document.getElementById('evaMessages');
const div=document.createElement('div');
div.className='eva-msg '+(isUser?'user':'eva');
let html=text.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\n/g,'<br>');
div.innerHTML=(isUser?'':'<img src="eva.png" alt="Eva" class="eva-msg-avatar">')
+'<div><div class="eva-msg-bubble">'+html+'</div><div class="eva-msg-time">'+getTimeStr()+'</div></div>';
msgs.appendChild(div);
msgs.scrollTop=msgs.scrollHeight;
}
function showTyping(){
const msgs=document.getElementById('evaMessages');
const div=document.createElement('div');
div.className='eva-msg eva';
div.id='evaTyping';
div.innerHTML='<img src="eva.png" alt="Eva" class="eva-msg-avatar"><div class="eva-typing"><span></span><span></span><span></span></div>';
msgs.appendChild(div);
msgs.scrollTop=msgs.scrollHeight;
}
function hideTyping(){const t=document.getElementById('evaTyping');if(t)t.remove()}
async function evaSend(){
const input=document.getElementById('evaInput');
const text=input.value.trim();
if(!text)return;
input.value='';
input.style.height='auto';
document.getElementById('evaSuggestions').style.display='none';
addEvaMsg(text,true);
evaHistory.push({role:'user',content:text});
document.getElementById('evaSendBtn').disabled=true;
showTyping();
try{
const res=await fetch('https://api.openai.com/v1/chat/completions',{
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+EVA_KEY},
body:JSON.stringify({model:'gpt-4o-mini',messages:[{role:'system',content:getEvaSystemPrompt(window._evaCustomers)},...evaHistory],temperature:0.7,max_tokens:800})
});
const data=await res.json();
hideTyping();
if(data.choices&&data.choices[0]){
const reply=data.choices[0].message.content;
evaHistory.push({role:'assistant',content:reply});
addEvaMsg(reply,false);
}else{
addEvaMsg('Ursäkta, jag kunde inte svara just nu. Försök igen.',false);
}
}catch(e){
hideTyping();
addEvaMsg('Anslutningsfel. Kontrollera din internetanslutning.',false);
}
document.getElementById('evaSendBtn').disabled=false;
document.getElementById('evaInput').focus();
}
function evaSuggest(btn){
document.getElementById('evaInput').value=btn.textContent;
evaSend();
}
/* === PRODUKTKATALOG === */
let catalogProducts = [];
let _suppliers = [];
async function loadSuppliers() {
try {
const r = await fetch('/api/products.php?suppliers=1');
const d = await r.json();
if(d.success) _suppliers = d.suppliers;
} catch(e){}
}
async function loadCatalogFromDB() {
loadSuppliers();
try {
const res = await fetch('/api/products.php');
const data = await res.json();
if(data.success && data.products) {
catalogProducts = data.products.map(p => {
let specs = null;
try { if(p.specs) specs = typeof p.specs === 'string' ? JSON.parse(p.specs) : p.specs; } catch(e){}
return {
id: p.id, name: p.name, cat: p.cat, catLabel: p.cat_label, subcat: p.subcat || '',
desc: p.description, price: parseFloat(p.price),
costPrice: p.cost_price ? parseFloat(p.cost_price) : null, costCurrency: p.cost_currency || 'SEK',
stock: p.stock, stockClass: p.stock_class, img: p.img || '',
watt: p.watt ? parseInt(p.watt) : null,
kwhCapacity: p.kwh_capacity ? parseFloat(p.kwh_capacity) : null,
greenTechEligible: parseInt(p.green_tech_eligible) === 1,
rotEligible: parseInt(p.rot_eligible) === 1,
tillvalObligatorisk: parseInt(p.tillval_obligatorisk) === 1,
tillvalHidden: parseInt(p.tillval_hidden) === 1,
specs: specs,
supplierId: p.supplier_id ? parseInt(p.supplier_id) : null,
unit: p.unit || 'st',
markupType: p.markup_type || 'percent',
markupValue: p.markup_value ? parseFloat(p.markup_value) : 0,
gallery: (() => { try { return p.gallery ? (typeof p.gallery === 'string' ? JSON.parse(p.gallery) : p.gallery) : []; } catch(e){ return []; } })(),
documents: (() => { try { return p.documents ? (typeof p.documents === 'string' ? JSON.parse(p.documents) : p.documents) : []; } catch(e){ return []; } })()
};
});
}
} catch(e) { console.error('Kunde inte ladda produkter:', e); }
filterCatalog();
}
let _catOrder = ['solceller','batteri','batteri_utbyggnad','laddbox','fonster','dorrar','tak','varmepump','taktvatt','isolering','tjanster','tillbehor'];
// Will be updated from categories API
function filterCatalog(){
const val=document.getElementById('catalogCatSelect').value;
const model='';
const modelSel=null;
const searchTerm = (document.getElementById('catalogSearch')?.value || '').trim().toLowerCase();
let products=catalogProducts;
// Hide tillval categories by default
const tillvalCats = ['tjanster','tillbehor'];
if(!val) products = products.filter(p => !tillvalCats.includes(p.cat));
if(val){
if(val.startsWith('g:')){
const prefix=val.substring(2);
products=products.filter(p=>p.cat===prefix||p.cat.startsWith(prefix+'_'));
} else {
products=products.filter(p=>p.cat===val);
}
}
if(searchTerm) {
products = products.filter(p =>
p.name.toLowerCase().includes(searchTerm)
|| p.id.toLowerCase().includes(searchTerm)
|| (p.desc && p.desc.toLowerCase().includes(searchTerm))
|| (p.catLabel && p.catLabel.toLowerCase().includes(searchTerm))
);
}
// Sortera efter kategori-ordning i dropdown, sedan namn
products = [...products].sort((a,b)=>{
const ai=_catOrder.indexOf(a.cat), bi=_catOrder.indexOf(b.cat);
const ca=(ai===-1?999:ai)-(bi===-1?999:bi);
if(ca!==0) return ca;
const sa=(a.subcat||'\uffff'), sb=(b.subcat||'\uffff');
const sc=sa.localeCompare(sb,'sv');
if(sc!==0) return sc;
return a.name.localeCompare(b.name,'sv');
});
const models=[...new Set(products.map(p=>p.name))];
if(modelSel) modelSel.innerHTML='<option value="">Alla modeller</option>'+models.map(m=>'<option value="'+m+'">'+m+'</option>').join('');
renderCatalog(products);
}
let _catalogView = 'grid';
function setCatalogView(v) {
_catalogView = v;
document.getElementById('catViewGrid').style.cssText = 'padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;'+(v==='grid'?'background:#024550;color:#fff':'background:#fff;color:#64748b');
document.getElementById('catViewList').style.cssText = 'padding:6px 8px;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;'+(v==='list'?'background:#024550;color:#fff':'background:#fff;color:#64748b');
filterCatalog();
}
function renderCatalog(products){
const grid=document.getElementById('catalogGrid');
if(!grid) return;
document.getElementById('catalogCount').textContent=products.length+' produkter';
const isAdmin = (gUserRole === 'systemadmin' || gUserRole === 'admin' || gUserRole === 'saljchef');
if(_catalogView === 'list') { renderCatalogList(products, grid, isAdmin); return; }
grid.style.display = '';
grid.className = '';
let lastCatSubcat = '';
let html = '<div class="catalog-grid">';
products.forEach(p => {
const groupKey = p.catLabel + '|' + (p.subcat || '');
lastCatSubcat = p.catLabel + '|' + (p.subcat || '');
const imgHtml=p.img?'<img src="'+p.img+'" alt="'+p.name+'">':'<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
const galleryThumbs = (p.gallery && p.gallery.length) ? '<div style="display:flex;gap:4px;padding:4px 8px;background:#f8f9fa;border-top:1px solid #f1f5f9">' + p.gallery.slice(0,4).map(g=>'<img src="'+g+'" style="width:28px;height:28px;object-fit:cover;border-radius:4px">').join('') + (p.gallery.length > 4 ? '<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#94a3b8;background:#e5e7eb;border-radius:4px">+' + (p.gallery.length - 4) + '</span>' : '') + '</div>' : '';
const editBtn = isAdmin ? '<button onclick="event.stopPropagation();editProduct(\''+p.id+'\')" style="position:absolute;top:8px;right:8px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:6px 8px;cursor:pointer;box-shadow:0 2px 6px rgba(0,0,0,.1);display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#334155;font-family:inherit"><svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Redigera</button>' : '';
const greenBadge = p.greenTechEligible ? '<span style="position:absolute;top:8px;left:8px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;font-size:10px;font-weight:700;padding:4px 8px;border-radius:6px;letter-spacing:.3px;box-shadow:0 2px 6px rgba(16,185,129,.3)">GRÖNT TEKNIK-AVDRAG</span>' : '';
let varHtml = '';
if(p.specs && p.specs.type === 'style_width_variants') {
const styles = p.specs.styles;
const allPrices = styles.flatMap(s=>s.variants.map(v=>v.price)).filter(x=>x!=null);
varHtml = '<div style="margin-top:8px;font-size:11px">'
+'<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:4px">'
+ styles.map(s=>'<span style="background:#f1f5f9;color:#334155;padding:2px 8px;border-radius:10px;font-weight:600;font-size:10px">'+s.style+'</span>').join('')
+'</div>'
+(allPrices.length?'<div style="font-weight:600;color:#024550">'+Math.min(...allPrices).toFixed(0)+' – '+Math.max(...allPrices).toFixed(0)+' kr/'+(p.unit||'st')+'</div>':'<div style="color:#94a3b8">Pris saknas</div>')
+'</div>';
} else if(p.specs && p.specs.type === 'width_variants') {
const allPrices = p.specs.variants.map(v=>v.price).filter(x=>x!=null);
const labels = p.specs.variants.map(v=>v.label||(v.width_mm?v.width_mm+'mm':(v.length_mm?v.length_mm+'mm':''))).slice(0,4);
varHtml = '<div style="margin-top:8px;font-size:11px">'
+'<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:4px">'
+ labels.map(l=>'<span style="background:#f1f5f9;color:#334155;padding:2px 8px;border-radius:10px;font-weight:600;font-size:10px">'+l+'</span>').join('')
+(p.specs.variants.length>4?'<span style="background:#f1f5f9;color:#94a3b8;padding:2px 8px;border-radius:10px;font-size:10px">+'+( p.specs.variants.length-4)+'</span>':'')
+'</div>'
+(allPrices.length?'<div style="font-weight:600;color:#024550">'+Math.min(...allPrices).toFixed(0)+' – '+Math.max(...allPrices).toFixed(0)+' kr/'+(p.unit||'st')+'</div>':'<div style="color:#94a3b8">Pris saknas</div>')
+'</div>';
} else if(p.specs && p.specs.type === 'window_sizes') {
const s = p.specs;
const minP = Math.min(...s.sizes.map(x=>x.price));
const maxP = Math.max(...s.sizes.map(x=>x.price));
const sizeCount = s.sizes.length;
const hasLam = s.sizes.some(x=>x.note && x.note.toLowerCase().includes('laminated'));
varHtml = '<div style="margin-top:8px;font-size:11px">'
+'<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px">'
+'<span style="background:#e0f2fe;color:#0284c7;padding:2px 8px;border-radius:10px;font-weight:600">'+sizeCount+' storlekar</span>'
+'<span style="background:#f0fdf4;color:#16a34a;padding:2px 8px;border-radius:10px;font-weight:600">'+s.model+'</span>'
+(hasLam?'<span style="background:#fef3c7;color:#d97706;padding:2px 8px;border-radius:10px;font-weight:600">Laminerat glas</span>':'')
+'</div>'
+'<div style="color:#64748b">Bredd: '+Math.min(...s.widths)+'–'+Math.max(...s.widths)+' dm | Höjd: '+Math.min(...s.heights)+'–'+Math.max(...s.heights)+' dm</div>'
+'<div style="font-weight:600;color:#024550;margin-top:4px">'+minP.toFixed(0)+' – '+maxP.toFixed(0)+' EUR</div>'
+'</div>';
} else if(p.specs && p.specs.variants && p.specs.variants.length) {
const v = p.specs.variants;
const first = v[0];
if(first.kwh !== undefined) {
varHtml = '<table style="width:100%;border-collapse:collapse;margin-top:8px;font-size:11px"><thead><tr style="background:#f1f5f9"><th style="padding:4px 6px;text-align:left;font-weight:600;color:#64748b">kWh</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Att betala</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Grönt teknik</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Totalt</th></tr></thead><tbody>'
+ v.map(r=>'<tr style="border-top:1px solid #f1f5f9"><td style="padding:3px 6px;font-weight:600">'+r.kwh+' kWh</td><td style="padding:3px 6px;text-align:right">'+r.att_betala.toLocaleString('sv-SE')+' kr</td><td style="padding:3px 6px;text-align:right;color:#059669">'+r.gron_teknik.toLocaleString('sv-SE')+' kr</td><td style="padding:3px 6px;text-align:right;font-weight:600">'+r.totalt.toLocaleString('sv-SE')+' kr</td></tr>').join('')
+ '</tbody></table>';
} else if(first.paneler !== undefined) {
const show = v.slice(0,6);
varHtml = '<table style="width:100%;border-collapse:collapse;margin-top:8px;font-size:11px"><thead><tr style="background:#f1f5f9"><th style="padding:4px 6px;text-align:left;font-weight:600;color:#64748b">Paneler</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Att betala</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Grönt teknik</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Totalt</th></tr></thead><tbody>'
+ show.map(r=>'<tr style="border-top:1px solid #f1f5f9"><td style="padding:3px 6px;font-weight:600">'+r.paneler+' st</td><td style="padding:3px 6px;text-align:right">'+r.att_betala.toLocaleString('sv-SE')+' kr</td><td style="padding:3px 6px;text-align:right;color:#059669">'+r.gron_teknik.toLocaleString('sv-SE')+' kr</td><td style="padding:3px 6px;text-align:right;font-weight:600">'+r.totalt.toLocaleString('sv-SE')+' kr</td></tr>').join('')
+ (v.length > 6 ? '<tr><td colspan="4" style="padding:3px 6px;text-align:center;color:#94a3b8;font-size:10px">... '+v.length+' rader totalt (8–'+v[v.length-1].paneler+' paneler)</td></tr>' : '')
+ '</tbody></table>';
} else if(first.cm !== undefined) {
varHtml = '<table style="width:100%;border-collapse:collapse;margin-top:8px;font-size:11px"><thead><tr style="background:#f1f5f9"><th style="padding:4px 6px;text-align:left;font-weight:600;color:#64748b">Tjocklek</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Pris/m²</th></tr></thead><tbody>'
+ v.map(r=>'<tr style="border-top:1px solid #f1f5f9"><td style="padding:3px 6px;font-weight:600">'+r.cm+' cm</td><td style="padding:3px 6px;text-align:right;font-weight:600">'+r.pris_per_m2+' kr/m²</td></tr>').join('')
+ '</tbody></table>';
} else if(first.material !== undefined) {
varHtml = '<table style="width:100%;border-collapse:collapse;margin-top:8px;font-size:11px"><thead><tr style="background:#f1f5f9"><th style="padding:4px 6px;text-align:left;font-weight:600;color:#64748b">Material</th><th style="padding:4px 6px;text-align:right;font-weight:600;color:#64748b">Pris/m²</th></tr></thead><tbody>'
+ v.map(r=>'<tr style="border-top:1px solid #f1f5f9"><td style="padding:3px 6px;font-weight:600">'+r.material+'</td><td style="padding:3px 6px;text-align:right;font-weight:600">'+r.pris_per_m2+' kr/m²</td></tr>').join('')
+ '</tbody></table>';
}
}
const priceHtml = (p.specs && (p.specs.variants || p.specs.type === 'window_sizes')) ? '' : '<div class="catalog-card-price">'+p.price.toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2})+' kr</div>';
html += '<div class="catalog-card" data-pid="'+p.id+'" style="position:relative;cursor:pointer'+(isAdmin?';transition:transform .15s,box-shadow .15s':'')+'"'
+(isAdmin?' draggable="true" ondragstart="catDragStart(event,\''+p.id+'\')" ondragover="catDragOver(event)" ondragenter="catDragEnter(event)" ondragleave="catDragLeave(event)" ondrop="catDrop(event,\''+p.id+'\')" ondragend="catDragEnd(event)"':'')
+' onclick="showProductModal(\''+p.id+'\')"><div class="catalog-card-img">'+imgHtml+'</div>'+galleryThumbs+greenBadge+editBtn+'<div class="catalog-card-body"><div class="catalog-card-cat">'+p.catLabel+'</div><div class="catalog-card-name">'+p.name+'</div><div class="catalog-card-desc">'+(p.desc||'')+'</div>'+varHtml+'<div class="catalog-card-footer">'+priceHtml+'<span class="catalog-card-badge status-badge '+p.stockClass+'">'+p.stock+'</span></div></div></div>';
});
html += '</div>'; // close grid
grid.innerHTML = html;
}
// === DRAG & DROP REORDER ===
let _catDragId = null;
function catDragStart(e, id) {
_catDragId = id;
e.dataTransfer.effectAllowed = 'move';
e.target.style.opacity = '0.4';
e.target.style.transform = 'scale(0.95)';
}
function catDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
function catDragEnter(e) {
e.preventDefault();
const card = e.target.closest('.catalog-card');
if(card && card.dataset.pid !== _catDragId) {
card.style.boxShadow = '0 0 0 2px #024550';
}
}
function catDragLeave(e) {
const card = e.target.closest('.catalog-card');
if(card) card.style.boxShadow = '';
}
function catDragEnd(e) {
_catDragId = null;
document.querySelectorAll('.catalog-card').forEach(c => { c.style.opacity = ''; c.style.transform = ''; c.style.boxShadow = ''; });
}
async function catDrop(e, targetId) {
e.preventDefault();
const card = e.target.closest('.catalog-card');
if(card) card.style.boxShadow = '';
if(!_catDragId || _catDragId === targetId) return;
// Swap positions in catalogProducts
const dragIdx = catalogProducts.findIndex(p => p.id === _catDragId);
const targetIdx = catalogProducts.findIndex(p => p.id === targetId);
if(dragIdx === -1 || targetIdx === -1) return;
// Move dragged item to target position
const [moved] = catalogProducts.splice(dragIdx, 1);
catalogProducts.splice(targetIdx, 0, moved);
// Update sort_order for affected products
const updates = [];
catalogProducts.forEach((p, i) => {
const newOrder = i + 1;
if(p.sortOrder !== newOrder) {
updates.push({ id: p.id, sort_order: newOrder });
}
});
// Re-render immediately
filterCatalog();
// Save to DB in background
if(updates.length) {
try {
await Promise.all(updates.map(u =>
fetch('/api/products.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: u.id, sort_order: u.sort_order })
})
));
} catch(err) { console.error('Sort save error:', err); }
}
_catDragId = null;
}
function renderCatalogList(products, grid, isAdmin) {
grid.style.display = 'block';
grid.className = '';
let html = '<table style="width:100%;border-collapse:collapse;font-size:13px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">'
+'<thead><tr style="background:#f8f9fa">'
+'<th style="padding:10px 14px;width:50px"></th>'
+'<th style="padding:10px 14px;text-align:left;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Produkt</th>'
+'<th style="padding:10px 14px;text-align:left;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Kategori</th>'
+'<th style="padding:10px 14px;text-align:right;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Pris</th>'
+'<th style="padding:10px 14px;text-align:right;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Inköp</th>'
+'<th style="padding:10px 14px;text-align:center;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Enhet</th>'
+'<th style="padding:10px 14px;text-align:center;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Påslag</th>'
+'<th style="padding:10px 14px;text-align:center;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Varianter</th>'
+'<th style="padding:10px 14px;text-align:left;font-weight:600;color:#64748b;font-size:11px;text-transform:uppercase">Lager</th>'
+(isAdmin?'<th style="padding:10px 14px;width:40px"></th>':'')
+'</tr></thead><tbody>';
let lastCat = '';
const colSpan = isAdmin ? 10 : 9;
let lastSubcat2 = null;
products.forEach(p => {
const subcatKey2 = p.cat + '|' + (p.subcat || '');
if(p.catLabel !== lastCat || subcatKey2 !== lastSubcat2) {
if(p.catLabel !== lastCat) {
html += '<tr><td colspan="'+colSpan+'" style="padding:10px 14px 6px;font-size:12px;font-weight:800;color:#024550;letter-spacing:.5px;text-transform:uppercase;background:#f8f9fa;border-top:2px solid #e5e7eb">'+p.catLabel+'</td></tr>';
}
if(p.subcat) {
const sl = p.subcat === 'old' ? '📦 Äldre produkter' : p.subcat === 'fonsterdorrar' ? 'Fönsterdörrar' : p.subcat === 'fonstertillbehor' ? 'Fönster' : p.subcat === 'solpanel' ? 'Solpanel' : p.subcat;
html += '<tr><td colspan="'+colSpan+'" style="padding:6px 14px;font-size:11px;font-weight:600;color:#94a3b8;background:#fafafa;border-top:1px dashed #e5e7eb">'+sl+'</td></tr>';
}
lastCat = p.catLabel;
lastSubcat2 = subcatKey2;
}
const varCount = (p.specs && p.specs.type === 'window_sizes') ? p.specs.sizes.length : (p.specs && p.specs.variants) ? p.specs.variants.length : 0;
const thumbHtml = p.img ? '<img src="'+p.img+'" style="width:36px;height:36px;object-fit:cover;border-radius:6px">' : '<div style="width:36px;height:36px;background:#f1f5f9;border-radius:6px;display:flex;align-items:center;justify-content:center"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#cbd5e1;fill:none;stroke-width:1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg></div>';
html += '<tr style="border-top:1px solid #f1f5f9;cursor:pointer" onclick="showProductModal(\''+p.id+'\')">'
+'<td style="padding:4px 14px">'+thumbHtml+'</td>'
+'<td style="padding:8px 14px;font-weight:600">'+p.name+'</td>'
+'<td style="padding:8px 14px;color:#64748b">'+p.catLabel+'</td>'
+'<td style="padding:8px 14px;text-align:right">'+p.price.toLocaleString('sv-SE')+' kr</td>'
+'<td style="padding:8px 14px;text-align:right;color:#94a3b8">'+(p.costPrice ? p.costPrice.toLocaleString('sv-SE')+' kr' : '–')+'</td>'
+'<td style="padding:8px 14px;text-align:center;color:#64748b">'+(p.unit||'st')+'</td>'
+'<td style="padding:8px 14px;text-align:center;color:#64748b">'+(p.markupValue ? (p.markupType==='percent' ? p.markupValue+'%' : p.markupValue.toLocaleString('sv-SE')+' kr') : '–')+'</td>'
+'<td style="padding:8px 14px;text-align:center">'+(varCount ? '<span style="background:#e0f2fe;color:#0284c7;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600">'+varCount+'</span>' : '–')+'</td>'
+'<td style="padding:8px 14px"><span class="status-badge '+p.stockClass+'" style="font-size:11px">'+p.stock+'</span></td>'
+(isAdmin?'<td style="padding:8px 14px"><button onclick="event.stopPropagation();editProduct(\''+p.id+'\')" style="background:none;border:none;cursor:pointer;color:#64748b;padding:4px"><svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button></td>':'')
+'</tr>';
});
html += '</tbody></table>';
grid.innerHTML = html;
}
// === PRODUCT EDIT MODAL ===
let _peVariants = [];
let _peServices = [];
function editProduct(id) {
const p = catalogProducts.find(x => x.id === id);
if(!p) return;
const cats = [{v:'solceller',l:'Solceller'},{v:'batteri',l:'Batteri'},{v:'batteri_utbyggnad',l:'Batteri Utbyggnad'},{v:'laddbox',l:'Laddbox'},{v:'fonster',l:'Fönster'},{v:'dorrar',l:'Dörrar'},{v:'tak',l:'Tak'},{v:'varmepump',l:'Värmepump'},{v:'taktvatt',l:'Taktvätt'},{v:'isolering',l:'Isolering'},{v:'tjanster',l:'Tjänster'},{v:'tillbehor',l:'Tillbehör'}];
const stocks = [{v:'I lager',c:'green'},{v:'Få kvar',c:'yellow'},{v:'Beställning',c:'blue'},{v:'Slut',c:'red'}];
_peVariants = (p.specs && p.specs.variants) ? JSON.parse(JSON.stringify(p.specs.variants)) : [];
_peSpecsType = (p.specs && p.specs.type) || '';
_peStyles = (p.specs && p.specs.styles) ? JSON.parse(JSON.stringify(p.specs.styles)) : [];
_peSizes = (p.specs && p.specs.sizes) ? JSON.parse(JSON.stringify(p.specs.sizes)) : [];
_peSpecsMeta = p.specs ? JSON.parse(JSON.stringify(p.specs)) : {};
const modal = document.createElement('div');
modal.id = 'productEditModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:1400px;width:100%;max-height:95vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:16px 24px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center">'
+'<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin:0">Redigera: '+p.name+'</h2>'
+'<button onclick="this.closest(\'#productEditModal\').remove()" style="background:none;border:none;cursor:pointer;padding:4px"><svg viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#94a3b8;fill:none;stroke-width:2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
+'</div>'
+'<style>#productEditModal input:not([type=checkbox]):not([type=file]),#productEditModal select,#productEditModal textarea{max-width:100%;min-width:0;box-sizing:border-box}</style>'
+'<div style="padding:20px 24px;display:grid;grid-template-columns:320px 1fr;gap:24px">'
// VÄNSTER: Grundinfo
+'<div style="min-width:0;padding-right:20px;border-right:1px solid #e5e7eb">'
+'<div style="margin-bottom:16px">'
+'<div id="peImgPreview" style="width:100%;aspect-ratio:16/9;background:#f8f9fa;border-radius:10px;border:2px dashed #e5e7eb;display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer;margin-bottom:6px" onclick="document.getElementById(\'peFileInput\').click()">'
+(p.img ? '<img src="'+p.img+'" style="width:100%;height:100%;object-fit:cover">' : '<div style="text-align:center;color:#94a3b8;font-size:12px">Klicka för bild</div>')
+'</div>'
+'<input type="file" id="peFileInput" accept="image/*" style="display:none" onchange="previewProductImage(this)">'
+'<div id="peUploadStatus" style="font-size:11px;color:#94a3b8"></div>'
+'</div>'
+'<div style="display:grid;grid-template-columns:80px 1fr;gap:8px 10px;align-items:center;font-size:13px">'
+'<label style="font-weight:600;color:#64748b">ID</label>'
+'<input id="peId" value="'+p.id+'" readonly style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;background:#f8f9fa;color:#94a3b8;font-family:inherit">'
+'<label style="font-weight:600;color:#64748b">Namn</label>'
+'<input id="peName" value="'+p.name.replace(/"/g,'"')+'" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit">'
+'<label style="font-weight:600;color:#64748b">Kategori</label>'
+'<select id="peCat" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit">'+cats.map(c=>'<option value="'+c.v+'"'+(c.v===p.cat?' selected':'')+'>'+c.l+'</option>').join('')+'</select>'
+'<label style="font-weight:600;color:#64748b">Pris (kr)</label>'
+'<input id="pePrice" type="number" value="'+p.price+'" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit">'
+'<label style="font-weight:600;color:#64748b">Inköpspris</label>'
+'<div style="display:flex;gap:4px"><input id="peCostPrice" type="number" value="'+(p.costPrice||'')+'" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;flex:1"><select id="peCostCurrency" style="padding:7px 6px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;width:70px">'+(function(){var cs=['SEK','EUR','USD','DKK','NOK'];return cs.map(function(c){return '<option value="'+c+'"'+((p.costCurrency||'SEK')===c?' selected':'')+'>'+c+'</option>';}).join('');}())+'</select></div>'
+'<label style="font-weight:600;color:#64748b">Lager</label>'
+'<select id="peStock" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit">'+stocks.map(s=>'<option value="'+s.v+'" data-class="'+s.c+'"'+(s.v===p.stock?' selected':'')+'>'+s.v+'</option>').join('')+'</select>'
+'<label style="font-weight:600;color:#64748b">Leverantör</label>'
+'<select id="peSupplier" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit"><option value="">– Ingen –</option>'+_suppliers.map(s=>'<option value="'+s.id+'"'+(s.id==p.supplierId?' selected':'')+'>'+s.name+'</option>').join('')+'</select>'
+'<label style="font-weight:600;color:#64748b">Enhet</label>'
+'<select id="peUnit" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit">'
+['st','kvm','m²','löpmeter','panel','mil','kWh','W','paket','tim','m'].map(u=>'<option value="'+u+'"'+(u===p.unit?' selected':'')+'>'+u+'</option>').join('')
+'</select>'
+'<label style="font-weight:600;color:#64748b">Påslag</label>'
+'<div style="display:flex;gap:6px"><select id="peMarkupType" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;flex:1"><option value="percent"'+(p.markupType==='percent'?' selected':'')+'>Procent (%)</option><option value="amount"'+(p.markupType==='amount'?' selected':'')+'>Belopp (kr)</option></select>'
+'<input id="peMarkupValue" type="number" step="0.01" value="'+(p.markupValue||0)+'" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;width:100px" placeholder="0"></div>'
+'<label style="font-weight:600;color:#64748b">Beskrivning</label>'
+'<textarea id="peDesc" rows="2" style="padding:7px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;resize:vertical">'+(p.desc||'').replace(/</g,'<')+'</textarea>'
+'</div>'
+'<div style="margin-top:10px;display:flex;flex-direction:column;gap:6px">'
+'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px"><input type="checkbox" id="peGreenTech" '+(p.greenTechEligible?'checked':'')+' style="width:16px;height:16px;accent-color:#059669">Grönt teknik-avdrag</label>'
+'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px"><input type="checkbox" id="peRotEligible" '+(p.rotEligible?'checked':'')+' style="width:16px;height:16px;accent-color:#2563eb">ROT-avdrag</label>'
+'<div style="border-top:1px solid #e5e7eb;padding-top:8px;margin-top:8px">'
+'<div data-tillval-label style="font-size:11px;font-weight:600;color:#94a3b8;margin-bottom:6px">NÄR PRODUKTEN ANVÄNDS SOM TILLVAL</div>'
+'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px"><input type="checkbox" id="peTillvalOblig" '+(p.tillvalObligatorisk?'checked':'')+' style="width:16px;height:16px;accent-color:#f59e0b">Obligatorisk (alltid inkluderad)</label>'
+'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;margin-top:4px"><input type="checkbox" id="peTillvalHidden" '+(p.tillvalHidden?'checked':'')+' style="width:16px;height:16px;accent-color:#f59e0b">Dold (syns ej som valbar)</label>'
+'</div>'
+'</div>'
+'<div style="display:flex;gap:8px;margin-top:16px">'
+'<button onclick="saveProduct(\''+p.id+'\')" style="flex:1;padding:10px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Spara</button>'
+'<button onclick="deleteProduct(\''+p.id+'\')" style="padding:10px 14px;background:#fee2e2;color:#dc2626;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Ta bort</button>'
+'</div>'
+'</div>'
// HÖGER: Fliksystem
+'<div style="min-width:0">'
// Flik-navigation
+'<div style="display:flex;gap:0;border-bottom:2px solid #e5e7eb;margin-bottom:12px">'
+'<button onclick="peSetTab(\'varianter\')" class="pe-tab" data-tab="varianter" style="padding:8px 16px;font-size:12px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid #024550;color:#024550;margin-bottom:-2px;font-family:inherit">Varianter</button>'
+'<button onclick="peSetTab(\'tjanster\')" class="pe-tab" data-tab="tjanster" style="padding:8px 16px;font-size:12px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Tillval</button>'
+'<button onclick="peSetTab(\'bilder\')" class="pe-tab" data-tab="bilder" style="padding:8px 16px;font-size:12px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Bilder</button>'
+'<button onclick="peSetTab(\'spec\')" class="pe-tab" data-tab="spec" style="padding:8px 16px;font-size:12px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Specifikation</button>'
+'<button onclick="peSetTab(\'dokument\')" class="pe-tab" data-tab="dokument" style="padding:8px 16px;font-size:12px;font-weight:600;border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;margin-bottom:-2px;font-family:inherit">Dokument</button>'
+'</div>'
// Flik: Varianter
+'<div id="peTabVarianter" class="pe-tab-panel">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">'
+'<h3 style="font-size:14px;font-weight:700;margin:0;color:#1a1a1a">Varianter</h3>'
+'<button onclick="peAddVariant()" style="padding:5px 12px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till</button>'
+'</div>'
+'<div id="peVariantsTable" style="max-height:300px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"></div>'
+'<div style="margin-top:16px;display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">'
+'<h3 style="font-size:14px;font-weight:700;margin:0;color:#1a1a1a">Regler <span style="font-weight:400;color:#94a3b8;font-size:11px;cursor:pointer" onclick="document.getElementById(\'peRulesHelp\').style.display=document.getElementById(\'peRulesHelp\').style.display===\'none\'?\'block\':\'none\'">?</span></h3>'
+'<button onclick="peAddRule()" style="padding:5px 12px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till</button>'
+'</div>'
+'<div id="peRulesHelp" style="display:none;margin-bottom:8px;padding:10px;background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;font-size:11px;line-height:1.6;color:#334155"><strong style="color:#0284c7">OM</strong> = villkor, <strong style="color:#0284c7">SÅ</strong> = åtgärd, <strong style="color:#0284c7">±</strong> = operator (= sätter, + adderar, − subtraherar, × multiplicerar). Formler: <code>{w}</code> <code>{h}</code> <code>{TB_FODER_width}</code></div>'
+'<div id="peRulesTable" style="max-height:300px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"></div>'
+'</div>'
// Flik: Tjänster + Tillbehör
+'<div id="peTabTjanster" class="pe-tab-panel" style="display:none">'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">'
+'<div>'
+'<h3 style="font-size:14px;font-weight:700;margin:0 0 8px;color:#1a1a1a">Kopplade tjänster</h3>'
+'<div id="peServicesTable" style="max-height:300px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"><div style="padding:12px;color:#94a3b8;font-size:12px">Laddar...</div></div>'
+'</div>'
+'<div>'
+'<h3 style="font-size:14px;font-weight:700;margin:0 0 8px;color:#1a1a1a">Kopplade tillbehör</h3>'
+'<div id="peAccessoriesTable" style="max-height:300px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"><div style="padding:12px;color:#94a3b8;font-size:12px">Laddar...</div></div>'
+'</div>'
+'</div>'
+'<div id="peUsedByBlock" style="margin-top:16px;display:none">'
+'<h3 style="font-size:14px;font-weight:700;margin:0 0 8px;color:#1a1a1a">Denna produkt är kopplad som tillval till</h3>'
+'<div id="peUsedByTable" style="max-height:200px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"></div>'
+'</div>'
+'</div>'
// Flik: Bilder
+'<div id="peTabBilder" class="pe-tab-panel" style="display:none">'
+'<h3 style="font-size:14px;font-weight:700;margin:0 0 8px;color:#1a1a1a">Galleri</h3>'
+'<div id="peGalleryGrid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px"></div>'
+'<button onclick="document.getElementById(\'peGalleryInput\').click()" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till bilder</button>'
+'<input type="file" id="peGalleryInput" accept="image/*" multiple style="display:none" onchange="peUploadGalleryImages(\''+p.id+'\')">'
+'</div>'
// Flik: Specifikation
+'<div id="peTabSpec" class="pe-tab-panel" style="display:none">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">'
+'<h3 style="font-size:14px;font-weight:700;margin:0;color:#1a1a1a">Specifikationer</h3>'
+'<button onclick="peAddAttribute()" style="padding:5px 12px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till</button>'
+'</div>'
+'<div id="peAttributesTable" style="max-height:400px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px"></div>'
+'</div>'
// Flik: Dokument
+'<div id="peTabDokument" class="pe-tab-panel" style="display:none">'
+'<h3 style="font-size:14px;font-weight:700;margin:0 0 8px;color:#1a1a1a">Dokument</h3>'
+'<div id="peDocumentsList" style="max-height:300px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:12px"></div>'
+'<div style="display:flex;gap:8px;align-items:center">'
+'<select id="peDocType" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit"><option value="manual">Manual</option><option value="datablad">Datablad</option><option value="certifikat">Certifikat</option><option value="ovrigt">Övrigt</option></select>'
+'<button onclick="document.getElementById(\'peDocInput\').click()" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">+ Ladda upp dokument</button>'
+'<input type="file" id="peDocInput" accept=".pdf,.doc,.docx" style="display:none" onchange="peUploadDocument(\''+p.id+'\')">'
+'</div>'
+'</div>'
+'</div>'
+'</div></div>';
document.body.appendChild(modal);
peRenderVariants();
peLoadServices(p.id);
peLoadUsedBy(p.id);
_peCroppedFile = null;
peInitGallery(p.id, p.gallery);
peInitAttributes(p.specs);
peInitRules(p.specs);
peInitDocuments(p.documents);
peRenderRules();
}
let _peSpecsType = '';
let _peStyles = [];
let _peSizes = [];
let _peSpecsMeta = {};
function peRenderVariants() {
const el = document.getElementById('peVariantsTable');
if(!el) return;
// === STYLE + WIDTH VARIANTS ===
if(_peSpecsType === 'style_width_variants' && _peStyles.length) {
let html = '<div style="max-height:500px;overflow-y:auto">';
_peStyles.forEach((style, si) => {
const isOpen = _peStyleExpanded && _peStyleExpanded[si];
const pricesWithVal = style.variants.filter(v=>v.price!=null).length;
const totalVars = style.variants.length;
html += '<div style="margin-bottom:8px;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">'
+'<div style="padding:8px 12px;background:#f8f9fa;display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="peToggleStyle('+si+')">'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2.5" style="transition:transform .2s;transform:rotate('+(isOpen?'0':'-90')+'deg)"><polyline points="6 9 12 15 18 9"/></svg>'
+'<strong style="font-size:13px">'+style.style+'</strong>'
+'<span style="font-size:11px;color:#94a3b8">'+(style.desc||'')+'</span>'
+'<span style="font-size:10px;padding:2px 8px;border-radius:10px;background:#e0f2fe;color:#0284c7;font-weight:600">'+totalVars+' varianter</span>'
+(pricesWithVal<totalVars?'<span style="font-size:10px;padding:2px 8px;border-radius:10px;background:#fef3c7;color:#d97706;font-weight:600">'+(totalVars-pricesWithVal)+' saknar pris</span>':'')
+'</div>'
+'<button onclick="event.stopPropagation();peAddStyleVariant('+si+')" style="padding:3px 10px;background:#024550;color:#fff;border:none;border-radius:4px;font-size:11px;cursor:pointer;font-family:inherit">+ Bredd</button>'
+'</div>'
+'<div id="peStyleBody-'+si+'" style="'+(isOpen?'':'display:none')+'">'
+'<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f1f5f9">'
+'<th style="padding:4px 8px;text-align:left;font-weight:600;color:#64748b">Bredd (mm)</th>'
+'<th style="padding:4px 8px;text-align:right;font-weight:600;color:#64748b">Pris (kr)</th>'
+'<th style="padding:4px 8px;text-align:left;font-weight:600;color:#64748b">Label</th>'
+'<th style="padding:4px 4px;width:30px"></th>'
+'</tr></thead><tbody>'
+ style.variants.map((v, vi) => '<tr style="border-top:1px solid #f1f5f9'+(v.price==null?';background:#fffbeb':'')+'">'
+'<td style="padding:2px 4px"><input type="number" value="'+(v.width_mm||v.length_mm||'')+'" onchange="peUpdateStyleVar('+si+','+vi+',\'width_mm\',this.value)" style="width:80px;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit"></td>'
+'<td style="padding:2px 4px;text-align:right"><input type="number" step="0.01" value="'+(v.price!=null?v.price:'')+'" placeholder="–" onchange="peUpdateStyleVar('+si+','+vi+',\'price\',this.value)" style="width:90px;padding:4px 6px;border:1px solid '+(v.price==null?'#fbbf24':'#e5e7eb')+';border-radius:4px;font-size:12px;font-family:inherit;text-align:right"></td>'
+'<td style="padding:2px 4px"><input type="text" value="'+(v.label||'')+'" onchange="peUpdateStyleVar('+si+','+vi+',\'label\',this.value)" style="width:100%;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit;box-sizing:border-box"></td>'
+'<td style="padding:2px 4px"><button onclick="peRemoveStyleVar('+si+','+vi+')" style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 4px">×</button></td>'
+'</tr>').join('')
+'</tbody></table></div></div>';
});
html += '<button onclick="peAddStyle()" style="margin-top:8px;padding:6px 14px;background:#f8f9fa;border:1px dashed #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit;color:#64748b">+ Ny stil</button>';
html += '</div>';
el.innerHTML = html;
return;
}
// === WIDTH VARIANTS (no styles) ===
if(_peSpecsType === 'width_variants' && _peVariants.length) {
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="background:#f8f9fa;position:sticky;top:0">'
+'<th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Bredd/Längd (mm)</th>'
+'<th style="padding:6px 8px;text-align:right;font-weight:600;color:#64748b">Pris (kr)</th>'
+'<th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Label</th>'
+'<th style="padding:6px 4px;width:30px"></th>'
+'</tr></thead><tbody>'
+ _peVariants.map((v, i) => '<tr style="border-top:1px solid #f1f5f9">'
+'<td style="padding:2px 4px"><input type="number" value="'+(v.width_mm||v.length_mm||'')+'" onchange="peUpdateVariant('+i+',\''+(v.width_mm!==undefined?'width_mm':'length_mm')+'\',this.value)" style="width:100px;padding:5px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit"></td>'
+'<td style="padding:2px 4px;text-align:right"><input type="number" step="0.01" value="'+(v.price!=null?v.price:'')+'" placeholder="–" onchange="peUpdateVariant('+i+',\'price\',this.value)" style="width:100px;padding:5px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit;text-align:right"></td>'
+'<td style="padding:2px 4px"><input type="text" value="'+(v.label||'')+'" onchange="peUpdateVariant('+i+',\'label\',this.value)" style="width:100%;padding:5px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit;box-sizing:border-box"></td>'
+'<td style="padding:2px 4px"><button onclick="peRemoveVariant('+i+')" style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 4px">×</button></td>'
+'</tr>').join('')
+'</tbody></table>';
return;
}
// === WINDOW SIZES ===
if(_peSpecsType === 'window_sizes' && _peSizes.length) {
const widths = [...new Set(_peSizes.map(s=>s.w))].sort((a,b)=>a-b);
const filterW = _peWinFilterW || '';
const filtered = filterW ? _peSizes.filter(s => s.w === parseInt(filterW)) : _peSizes;
el.innerHTML = '<div>'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px">'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<span style="font-size:12px;font-weight:600;color:#334155">'+_peSizes.length+' storlekar</span>'
+'<select id="peWinFilterW" onchange="_peWinFilterW=this.value;peRenderVariants()" style="padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;font-family:inherit">'
+'<option value="">Alla bredder</option>'
+widths.map(w=>'<option value="'+w+'"'+(filterW==w?' selected':'')+'>'+w+' dm</option>').join('')
+'</select>'
+'<span style="font-size:11px;color:#94a3b8">Visar '+filtered.length+' st</span>'
+'</div>'
+'<div style="display:flex;gap:6px">'
+'<label style="padding:4px 10px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:4px">'
+'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>'
+'Importera Excel'
+'<input type="file" accept=".xlsx,.xls,.csv" style="display:none" onchange="peImportWindowExcel(this)">'
+'</label>'
+'<button onclick="peAddWindowSize()" style="padding:4px 10px;background:#f8f9fa;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till</button>'
+'</div></div>'
+'<div style="max-height:400px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:8px">'
+'<table style="width:100%;border-collapse:collapse;font-size:11px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0;z-index:1">'
+'<th style="padding:5px 6px;text-align:left;font-weight:600;color:#64748b">Art.nr</th>'
+'<th style="padding:5px 6px;text-align:right;font-weight:600;color:#64748b">Bredd</th>'
+'<th style="padding:5px 6px;text-align:right;font-weight:600;color:#64748b">Höjd</th>'
+'<th style="padding:5px 6px;text-align:right;font-weight:600;color:#64748b">Pris (EUR)</th>'
+'<th style="padding:5px 6px;text-align:left;font-weight:600;color:#64748b">Not</th>'
+'<th style="padding:5px 4px;width:24px"></th>'
+'</tr></thead><tbody>'
+filtered.map((s,i) => {
const realIdx = _peSizes.indexOf(s);
return '<tr style="border-top:1px solid #f1f5f9">'
+'<td style="padding:2px 4px;color:#94a3b8;font-size:10px">'+(s.article_id||'')+'</td>'
+'<td style="padding:2px 4px;text-align:right;font-weight:600">'+s.w+' dm</td>'
+'<td style="padding:2px 4px;text-align:right;font-weight:600">'+s.h+' dm</td>'
+'<td style="padding:2px 4px"><input type="number" step="0.01" value="'+(s.price!=null?s.price:'')+'" onchange="peUpdateWinSize('+realIdx+',\'price\',this.value)" style="width:80px;padding:3px 5px;border:1px solid #e5e7eb;border-radius:4px;font-size:11px;text-align:right;font-family:inherit"></td>'
+'<td style="padding:2px 4px"><input type="text" value="'+(s.note||'')+'" onchange="peUpdateWinSize('+realIdx+',\'note\',this.value)" style="width:100%;padding:3px 5px;border:1px solid #e5e7eb;border-radius:4px;font-size:11px;font-family:inherit;box-sizing:border-box"></td>'
+'<td style="padding:2px 4px"><button onclick="peRemoveWinSize('+realIdx+')" style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:14px;padding:0">×</button></td>'
+'</tr>';
}).join('')
+'</tbody></table></div></div>';
return;
}
// === DEFAULT (old format) ===
if(!_peVariants.length) {
el.innerHTML = '<div style="padding:12px;color:#94a3b8;font-size:12px;text-align:center">Inga varianter. Klicka "+ Lägg till" för att skapa.</div>';
return;
}
const keys = Object.keys(_peVariants[0]);
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0">'
+ keys.map(k=>'<th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b;white-space:nowrap">'+k+'</th>').join('')
+'<th style="padding:6px 8px;text-align:center;font-weight:600;color:#64748b;width:60px">Förvald</th>'
+'<th style="padding:6px 4px;width:30px"></th>'
+'</tr></thead><tbody>'
+ _peVariants.map((v,i)=>'<tr style="border-top:1px solid #f1f5f9">'
+ keys.map(k=>'<td style="padding:2px 4px"><input type="'+(typeof v[k]==='number'?'number':'text')+'" value="'+(v[k]!=null?v[k]:'')+'" onchange="peUpdateVariant('+i+',\''+k+'\',this.value)" style="width:100%;padding:5px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit;box-sizing:border-box"></td>').join('')
+'<td style="padding:2px 4px;text-align:center"><input type="checkbox" '+(v.is_default?'checked':'')+' onchange="peSetDefaultVariant('+i+',this.checked)" style="width:16px;height:16px;accent-color:#024550"></td>'
+'<td style="padding:2px 4px"><button onclick="peRemoveVariant('+i+')" style="background:none;border:none;cursor:pointer;color:#dc2626;font-size:16px;padding:2px 4px" title="Ta bort">×</button></td>'
+'</tr>').join('')
+'</tbody></table>';
}
let _peWinFilterW = '';
function peUpdateWinSize(idx, key, val) {
if(key === 'price') { const n = parseFloat(val); _peSizes[idx].price = val===''?null:(isNaN(n)?null:n); }
else { _peSizes[idx][key] = val; }
}
function peRemoveWinSize(idx) { _peSizes.splice(idx, 1); peRenderVariants(); }
function peAddWindowSize() {
_peSizes.push({w:0, h:0, price:null, note:'', article_id:''});
peRenderVariants();
}
function peImportWindowExcel(input) {
const file = input.files[0];
if(!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
// Parse CSV (simple) or show message for xlsx
if(file.name.endsWith('.csv')) {
const lines = e.target.result.split('\n').filter(l=>l.trim());
const header = lines[0].split(',').map(h=>h.trim().toLowerCase());
let imported = 0;
for(let i=1; i<lines.length; i++) {
const cols = lines[i].split(',').map(c=>c.trim());
if(cols.length < 4) continue;
const w = parseInt(cols[header.indexOf('width')]||cols[2]) || 0;
const h = parseInt(cols[header.indexOf('height')]||cols[3]) || 0;
const price = parseFloat(cols[header.indexOf('price')]||cols[6]) || null;
const note = cols[header.indexOf('note')]||cols[7]||'';
const artId = cols[header.indexOf('modellid')]||cols[0]||'';
// Update existing or add
const existing = _peSizes.find(s => s.w === w && s.h === h);
if(existing) { existing.price = price; if(note) existing.note = note; if(artId) existing.article_id = parseInt(artId)||artId; }
else { _peSizes.push({w, h, price, note, article_id: parseInt(artId)||artId}); }
imported++;
}
alert('Importerade/uppdaterade ' + imported + ' rader');
} else {
alert('Excel-import (.xlsx) kräver att filen först sparas som CSV.\n\nI Excel: Arkiv → Spara som → CSV (kommaavgränsad)\n\nKolumner: ModellID, ProduktTyp, Width, Height, Bredd, Höjd, Price, Note');
}
} catch(err) { alert('Importfel: ' + err.message); }
peRenderVariants();
input.value = '';
};
if(file.name.endsWith('.csv')) reader.readAsText(file);
else reader.readAsArrayBuffer(file);
}
let _peStyleExpanded = {};
function peToggleStyle(si) {
_peStyleExpanded[si] = !_peStyleExpanded[si];
const body = document.getElementById('peStyleBody-'+si);
const arrow = body?.previousElementSibling?.querySelector('svg');
if(body) body.style.display = _peStyleExpanded[si] ? '' : 'none';
if(arrow) arrow.style.transform = 'rotate('+(_peStyleExpanded[si]?'0':'-90')+'deg)';
}
function peUpdateStyleVar(si, vi, key, val) {
if(key === 'price') { const n = parseFloat(val); _peStyles[si].variants[vi][key] = val===''?null:(isNaN(n)?null:n); }
else if(key === 'width_mm' || key === 'length_mm') { _peStyles[si].variants[vi][key] = parseInt(val)||0; }
else { _peStyles[si].variants[vi][key] = val; }
}
function peRemoveStyleVar(si, vi) { _peStyles[si].variants.splice(vi, 1); peRenderVariants(); }
function peAddStyleVariant(si) { _peStyles[si].variants.push({width_mm:0,price:null}); peRenderVariants(); }
function peAddStyle() { _peStyles.push({style:'Ny stil',desc:'',variants:[{width_mm:0,price:null}]}); peRenderVariants(); }
function peAddVariant() {
if(_peVariants.length) {
const tmpl = {};
Object.keys(_peVariants[0]).forEach(k=>tmpl[k]=0);
_peVariants.push(tmpl);
} else {
_peVariants.push({label:'',pris:0});
}
peRenderVariants();
}
function peSetDefaultVariant(idx, checked) {
_peVariants.forEach((v,i) => { v.is_default = (i === idx && checked) ? 1 : 0; });
peRenderVariants();
}
function peRemoveVariant(i) {
_peVariants.splice(i, 1);
peRenderVariants();
}
function peUpdateVariant(i, key, val) {
const num = parseFloat(val);
_peVariants[i][key] = isNaN(num) ? val : num;
}
let _peAccessories = [];
async function peLoadServices(productId) {
const el = document.getElementById('peServicesTable');
const elAcc = document.getElementById('peAccessoriesTable');
try {
const [svcRes, accRes, linkedRes] = await Promise.all([
fetch('/api/products.php?cat=tjanster&active=1'),
fetch('/api/products.php?cat=tillbehor&active=1'),
fetch('/api/products.php?services_for='+productId)
]);
const svcData = await svcRes.json();
const accData = await accRes.json();
const linkedData = await linkedRes.json();
const allSvc = svcData.success ? svcData.products : [];
const allAcc = accData.success ? accData.products : [];
const linkedMap = {}; if(linkedData.success) (linkedData.services||[]).forEach(s=>{linkedMap[s.service_id]={default_on:parseInt(s.default_on),hidden:parseInt(s.hidden||0)};});
const linked = Object.keys(linkedMap);
_peServices = allSvc.map(s=>({id:s.id, name:s.name, price:parseFloat(s.price), linked:linked.includes(s.id)}));
_peAccessories = allAcc.map(s=>({id:s.id, name:s.name, price:parseFloat(s.price), linked:linked.includes(s.id)}));
peRenderServices();
peRenderAccessories();
} catch(e) {
el.innerHTML = '<div style="padding:12px;color:#ef4444;font-size:12px">Kunde inte ladda tjänster</div>';
if(elAcc) elAcc.innerHTML = '<div style="padding:12px;color:#ef4444;font-size:12px">Kunde inte ladda tillbehör</div>';
}
}
function peRenderServices() {
const el = document.getElementById('peServicesTable');
if(!_peServices.length) { el.innerHTML = '<div style="padding:12px;color:#94a3b8;font-size:12px">Inga tjänster</div>'; return; }
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0"><th style="padding:6px 8px;width:30px"></th><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Tjänst</th><th style="padding:6px 8px;text-align:right;font-weight:600;color:#64748b">Pris</th></tr></thead><tbody>'
+ _peServices.map((s,i)=>'<tr style="border-top:1px solid #f1f5f9'+(s.linked?';background:#f0fdf4':'')+'"><td style="padding:4px 8px"><input type="checkbox" '+(s.linked?'checked':'')+' onchange="_peServices['+i+'].linked=this.checked;peRenderServices()" style="width:15px;height:15px;accent-color:#059669"></td><td style="padding:4px 8px">'+s.name+'</td><td style="padding:4px 8px;text-align:right">'+s.price.toLocaleString('sv-SE')+' kr</td></tr>').join('')
+'</tbody></table>';
}
function peRenderAccessories() {
const el = document.getElementById('peAccessoriesTable');
if(!el) return;
if(!_peAccessories.length) { el.innerHTML = '<div style="padding:12px;color:#94a3b8;font-size:12px">Inga tillbehör</div>'; return; }
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0"><th style="padding:6px 8px;width:30px"></th><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Tillbehör</th><th style="padding:6px 8px;text-align:right;font-weight:600;color:#64748b">Pris</th></tr></thead><tbody>'
+ _peAccessories.map((s,i)=>'<tr style="border-top:1px solid #f1f5f9'+(s.linked?';background:#f0fdf4':'')+'"><td style="padding:4px 8px"><input type="checkbox" '+(s.linked?'checked':'')+' onchange="_peAccessories['+i+'].linked=this.checked;peRenderAccessories()" style="width:15px;height:15px;accent-color:#0284c7"></td><td style="padding:4px 8px">'+s.name+'</td><td style="padding:4px 8px;text-align:right">'+s.price.toLocaleString('sv-SE')+' kr</td></tr>').join('')
+'</tbody></table>';
}
// === KOPPLAD TILL (reverse lookup med per-link flags) ===
var _peUsedByData = [];
var _peUsedByServiceId = '';
var _peUsedByVariants = [];
function peUpdateLinkFlag(productId, serviceId, field, value) {
var sendValue = (field === 'default_variant') ? value : (value ? 1 : 0);
fetch('/api/products.php?update_link=1', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({product_id: productId, service_id: serviceId, field: field, value: sendValue}) });
}
function peGetVariantsForProduct(productId) {
var prod = catalogProducts.find(function(x) { return x.id === productId; });
if (!prod || !prod.specs) return [];
var specs = typeof prod.specs === 'string' ? JSON.parse(prod.specs) : prod.specs;
var variants = [];
if (specs.styles && specs.styles.length > 0) {
specs.styles.forEach(function(style) {
if (style.variants && style.variants.length > 0) {
style.variants.forEach(function(v) {
variants.push({label: style.style + ' ' + v.width_mm + 'mm', value: style.style + '|' + v.width_mm, price: v.price});
});
} else {
variants.push({label: style.style, value: style.style, price: null});
}
});
} else if (specs.variants && specs.variants.length > 0) {
specs.variants.forEach(function(v) {
var label = Object.entries(v).filter(function(e) { return e[0] !== 'price'; }).map(function(e) { return e[1]; }).join(' ');
variants.push({label: label, value: label, price: v.price});
});
}
return variants;
}
function peShowVariantPicker(rowIdx, productId) {
var sid = _peUsedByServiceId;
var variants = _peUsedByVariants;
var old = document.getElementById('peVariantPicker'); if (old) old.remove();
var picker = document.createElement('div');
picker.id = 'peVariantPicker';
picker.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px';
picker.onclick = function(e) { if (e.target === picker) { _peUsedByData[rowIdx].default_on = 0; _peUsedByData[rowIdx].default_variant = null; peUpdateLinkFlag(productId, sid, 'default_on', false); peRenderUsedBy(); picker.remove(); } };
var currentVariant = _peUsedByData[rowIdx].default_variant || '';
var html = '<div style="background:#fff;border-radius:12px;padding:20px;max-width:400px;width:100%;max-height:70vh;overflow-y:auto;box-shadow:0 10px 40px rgba(0,0,0,.2)">'
+ '<h3 style="margin:0 0 4px;font-size:15px;font-weight:700">Välj förvald variant</h3>'
+ '<p style="margin:0 0 12px;font-size:12px;color:#94a3b8">för ' + _peUsedByData[rowIdx].name + '</p>'
+ '<div style="display:flex;flex-direction:column;gap:4px">';
variants.forEach(function(v) {
var selected = (v.value === currentVariant);
var priceStr = v.price != null ? ' — ' + parseFloat(v.price).toLocaleString('sv-SE') + ' kr' : '';
html += '<button onclick="peSelectVariant(' + rowIdx + ',\'' + productId + '\',\'' + v.value.replace(/'/g, "\\'") + '\')" style="padding:8px 12px;border:1px solid ' + (selected ? '#059669' : '#e5e7eb') + ';border-radius:8px;background:' + (selected ? '#f0fdf4' : '#fff') + ';cursor:pointer;text-align:left;font-size:13px;font-family:inherit">' + v.label + '<span style="color:#94a3b8">' + priceStr + '</span></button>';
});
html += '</div><button onclick="peSelectVariant(' + rowIdx + ',\'' + productId + '\',null)" style="margin-top:10px;padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#94a3b8;font-family:inherit">Ingen specifik variant</button></div>';
picker.innerHTML = html;
document.body.appendChild(picker);
}
function peSelectVariant(rowIdx, productId, variantValue) {
var sid = _peUsedByServiceId;
_peUsedByData[rowIdx].default_on = 1;
_peUsedByData[rowIdx].default_variant = variantValue;
peUpdateLinkFlag(productId, sid, 'default_on', true);
peUpdateLinkFlag(productId, sid, 'default_variant', variantValue);
peRenderUsedBy();
var picker = document.getElementById('peVariantPicker'); if (picker) picker.remove();
}
function peHandleDefaultOn(rowIdx, checked, productId) {
var sid = _peUsedByServiceId;
if (checked && _peUsedByVariants.length > 0) {
peShowVariantPicker(rowIdx, productId);
} else {
_peUsedByData[rowIdx].default_on = checked ? 1 : 0;
if (!checked) { _peUsedByData[rowIdx].default_variant = null; peUpdateLinkFlag(productId, sid, 'default_variant', null); }
peUpdateLinkFlag(productId, sid, 'default_on', checked);
peRenderUsedBy();
}
}
function peLoadUsedBy(productId) {
var block = document.getElementById('peUsedByBlock');
var el = document.getElementById('peUsedByTable');
if (!block || !el) return;
_peUsedByServiceId = productId;
_peUsedByVariants = peGetVariantsForProduct(productId);
fetch('/api/products.php?used_by=' + productId).then(function(r) { return r.json(); }).then(function(data) {
_peUsedByData = data.success ? data.products : [];
if (!_peUsedByData.length) { block.style.display = 'none'; return; }
block.style.display = 'block';
peRenderUsedBy();
var globOblig = document.getElementById('peTillvalOblig');
var globHidden = document.getElementById('peTillvalHidden');
if (globOblig) { globOblig.disabled = true; globOblig.parentElement.style.opacity = '0.4'; }
if (globHidden) { globHidden.disabled = true; globHidden.parentElement.style.opacity = '0.4'; }
var tillvalLabel = document.querySelector('[data-tillval-label]');
if (tillvalLabel) tillvalLabel.innerHTML = 'NÄR PRODUKTEN ANVÄNDS SOM TILLVAL <span style="font-weight:400;color:#b0b0b0">(hanteras per koppling)</span>';
});
}
function peRenderUsedBy() {
var el = document.getElementById('peUsedByTable');
if (!el) return;
var sid = _peUsedByServiceId;
var hasVariants = _peUsedByVariants.length > 0;
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+ '<thead><tr style="background:#f8f9fa;position:sticky;top:0">'
+ '<th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Produkt</th>'
+ '<th style="padding:6px 8px;text-align:center;font-weight:600;color:#64748b;width:' + (hasVariants ? '140' : '70') + 'px">Förvald</th>'
+ '<th style="padding:6px 8px;text-align:center;font-weight:600;color:#64748b;width:80px">Obligatorisk</th>'
+ '<th style="padding:6px 8px;text-align:center;font-weight:600;color:#64748b;width:50px">Dold</th>'
+ '</tr></thead><tbody>'
+ _peUsedByData.map(function(p, i) {
var defOn = parseInt(p.default_on) === 1;
var mand = parseInt(p.mandatory) === 1;
var hid = parseInt(p.hidden) === 1;
var defVar = p.default_variant || '';
var variantLabel = '';
if (defOn && defVar && hasVariants) {
var found = _peUsedByVariants.find(function(v) { return v.value === defVar; });
variantLabel = found ? found.label : defVar;
}
return '<tr style="border-top:1px solid #f1f5f9">'
+ '<td style="padding:5px 8px"><div style="font-weight:500">' + p.name + '</div><div style="font-size:11px;color:#94a3b8">' + (p.cat_label || '') + '</div></td>'
+ '<td style="padding:5px 8px;text-align:center"><div style="display:flex;align-items:center;justify-content:center;gap:4px">'
+ '<input type="checkbox" ' + (defOn ? 'checked' : '') + ' onchange="peHandleDefaultOn(' + i + ',this.checked,\'' + p.id + '\')" style="width:16px;height:16px;accent-color:#059669;cursor:pointer">'
+ (defOn && variantLabel ? '<span onclick="peShowVariantPicker(' + i + ',\'' + p.id + '\')" style="font-size:10px;color:#059669;cursor:pointer;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + variantLabel + '">' + variantLabel + '</span>' : '')
+ '</div></td>'
+ '<td style="padding:5px 8px;text-align:center"><input type="checkbox" ' + (mand ? 'checked' : '') + ' onchange="_peUsedByData[' + i + '].mandatory=this.checked?1:0;peUpdateLinkFlag(\'' + p.id + '\',\'' + sid + '\',\'mandatory\',this.checked)" style="width:16px;height:16px;accent-color:#f59e0b;cursor:pointer"></td>'
+ '<td style="padding:5px 8px;text-align:center"><input type="checkbox" ' + (hid ? 'checked' : '') + ' onchange="_peUsedByData[' + i + '].hidden=this.checked?1:0;peUpdateLinkFlag(\'' + p.id + '\',\'' + sid + '\',\'hidden\',this.checked)" style="width:16px;height:16px;accent-color:#ef4444;cursor:pointer"></td>'
+ '</tr>';
}).join('')
+ '</tbody></table>';
}
// === FLIK-SYSTEM ===
function peSetTab(tab) {
document.querySelectorAll('.pe-tab-panel').forEach(p => p.style.display = 'none');
document.querySelectorAll('.pe-tab').forEach(b => { b.style.borderBottomColor = 'transparent'; b.style.color = '#64748b'; });
const panel = document.getElementById('peTab' + tab.charAt(0).toUpperCase() + tab.slice(1));
if(panel) panel.style.display = 'block';
const btn = document.querySelector('.pe-tab[data-tab="'+tab+'"]');
if(btn) { btn.style.borderBottomColor = '#024550'; btn.style.color = '#024550'; }
// Ladda galleri/spec/dokument vid första öppning
if(tab === 'bilder') peRenderGallery();
if(tab === 'spec') peRenderAttributes();
if(tab === 'dokument') peRenderDocuments();
}
// === GALLERI ===
let _peGallery = [];
let _peProductId = '';
function peInitGallery(productId, gallery) {
_peProductId = productId;
_peGallery = Array.isArray(gallery) ? [...gallery] : [];
}
function peRenderGallery() {
const el = document.getElementById('peGalleryGrid');
if(!el) return;
if(!_peGallery.length) { el.innerHTML = '<div style="grid-column:1/-1;padding:20px;text-align:center;color:#94a3b8;font-size:12px;border:1px dashed #e5e7eb;border-radius:8px">Inga galleribilder</div>'; return; }
el.innerHTML = _peGallery.map((img, i) =>
'<div style="position:relative;aspect-ratio:1;border-radius:8px;overflow:hidden;border:1px solid #e5e7eb;cursor:pointer" onclick="peSetMainImage('+i+')" title="Klicka för att sätta som huvudbild">'
+'<img src="'+img+'" style="width:100%;height:100%;object-fit:cover">'
+'<button onclick="event.stopPropagation();peRemoveGalleryImage('+i+')" style="position:absolute;top:2px;right:2px;width:20px;height:20px;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:50%;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1">×</button>'
+'</div>'
).join('');
}
async function peUploadGalleryImages(productId) {
const input = document.getElementById('peGalleryInput');
if(!input || !input.files.length) return;
for(const file of input.files) {
const fd = new FormData();
fd.append('id', productId);
fd.append('image', file);
try {
const res = await fetch('/api/products.php?upload=1&gallery=1', { method:'POST', body: fd });
const data = await res.json();
if(data.success && data.gallery) {
_peGallery = data.gallery;
}
} catch(e) { console.error('Gallery upload error:', e); }
}
peRenderGallery();
input.value = '';
}
async function peRemoveGalleryImage(idx) {
const imgPath = _peGallery[idx];
if(!imgPath) return;
try {
const res = await fetch('/api/products.php?remove_gallery=1', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id: _peProductId, img_path: imgPath })
});
const data = await res.json();
if(data.success) { _peGallery = data.gallery || []; peRenderGallery(); }
} catch(e) { console.error('Remove gallery error:', e); }
}
async function peSetMainImage(idx) {
const galleryImg = _peGallery[idx];
if(!galleryImg) return;
// Byt: gammal huvudbild → gallery, galleribild → huvudbild
const mainPreview = document.getElementById('peImgPreview');
const currentMain = mainPreview?.querySelector('img')?.src || '';
// Uppdatera preview
mainPreview.innerHTML = '<img src="'+galleryImg+'" style="width:100%;height:100%;object-fit:cover">';
// Byt i gallery-array
_peGallery.splice(idx, 1);
// Lägg gammal huvudbild i gallery om den finns
if(currentMain && !currentMain.includes('data:')) {
// Extrahera relativ path
const url = new URL(currentMain, window.location.origin);
const relPath = url.pathname.replace(/^\//, '');
if(relPath.startsWith('Photo/')) _peGallery.unshift(relPath);
}
// Spara direkt till DB
try {
await fetch('/api/products.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id: _peProductId, img: galleryImg, gallery: _peGallery })
});
} catch(e) { console.error('Set main image error:', e); }
peRenderGallery();
}
// === SPECIFIKATIONER (nyckel-värde) ===
let _peAttributes = [];
function peInitAttributes(specs) {
_peAttributes = (specs && specs.attributes) ? [...specs.attributes] : [];
}
function peRenderAttributes() {
const el = document.getElementById('peAttributesTable');
if(!el) return;
if(!_peAttributes.length) { el.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:12px">Inga specifikationer. Klicka "+ Lägg till".</div>'; return; }
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0"><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Egenskap</th><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Värde</th><th style="padding:6px 8px;width:30px"></th></tr></thead><tbody>'
+ _peAttributes.map((a, i) =>
'<tr style="border-top:1px solid #f1f5f9">'
+'<td style="padding:4px 6px"><input value="'+((a.key||'').replace(/"/g,'"'))+'" onchange="_peAttributes['+i+'].key=this.value" style="width:100%;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit" placeholder="t.ex. Vikt"></td>'
+'<td style="padding:4px 6px"><input value="'+((a.value||'').replace(/"/g,'"'))+'" onchange="_peAttributes['+i+'].value=this.value" style="width:100%;padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:12px;font-family:inherit" placeholder="t.ex. 45 kg"></td>'
+'<td style="padding:4px 6px;text-align:center"><button onclick="_peAttributes.splice('+i+',1);peRenderAttributes()" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:14px">×</button></td>'
+'</tr>'
).join('')
+'</tbody></table>';
}
function peAddAttribute() {
_peAttributes.push({ key: '', value: '' });
peRenderAttributes();
}
// === REGLER (tillval ↔ varianter) ===
let _peRules = [];
function peInitRules(specs) {
_peRules = (specs && specs.rules) ? [...specs.rules] : [];
}
function peGetVariantFields() {
if(_peSpecsType === 'window_sizes' && _peSizes.length) return ['w','h','price','note','article_id'];
if(_peSpecsType === 'style_width_variants' && _peStyles.length) return ['style','width_mm','price'];
if(_peSpecsType === 'width_variants' && _peVariants.length) return ['width_mm','price','label'];
if(!_peVariants.length) return [];
return Object.keys(_peVariants[0]).filter(k => k !== '__idx');
}
function peGetTillvalOptions() {
const svc = (_peServices || []).filter(s => s.linked).map(s => ({id: s.id, name: s.name}));
const acc = (_peAccessories || []).filter(s => s.linked).map(s => ({id: s.id, name: s.name}));
return [...svc, ...acc];
}
function peRenderRules() {
const el = document.getElementById('peRulesTable');
if(!el) return;
const fields = peGetVariantFields();
const tillval = peGetTillvalOptions();
const operators = [{v:'*',l:'Alla'},{v:'==',l:'='},{v:'!=',l:'≠'},{v:'<',l:'<'},{v:'<=',l:'≤'},{v:'>',l:'>'},{v:'>=',l:'≥'}];
const actions = [
{v:'max',l:'Max antal',g:'Antal'},
{v:'min',l:'Min antal',g:'Antal'},
{v:'berakna_antal',l:'Beräkna antal',g:'Antal'},
{v:'kraver',l:'Kräver',g:'Villkor'},
{v:'exkluderar',l:'Exkluderar',g:'Villkor'},
{v:'inkluderar',l:'Inkluderar',g:'Villkor'},
{v:'satt_pris',l:'Sätt pris',g:'Pris'},
{v:'pris_tillagg',l:'Pristillägg (+/-)',g:'Pris'},
{v:'pris_per_enhet',l:'Pris × enhet',g:'Pris'},
{v:'pris_multiplikator',l:'Pris × faktor',g:'Pris'},
{v:'satt_langd',l:'Längd (mm)',g:'Dimension'},{v:'satt_bredd',l:'Bredd (mm)',g:'Dimension'},{v:'satt_grundpris',l:'Sätt produktpris',g:'Produkt'}
];
let html = '';
if(!_peRules.length) {
html = '<div style="padding:16px;text-align:center;color:#94a3b8;font-size:12px">Inga regler. Klicka "+ Lägg till".</div>';
} else {
html = _peRules.map((r, i) => {
const fieldOpts = fields.map(f => '<option value="'+f+'"'+(f===r.field?' selected':'')+'>'+f+'</option>').join('');
const opOpts = operators.map(o => '<option value="'+o.v+'"'+(o.v===r.operator?' selected':'')+'>'+o.l+'</option>').join('');
const tvOpts = tillval.map(t => '<option value="'+t.id+'"'+(t.id===r.tillval_id?' selected':'')+'>'+t.name+'</option>').join('');
const groups = {};
actions.forEach(a => { if(!groups[a.g]) groups[a.g]=[]; groups[a.g].push(a); });
const actOpts = Object.entries(groups).map(([g,acts]) => '<optgroup label="'+g+'">'+acts.map(a=>'<option value="'+a.v+'"'+(a.v===r.action?' selected':'')+'>'+a.l+'</option>').join('')+'</optgroup>').join('');
const s = 'padding:4px 6px;border:1px solid #e5e7eb;border-radius:4px;font-size:11px;font-family:inherit;background:#fff';
const isProductAction = r.action === 'satt_grundpris';
const isDimension = r.action==='satt_langd'||r.action==='satt_bredd';
const needsFormula = ['berakna_antal','pris_per_enhet','pris_multiplikator','satt_grundpris','satt_langd','satt_bredd'].includes(r.action);
const chainOpHtml = isDimension ? '<select onchange="_peRules['+i+'].chain_op=this.value" style="padding:3px 2px;border:1.5px solid #bae6fd;border-radius:4px;font-size:13px;font-weight:700;text-align:center;color:#0284c7;background:#f0f9ff;width:36px;font-family:inherit;cursor:pointer"><option value="="'+((r.chain_op||'=')=='='?' selected':'')+'>=</option><option value="+"'+(r.chain_op=='+'?' selected':'')+'>+</option><option value="-"'+(r.chain_op=='-'?' selected':'')+'>−</option><option value="*"'+(r.chain_op=='*'?' selected':'')+'>×</option></select>' : '<span style="font-size:10px;color:#64748b">=</span>';
return '<div style="border:1px solid #e5e7eb;border-radius:8px;padding:10px;margin-bottom:6px;background:#fafbfc;position:relative">'
+'<div style="position:absolute;top:6px;left:10px;font-size:10px;font-weight:700;color:#cbd5e1">'+(i+1)+'</div>'
+'<button onclick="_peRules.splice('+i+',1);peRenderRules()" style="position:absolute;top:4px;right:6px;background:none;border:none;color:#ef4444;cursor:pointer;font-size:14px">×</button>'
+'<div style="display:grid;grid-template-columns:28px 1fr 50px 60px;gap:4px;align-items:center;margin-bottom:6px">'
+'<span style="font-size:10px;font-weight:700;color:#64748b;text-transform:uppercase">OM</span>'
+'<select onchange="_peRules['+i+'].field=this.value;peRenderRules()" style="'+s+'"><option value="">Alla</option>'+fieldOpts+'</select>'
+'<select onchange="_peRules['+i+'].operator=this.value" style="'+s+'">'+opOpts+'</select>'
+'<input value="'+((r.value||'').toString().replace(/"/g,'"'))+'" onchange="_peRules['+i+'].value=this.value" style="'+s+'" placeholder="\u2014">'
+'</div>'
+'<div style="display:grid;grid-template-columns:28px 1fr 36px 1fr;gap:4px;align-items:center">'
+'<span style="font-size:10px;font-weight:700;color:#64748b;text-transform:uppercase">S\u00c5</span>'
+'<select onchange="_peRules['+i+'].action=this.value;peRenderRules()" style="'+s+'">'+actOpts+'</select>'
+chainOpHtml
+'<input value="'+((r.action_value||'').toString().replace(/"/g,'"'))+'" onchange="_peRules['+i+'].action_value=this.value" style="'+s+';'+(needsFormula?'font-family:monospace;':'') +'" placeholder="'+(needsFormula?'{w} * 100':'v\u00e4rde')+'">'
+'</div>'
+(isProductAction ? '' : '<div style="margin-top:4px;display:grid;grid-template-columns:28px 1fr;gap:4px;align-items:center"><span style="font-size:10px;font-weight:700;color:#64748b">P\u00c5</span><select onchange="_peRules['+i+'].tillval_id=this.value" style="'+s+'"><option value="">\u2014 egen produkt \u2014</option>'+tvOpts+'</select></div>')
+'<div style="margin-top:4px;padding-left:32px"><input value="'+((r.label||'').replace(/"/g,'"'))+'" onchange="_peRules['+i+'].label=this.value" style="'+s+';width:100%;color:#94a3b8;font-style:italic" placeholder="Notering..."></div>'
+'</div>';
}).join('');
}
// Hjälptext med tillgängliga fält
let helpFields = fields.length ? fields.map(f => '<code style="background:#e0f2fe;padding:1px 4px;border-radius:3px;font-size:10px">{'+f+'}</code>').join(' ') : '<em>Lägg till varianter först</em>';
el.innerHTML = html
+'<div style="padding:8px;border-top:1px solid #e5e7eb;font-size:10px;color:#94a3b8;line-height:1.6">'
+'<strong>Formler:</strong> Använd '+helpFields+' i värdefält. '
+'Exempel: <code style="background:#f1f5f9;padding:1px 4px;border-radius:3px;font-size:10px">Math.floor({bredd} / 40)</code> · '
+'<code style="background:#f1f5f9;padding:1px 4px;border-radius:3px;font-size:10px">{pris_per_m2} * {area}</code> · '
+'<code style="background:#f1f5f9;padding:1px 4px;border-radius:3px;font-size:10px">{totalt} * 1.2</code>'
+'</div>';
}
function peAddRule() {
_peRules.push({ field:'', operator:'*', value:'', tillval_id:'', action:'satt_pris', action_value:'', label:'', chain_op:'=' });
peRenderRules();
}
// Utvärdera en regel-formel med variant-data
function peEvalFormula(formula, variantData) {
if(!formula) return 0;
// Om rent nummer
const num = parseFloat(formula);
if(!isNaN(num) && String(num) === String(formula).trim()) return num;
// Ersätt {fält} med värden
let expr = String(formula).replace(/\{(\w+)\}/g, (_, key) => {
const val = variantData[key];
return val !== undefined && val !== '' ? parseFloat(val) || 0 : 0;
});
try { return Function('"use strict"; return (' + expr + ')')(); }
catch(e) { console.warn('Formelfel:', expr, e); return 0; }
}
// Kör alla regler mot en variant-rad och returnera beräknade tillval
function peApplyRules(rules, variantData, tillvalList) {
const result = {};
(rules || []).forEach(rule => {
// Kolla villkor
if(rule.operator !== '*' && rule.field) {
const fieldVal = parseFloat(variantData[rule.field]) || 0;
const ruleVal = parseFloat(rule.value) || 0;
let match = false;
switch(rule.operator) {
case '==': match = fieldVal == ruleVal; break;
case '!=': match = fieldVal != ruleVal; break;
case '<': match = fieldVal < ruleVal; break;
case '<=': match = fieldVal <= ruleVal; break;
case '>': match = fieldVal > ruleVal; break;
case '>=': match = fieldVal >= ruleVal; break;
}
if(!match) return;
}
const tv = rule.tillval_id || '__product';
if(!result[tv]) result[tv] = { pris: null, antal: null, inkluderad: null, exkluderad: false };
const val = peEvalFormula(rule.action_value, variantData);
switch(rule.action) {
case 'max': result[tv].max = val; break;
case 'min': result[tv].min = val; break;
case 'berakna_antal': result[tv].antal = val; break;
case 'kraver': result[tv].inkluderad = true; result[tv].antal = result[tv].antal || 1; break;
case 'exkluderar': result[tv].exkluderad = true; break;
case 'inkluderar': result[tv].inkluderad = true; break;
case 'satt_pris': result[tv].pris = val; break;
case 'pris_tillagg': result[tv].pris = (result[tv].pris || 0) + val; break;
case 'pris_per_enhet': result[tv].pris = val; break;
case 'pris_multiplikator': result[tv].multiplikator = val; break;
case 'satt_langd': { var op=rule.chain_op||'='; if(op==='+') result[tv].langd=(result[tv].langd||0)+val; else if(op==='-') result[tv].langd=(result[tv].langd||0)-val; else if(op==='*') result[tv].langd=(result[tv].langd||0)*val; else result[tv].langd=val; break; } case 'satt_bredd': { var op=rule.chain_op||'='; if(op==='+') result[tv].bredd=(result[tv].bredd||0)+val; else if(op==='-') result[tv].bredd=(result[tv].bredd||0)-val; else if(op==='*') result[tv].bredd=(result[tv].bredd||0)*val; else result[tv].bredd=val; break; } case 'satt_grundpris': result['__product'] = result['__product'] || {}; result['__product'].pris = val; break;
}
});
return result;
}
// === DOKUMENT ===
let _peDocuments = [];
function peInitDocuments(documents) {
_peDocuments = Array.isArray(documents) ? [...documents] : [];
}
function peRenderDocuments() {
const el = document.getElementById('peDocumentsList');
if(!el) return;
if(!_peDocuments.length) { el.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:12px">Inga dokument uppladdade.</div>'; return; }
const typeLabels = { manual:'Manual', datablad:'Datablad', certifikat:'Certifikat', ovrigt:'Övrigt' };
el.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px">'
+'<thead><tr style="background:#f8f9fa;position:sticky;top:0"><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Dokument</th><th style="padding:6px 8px;text-align:left;font-weight:600;color:#64748b">Typ</th><th style="padding:6px 8px;text-align:right;font-weight:600;color:#64748b">Storlek</th><th style="padding:6px 8px;width:60px"></th></tr></thead><tbody>'
+ _peDocuments.map((d, i) => {
const sizeStr = d.size ? (d.size > 1048576 ? (d.size/1048576).toFixed(1)+' MB' : (d.size/1024).toFixed(0)+' KB') : '—';
return '<tr style="border-top:1px solid #f1f5f9">'
+'<td style="padding:6px 8px"><a href="'+d.file+'" target="_blank" style="color:#0284c7;text-decoration:none;font-weight:500">'+escHtml(d.name)+'</a></td>'
+'<td style="padding:6px 8px;color:#64748b"><span style="background:#f1f5f9;padding:2px 8px;border-radius:4px;font-size:11px">'+(typeLabels[d.type]||d.type)+'</span></td>'
+'<td style="padding:6px 8px;text-align:right;color:#94a3b8">'+sizeStr+'</td>'
+'<td style="padding:6px 8px;text-align:center"><button onclick="peRemoveDocument('+i+')" style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:14px" title="Ta bort">×</button></td>'
+'</tr>';
}).join('')
+'</tbody></table>';
}
async function peUploadDocument(productId) {
const input = document.getElementById('peDocInput');
if(!input || !input.files.length) return;
const file = input.files[0];
const docType = document.getElementById('peDocType')?.value || 'manual';
const fd = new FormData();
fd.append('id', productId);
fd.append('document', file);
fd.append('doc_name', file.name.replace(/\.[^.]+$/, ''));
fd.append('doc_type', docType);
try {
const res = await fetch('/api/products.php?upload=1&document=1', { method:'POST', body: fd });
const data = await res.json();
if(data.success && data.documents) {
_peDocuments = data.documents;
peRenderDocuments();
} else {
alert('Fel: ' + (data.error || 'okänt'));
}
} catch(e) { alert('Uppladdningsfel: ' + e.message); }
input.value = '';
}
async function peRemoveDocument(idx) {
const doc = _peDocuments[idx];
if(!doc) return;
try {
const res = await fetch('/api/products.php?remove_document=1', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id: _peProductId, doc_file: doc.file })
});
const data = await res.json();
if(data.success) { _peDocuments = data.documents || []; peRenderDocuments(); }
} catch(e) { console.error('Remove document error:', e); }
}
let _peCroppedFile = null;
function previewProductImage(input) {
if(!input.files || !input.files[0]) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = e => openImageCropper(e.target.result, file.name);
reader.readAsDataURL(file);
}
function openImageCropper(dataUrl, fileName) {
// Remove existing cropper
document.getElementById('imageCropperModal')?.remove();
const modal = document.createElement('div');
modal.id = 'imageCropperModal';
modal.style.cssText = 'position:fixed;inset:0;z-index:100001;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;font-family:Inter,sans-serif';
modal.innerHTML = '<div style="background:#fff;border-radius:16px;width:600px;max-width:95vw;overflow:hidden">'
+'<div style="padding:16px 20px;border-bottom:1px solid #e5e7eb;display:flex;justify-content:space-between;align-items:center">'
+'<h3 style="margin:0;font-size:16px;font-weight:700;color:#1a1a1a">Beskär bild</h3>'
+'<button onclick="document.getElementById(\'imageCropperModal\').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8;line-height:1">×</button>'
+'</div>'
// Crop area
+'<div style="position:relative;width:100%;aspect-ratio:16/9;overflow:hidden;background:#111;cursor:grab" id="cropContainer">'
+'<img id="cropImage" src="'+dataUrl+'" style="position:absolute;transform-origin:0 0;user-select:none;-webkit-user-drag:none" draggable="false">'
+'</div>'
// Controls
+'<div style="padding:16px 20px">'
+'<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">'
+'<span style="font-size:12px;color:#64748b;min-width:50px">Zoom</span>'
+'<input type="range" id="cropZoom" min="10" max="300" value="100" style="flex:1;accent-color:#024550" oninput="updateCropZoom(this.value)">'
+'<span id="cropZoomLabel" style="font-size:12px;color:#64748b;min-width:40px;text-align:right">100%</span>'
+'</div>'
+'<div style="display:flex;gap:8px">'
+'<button onclick="applyCrop()" style="flex:1;padding:10px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Använd beskärning</button>'
+'<button onclick="applyCropFull()" style="padding:10px 16px;background:#f1f5f9;color:#334155;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Hela bilden</button>'
+'<button onclick="document.getElementById(\'imageCropperModal\').remove()" style="padding:10px 16px;background:#fee2e2;color:#dc2626;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Avbryt</button>'
+'</div>'
+'</div>'
+'</div>';
document.body.appendChild(modal);
// Init crop state
const img = document.getElementById('cropImage');
const container = document.getElementById('cropContainer');
let scale = 1, posX = 0, posY = 0, dragging = false, startX = 0, startY = 0;
img.onload = function() {
// Fit image to cover container
const cw = container.offsetWidth, ch = container.offsetHeight;
const iw = img.naturalWidth, ih = img.naturalHeight;
const coverScale = Math.max(cw / iw, ch / ih);
scale = coverScale;
posX = (cw - iw * scale) / 2;
posY = (ch - ih * scale) / 2;
img.style.transform = 'translate('+posX+'px,'+posY+'px) scale('+scale+')';
document.getElementById('cropZoom').value = 100;
document.getElementById('cropZoomLabel').textContent = '100%';
// Store base scale
img._baseScale = coverScale;
img._scale = scale;
img._posX = posX;
img._posY = posY;
};
if(img.complete) img.onload();
// Drag
container.onmousedown = function(e) {
dragging = true;
startX = e.clientX - (img._posX || 0);
startY = e.clientY - (img._posY || 0);
container.style.cursor = 'grabbing';
e.preventDefault();
};
document.addEventListener('mousemove', window._cropMouseMove = function(e) {
if(!dragging) return;
img._posX = e.clientX - startX;
img._posY = e.clientY - startY;
img.style.transform = 'translate('+img._posX+'px,'+img._posY+'px) scale('+(img._scale||1)+')';
});
document.addEventListener('mouseup', window._cropMouseUp = function() {
dragging = false;
if(container) container.style.cursor = 'grab';
});
// Touch support
container.ontouchstart = function(e) {
if(e.touches.length === 1) {
dragging = true;
startX = e.touches[0].clientX - (img._posX || 0);
startY = e.touches[0].clientY - (img._posY || 0);
e.preventDefault();
}
};
container.ontouchmove = function(e) {
if(!dragging || e.touches.length !== 1) return;
img._posX = e.touches[0].clientX - startX;
img._posY = e.touches[0].clientY - startY;
img.style.transform = 'translate('+img._posX+'px,'+img._posY+'px) scale('+(img._scale||1)+')';
e.preventDefault();
};
container.ontouchend = function() { dragging = false; };
// Scroll wheel zoom
container.onwheel = function(e) {
e.preventDefault();
const slider = document.getElementById('cropZoom');
let v = parseInt(slider.value) + (e.deltaY < 0 ? 10 : -10);
v = Math.max(10, Math.min(300, v));
slider.value = v;
updateCropZoom(v);
};
// Store filename for later
img._fileName = fileName;
}
function updateCropZoom(val) {
const img = document.getElementById('cropImage');
if(!img || !img._baseScale) return;
const pct = parseInt(val);
img._scale = img._baseScale * (pct / 100);
img.style.transform = 'translate('+(img._posX||0)+'px,'+(img._posY||0)+'px) scale('+img._scale+')';
document.getElementById('cropZoomLabel').textContent = pct + '%';
}
function applyCrop() {
const img = document.getElementById('cropImage');
const container = document.getElementById('cropContainer');
if(!img || !container) return;
const cw = container.offsetWidth, ch = container.offsetHeight;
const canvas = document.createElement('canvas');
canvas.width = cw * 2; // 2x for retina
canvas.height = ch * 2;
const ctx = canvas.getContext('2d');
ctx.scale(2, 2);
// Draw image with current transform
const s = img._scale || 1;
const px = img._posX || 0;
const py = img._posY || 0;
ctx.drawImage(img, px, py, img.naturalWidth * s, img.naturalHeight * s);
canvas.toBlob(function(blob) {
_peCroppedFile = new File([blob], (img._fileName || 'cropped.webp').replace(/\.[^.]+$/, '.webp'), { type: 'image/webp' });
// Update preview
const preview = document.getElementById('peImgPreview');
if(preview) preview.innerHTML = '<img src="'+URL.createObjectURL(blob)+'" style="width:100%;height:100%;object-fit:cover">';
document.getElementById('peUploadStatus').textContent = 'Beskuren bild (' + (blob.size/1024).toFixed(0) + ' KB) — sparas vid klick på Spara';
// Cleanup
document.removeEventListener('mousemove', window._cropMouseMove);
document.removeEventListener('mouseup', window._cropMouseUp);
document.getElementById('imageCropperModal')?.remove();
}, 'image/webp', 0.9);
}
function applyCropFull() {
const img = document.getElementById('cropImage');
if(!img) return;
// Use full original image, just convert to webp at original size
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(function(blob) {
_peCroppedFile = new File([blob], (img._fileName || 'full.webp').replace(/\.[^.]+$/, '.webp'), { type: 'image/webp' });
const preview = document.getElementById('peImgPreview');
if(preview) preview.innerHTML = '<img src="'+URL.createObjectURL(blob)+'" style="width:100%;height:100%;object-fit:cover">';
document.getElementById('peUploadStatus').textContent = 'Hela bilden (' + (blob.size/1024).toFixed(0) + ' KB) — sparas vid klick på Spara';
document.removeEventListener('mousemove', window._cropMouseMove);
document.removeEventListener('mouseup', window._cropMouseUp);
document.getElementById('imageCropperModal')?.remove();
}, 'image/webp', 0.9);
}
async function saveProduct(id) {
const catSel = document.getElementById('peCat');
const stockSel = document.getElementById('peStock');
const catLabel = catSel.options[catSel.selectedIndex].text;
const stockClass = stockSel.options[stockSel.selectedIndex].dataset.class;
// Upload image first if cropped or selected
const fileInput = document.getElementById('peFileInput');
let imgPath = '';
const imageFile = _peCroppedFile || (fileInput && fileInput.files && fileInput.files[0]);
if(imageFile) {
const fd = new FormData();
fd.append('id', id);
fd.append('image', imageFile);
document.getElementById('peUploadStatus').textContent = 'Laddar upp bild...';
try {
const upRes = await fetch('/api/products.php?upload=1', { method:'POST', body: fd });
const upData = await upRes.json();
if(upData.success) {
imgPath = upData.img;
document.getElementById('peUploadStatus').textContent = 'Bild uppladdad!';
} else {
document.getElementById('peUploadStatus').textContent = 'Fel: ' + (upData.error || 'okänt');
return;
}
} catch(e) {
document.getElementById('peUploadStatus').textContent = 'Uppladdningsfel: ' + e.message;
return;
}
}
const body = {
id: id,
name: document.getElementById('peName').value,
cat: catSel.value,
cat_label: catLabel,
description: document.getElementById('peDesc').value,
price: parseFloat(document.getElementById('pePrice').value) || 0,
cost_price: parseFloat(document.getElementById('peCostPrice').value) || null, cost_currency: document.getElementById('peCostCurrency')?.value || 'SEK',
stock: stockSel.value,
stock_class: stockClass,
green_tech_eligible: document.getElementById('peGreenTech').checked ? 1 : 0,
rot_eligible: document.getElementById('peRotEligible')?.checked ? 1 : 0,
supplier_id: parseInt(document.getElementById('peSupplier')?.value) || null,
unit: document.getElementById('peUnit')?.value || 'st',
markup_type: document.getElementById('peMarkupType')?.value || 'percent',
markup_value: parseFloat(document.getElementById('peMarkupValue')?.value) || 0,
tillval_obligatorisk: document.getElementById('peTillvalOblig')?.checked ? 1 : 0,
tillval_hidden: document.getElementById('peTillvalHidden')?.checked ? 1 : 0
};
if(imgPath) body.img = imgPath;
// Spara varianter + attribut i specs
const existingSpecs = catalogProducts.find(x=>x.id===id)?.specs || {};
body.specs = {...existingSpecs};
// Spara rätt specs-typ
if(_peSpecsType === 'style_width_variants') {
body.specs.type = 'style_width_variants';
body.specs.styles = _peStyles;
delete body.specs.variants;
} else if(_peSpecsType === 'width_variants') {
body.specs.type = 'width_variants';
body.specs.variants = _peVariants;
} else if(_peSpecsType === 'window_sizes') {
body.specs.type = 'window_sizes';
body.specs.sizes = _peSizes;
body.specs.widths = _peSpecsMeta.widths;
body.specs.heights = _peSpecsMeta.heights;
body.specs.supplier = _peSpecsMeta.supplier;
body.specs.model = _peSpecsMeta.model;
} else if(_peVariants.length) body.specs.variants = _peVariants;
if(_peAttributes.length) body.specs.attributes = _peAttributes.filter(a => a.key || a.value);
else delete body.specs.attributes;
if(_peRules.length) body.specs.rules = _peRules.filter(r => r.action && r.action_value);
else delete body.specs.rules;
// Spara tjänst- och tillbehörs-kopplingar (båda i product_services)
const linkedSvc = _peServices.filter(s=>s.linked).map(s=>s.id);
const linkedAcc = _peAccessories.filter(s=>s.linked).map(s=>s.id);
body._services = [...linkedSvc, ...linkedAcc];
try {
const res = await fetch('/api/products.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const data = await res.json();
if(data.success) {
document.getElementById('productEditModal')?.remove();
await loadCatalogFromDB();
filterCatalog();
} else {
alert('Fel: ' + (data.error || 'okänt'));
}
} catch(e) {
alert('Fel: ' + e.message);
}
}
async function deleteProduct(id) {
if(!confirm('Ta bort produkt ' + id + '?')) return;
try {
const res = await fetch('/api/products.php?id=' + id, { method: 'DELETE' });
const data = await res.json();
if(data.success) {
document.getElementById('productEditModal')?.remove();
await loadCatalogFromDB();
filterCatalog();
}
} catch(e) { alert('Fel: ' + e.message); }
}
function showAddProductModal() {
const cats = [{v:'solceller',l:'Solceller'},{v:'batteri',l:'Batteri'},{v:'batteri_utbyggnad',l:'Batteri Utbyggnad'},{v:'laddbox',l:'Laddbox'},{v:'fonster',l:'Fönster'},{v:'dorrar',l:'Dörrar'},{v:'tak',l:'Tak'},{v:'varmepump',l:'Värmepump'},{v:'taktvatt',l:'Taktvätt'},{v:'isolering',l:'Isolering'},{v:'tjanster',l:'Tjänster'},{v:'tillbehor',l:'Tillbehör'}];
const stocks = [{v:'I lager',c:'green'},{v:'Få kvar',c:'yellow'},{v:'Beställning',c:'blue'},{v:'Slut',c:'red'}];
const modal = document.createElement('div');
modal.id = 'productEditModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:600px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:20px 24px;border-bottom:1px solid #f1f5f9"><h2 style="font-size:18px;font-weight:700;margin:0">Ny produkt</h2></div>'
+'<div style="padding:24px">'
+'<div id="peImgPreview" style="width:100%;aspect-ratio:16/9;background:#f8f9fa;border-radius:10px;border:2px dashed #e5e7eb;display:flex;align-items:center;justify-content:center;overflow:hidden;cursor:pointer;margin-bottom:8px" onclick="document.getElementById(\'peFileInput\').click()">'
+'<div style="text-align:center;color:#94a3b8"><svg viewBox="0 0 24 24" style="width:40px;height:40px;stroke:#cbd5e1;fill:none;stroke-width:1.5;margin:0 auto 8px;display:block"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg><div style="font-size:13px">Klicka för att ladda upp bild</div></div>'
+'</div>'
+'<input type="file" id="peFileInput" accept="image/*" style="display:none" onchange="previewProductImage(this)">'
+'<div id="peUploadStatus" style="font-size:11px;color:#94a3b8;margin-bottom:12px"></div>'
+'<div style="display:grid;grid-template-columns:100px 1fr;gap:12px 16px;align-items:center">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">ID</label>'
+'<input id="peId" placeholder="t.ex. SOL05" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Namn</label>'
+'<input id="peName" placeholder="Produktnamn" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Kategori</label>'
+'<select id="peCat" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'+cats.map(c=>'<option value="'+c.v+'">'+c.l+'</option>').join('')+'</select>'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Pris (kr)</label>'
+'<input id="pePrice" type="number" placeholder="0" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Watt</label>'
+'<input id="peWatt" type="number" placeholder="Solpaneler" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">kWh</label>'
+'<input id="peKwh" type="number" step="0.1" placeholder="Batteri" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Lagerstatus</label>'
+'<select id="peStock" style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit">'+stocks.map(s=>'<option value="'+s.v+'" data-class="'+s.c+'">'+s.v+'</option>').join('')+'</select>'
+'<label style="font-size:12px;font-weight:600;color:#64748b">Beskrivning</label>'
+'<textarea id="peDesc" rows="3" placeholder="Produktbeskrivning..." style="padding:10px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical"></textarea>'
+'</div>'
+'<div style="display:flex;gap:10px;margin-top:20px">'
+'<button onclick="saveNewProduct()" style="flex:1;padding:12px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Skapa produkt</button>'
+'<button onclick="this.closest(\'#productEditModal\').remove()" style="padding:12px 20px;background:#f1f5f9;color:#64748b;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit">Avbryt</button>'
+'</div></div></div>';
document.body.appendChild(modal);
}
async function saveNewProduct() {
const id = document.getElementById('peId').value.trim();
if(!id) { alert('ID krävs'); return; }
const name = document.getElementById('peName').value.trim();
if(!name) { alert('Namn krävs'); return; }
const catSel = document.getElementById('peCat');
const stockSel = document.getElementById('peStock');
// Create product first
const body = {
id: id,
name: name,
cat: catSel.value,
cat_label: catSel.options[catSel.selectedIndex].text,
description: document.getElementById('peDesc').value,
price: parseFloat(document.getElementById('pePrice').value) || 0,
stock: stockSel.value,
stock_class: stockSel.options[stockSel.selectedIndex].dataset.class,
watt: parseInt(document.getElementById('peWatt').value) || null,
kwh_capacity: parseFloat(document.getElementById('peKwh').value) || null
};
try {
const res = await fetch('/api/products.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const data = await res.json();
if(!data.success) { alert('Fel: ' + (data.error||'')); return; }
// Upload image if selected
const fileInput = document.getElementById('peFileInput');
if(fileInput && fileInput.files && fileInput.files[0]) {
const fd = new FormData();
fd.append('id', id);
fd.append('image', fileInput.files[0]);
await fetch('/api/products.php?upload=1', { method:'POST', body: fd });
}
document.getElementById('productEditModal')?.remove();
await loadCatalogFromDB();
filterCatalog();
} catch(e) { alert('Fel: ' + e.message); }
}
// Init currency rates
loadCurrencySettings();
// Init catalog from DB
loadCatalogFromDB();
/* === DASHBOARD CHARTS === */
function initDashCharts(){
const ctxM=document.getElementById('chartMonthly');
const ctxY=document.getElementById('chartYearly');
if(!ctxM||!ctxY)return;
new Chart(ctxM,{
type:'bar',
data:{
labels:['V6','V7','V8','V9'],
datasets:[
{label:'Order',data:[185000,92500,248000,134200],backgroundColor:'#10b981',borderRadius:4},
{label:'Offerter',data:[87200,53400,96800,42500],backgroundColor:'#eab308',borderRadius:4},
{label:'Förlorade',data:[0,156000,0,73200],backgroundColor:'#ef4444',borderRadius:4}
]
},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:12,font:{family:'Inter',size:11}}}},scales:{y:{beginAtZero:true,ticks:{callback:v=>v>=1000?(v/1000)+'k':''+v,font:{family:'Inter',size:11}},grid:{color:'#f1f5f9'}},x:{grid:{display:false},ticks:{font:{family:'Inter',size:11}}}}}
});
new Chart(ctxY,{
type:'line',
data:{
labels:['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'],
datasets:[
{label:'Försäljning',data:[420000,659700,null,null,null,null,null,null,null,null,null,null],borderColor:'#024550',backgroundColor:'rgba(2,69,80,.08)',fill:true,tension:.3,pointRadius:5,pointBackgroundColor:'#024550'},
{label:'Mål',data:[500000,500000,550000,550000,600000,650000,400000,450000,600000,650000,700000,750000],borderColor:'#e5e7eb',borderDash:[5,5],fill:false,tension:.3,pointRadius:0}
]
},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:12,font:{family:'Inter',size:11}}}},scales:{y:{beginAtZero:true,ticks:{callback:v=>v>=1000?(v/1000)+'k':''+v,font:{family:'Inter',size:11}},grid:{color:'#f1f5f9'}},x:{grid:{display:false},ticks:{font:{family:'Inter',size:11}}}}}
});
}
initDashCharts();
/* === FÄLTSÄLJ === */
/* === FÄLTSÄLJ - Google Maps + Solar API === */
const GOOGLE_MAPS_KEY = 'AIzaSyBJPnfNCe6un1J7ck6co7zr2uvGrxhFYXY';
const faltStatusColors={unvisited:'#94a3b8',interested:'#10b981',kalkyl:'#7c3aed',offert:'#2563eb',not_interested:'#ef4444',not_home:'#eab308',callback:'#3b82f6'};
const faltStatusLabels={unvisited:'Ej besökt',interested:'Intresserad',kalkyl:'Kalkyl',offert:'Offert',not_interested:'Ej intresserad',not_home:'Ej hemma',callback:'Återbesök'};
let gMap=null, gMarker=null, gInfoWindow=null, faltProspects=[], faltGMarkers=[];
let faltActiveIdx = -1;
let faltAutocomplete=null;
function initFaltMap(){
if(gMap) return;
const el = document.getElementById('faltMap');
if(!el) return;
gMap = new google.maps.Map(el, {
center:{lat:59.3293,lng:18.0686}, // Stockholm default
zoom:14,
mapTypeId:'hybrid',
mapTypeControl:true,
streetViewControl:true,
fullscreenControl:true,
});
gInfoWindow = new google.maps.InfoWindow();
// Places autocomplete
const input = document.getElementById('faltAddressInput');
if(input){
faltAutocomplete = new google.maps.places.Autocomplete(input, {
componentRestrictions:{country:'se'},
fields:['formatted_address','geometry','name','address_components'],
});
faltAutocomplete.addListener('place_changed', function(){
const place = faltAutocomplete.getPlace();
if(!place.geometry) return;
const loc = place.geometry.location;
gMap.setCenter(loc);
gMap.setZoom(19);
placeMarkerAndSolar(loc.lat(), loc.lng(), place.formatted_address);
saveAddressHistory(place.formatted_address, loc.lat(), loc.lng());
hideAddressHistory();
});
// Filtrera historik medan man skriver
input.addEventListener('input', function(){ showAddressHistory(); });
// Hide dropdown when clicking outside
document.addEventListener('click', function(e){
if(!document.getElementById('faltSearchWrap')?.contains(e.target)) hideAddressHistory();
});
}
// Click on map
gMap.addListener('click', function(e){
reverseGeocodeAndSolar(e.latLng.lat(), e.latLng.lng());
});
// Auto-center on user's GPS position
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(function(pos){
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
gMap.setCenter({lat, lng});
gMap.setZoom(17);
}, function(){}, {enableHighAccuracy:true, timeout:5000});
}
// Load saved prospects
const saved = localStorage.getItem('faltProspects');
if(saved){
try { faltProspects = JSON.parse(saved); } catch(e) { faltProspects = []; }
}
// Validera kalkyl-kopplingar mot databasen
(async function(){
var needsSave = false;
for(var i = 0; i < faltProspects.length; i++){
var p = faltProspects[i];
var ids = p.quoteIds || (p.quoteId ? [p.quoteId] : []);
if(ids.length === 0) continue;
var validIds = [];
for(var j = 0; j < ids.length; j++){
try {
var res = await fetch('/api/quotes.php?id='+ids[j]);
var data = await res.json();
if(data.success && data.quote) validIds.push(ids[j]);
} catch(e){}
}
if(validIds.length !== ids.length){
needsSave = true;
p.quoteIds = validIds;
p.quoteId = validIds.length > 0 ? validIds[validIds.length - 1] : null;
if(validIds.length === 0 && p.status === 'kalkyl') p.status = 'interested';
}
}
if(needsSave){
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers(); renderFaltList(); updateFaltStats(); renderLeadsTable();
}
})();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderFaltKalkyler();
}
// === DRAW AREA TO ADD PROSPECTS ===
let drawingManager = null;
let drawnShape = null;
function startDrawArea(){
if(!gMap) return;
const btn = document.getElementById('btnDrawArea');
// If already drawing, cancel
if(drawingManager){
drawingManager.setDrawingMode(null);
drawingManager.setMap(null);
drawingManager = null;
if(drawnShape){ drawnShape.setMap(null); drawnShape = null; }
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5"/></svg> Markera område';
btn.style.background = '#024550';
return;
}
btn.innerHTML = 'Klicka för att rita polygon... (dubbelklicka = klar)';
btn.style.background = '#10b981';
drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.POLYGON,
drawingControl: false,
polygonOptions: {
fillColor: '#eab308',
fillOpacity: 0.2,
strokeColor: '#eab308',
strokeWeight: 2,
editable: false,
clickable: false,
}
});
drawingManager.setMap(gMap);
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(polygon){
drawingManager.setDrawingMode(null);
drawingManager.setMap(null);
drawingManager = null;
drawnShape = polygon;
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5"/></svg> Markera område';
btn.style.background = '#024550';
scanPolygonForAddresses(polygon);
});
}
function pointInPolygon(lat, lng, polygon){
return google.maps.geometry ? google.maps.geometry.poly.containsLocation(new google.maps.LatLng(lat, lng), polygon) : true;
}
async function scanPolygonForAddresses(polygon){
const path = polygon.getPath();
// Get bounding box of polygon
let minLat=90, maxLat=-90, minLng=180, maxLng=-180;
path.forEach(p => {
if(p.lat()<minLat) minLat=p.lat();
if(p.lat()>maxLat) maxLat=p.lat();
if(p.lng()<minLng) minLng=p.lng();
if(p.lng()>maxLng) maxLng=p.lng();
});
const geocoder = new google.maps.Geocoder();
const latStep = 0.0003; // ~33m
const lngStep = 0.0005; // ~33m
const points = [];
for(let lat = minLat; lat <= maxLat; lat += latStep){
for(let lng = minLng; lng <= maxLng; lng += lngStep){
// Check if point is inside polygon
if(google.maps.geometry && !google.maps.geometry.poly.containsLocation(new google.maps.LatLng(lat, lng), polygon)) continue;
points.push({lat, lng});
}
}
if(points.length > 300){
alert('Området är för stort ('+points.length+' punkter). Rita ett mindre område eller zooma in mer.');
if(drawnShape){ drawnShape.setMap(null); drawnShape = null; }
return;
}
if(points.length === 0){
alert('Området är för litet. Rita ett större område.');
if(drawnShape){ drawnShape.setMap(null); drawnShape = null; }
return;
}
// Show progress
const btn = document.getElementById('btnDrawArea');
btn.innerHTML = 'Skannar 0/'+points.length+'...';
btn.style.background = '#eab308';
btn.disabled = true;
const foundAddresses = new Map(); // address -> {lat,lng}
let done = 0;
// Process in batches of 5 with delay
for(let batch = 0; batch < points.length; batch += 5){
const chunk = points.slice(batch, batch+5);
const promises = chunk.map(pt =>
new Promise(resolve => {
geocoder.geocode({location: pt}, function(results, status){
if(status === 'OK' && results[0]){
const r = results[0];
// Only street addresses (not just postcode/city)
const hasStreetNumber = r.types.includes('street_address') || r.types.includes('premise') || r.types.includes('subpremise') || r.address_components.some(c=>c.types.includes('street_number'));
if(hasStreetNumber){
const addr = r.formatted_address;
if(!foundAddresses.has(addr)){
foundAddresses.set(addr, {
lat: r.geometry.location.lat(),
lng: r.geometry.location.lng()
});
}
}
}
resolve();
});
})
);
await Promise.all(promises);
done += chunk.length;
btn.innerHTML = 'Skannar '+done+'/'+points.length+'...';
// Small delay to avoid rate limits
if(batch + 5 < points.length) await new Promise(r => setTimeout(r, 200));
}
// Remove shape
if(drawnShape){ drawnShape.setMap(null); drawnShape = null; }
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5"/></svg> Markera område';
btn.style.background = '#024550';
btn.disabled = false;
// Filter out already existing prospects
const newAddresses = [];
foundAddresses.forEach((coords, addr) => {
const exists = faltProspects.some(p => Math.abs(p.lat-coords.lat)<0.0001 && Math.abs(p.lng-coords.lng)<0.0001);
if(!exists) newAddresses.push({addr, ...coords});
});
if(newAddresses.length === 0){
alert('Inga nya fastigheter hittades i området ('+foundAddresses.size+' redan i listan).');
return;
}
if(!confirm('Hittade '+newAddresses.length+' fastigheter. Lägga till alla som prospekt?')) return;
// Add all as prospects
newAddresses.forEach(a => {
const parts = a.addr.split(',');
faltProspects.push({
id: Date.now() + Math.random()*1000|0,
addr: parts[0]?.trim() || a.addr,
city: parts.slice(1).join(',').trim() || '',
lat: a.lat, lng: a.lng,
status: 'unvisited', note: '',
created: new Date().toISOString().split('T')[0],
maxPanels:null, yearlyKwh:null, roofArea:null,
kundNamn:'', kundTel:'', kundEmail:'', product:'',
checkinTime:null,
});
});
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
}
// === ADDRESS HISTORY ===
function getAddressHistory(){
try { return JSON.parse(localStorage.getItem('faltAddrHistory') || '[]'); } catch(e){ return []; }
}
function saveAddressHistory(address, lat, lng){
let hist = getAddressHistory();
// Ta bort duplikat
hist = hist.filter(h => h.address !== address);
hist.unshift({ address, lat, lng, ts: Date.now() });
if(hist.length > 15) hist = hist.slice(0, 15);
localStorage.setItem('faltAddrHistory', JSON.stringify(hist));
}
function showAddressHistory(){
const dd = document.getElementById('addressHistoryDropdown');
if(!dd) return;
const hist = getAddressHistory();
const input = document.getElementById('faltAddressInput');
const val = (input?.value || '').trim().toLowerCase();
if(hist.length === 0 && !val){ dd.style.display='none'; return; }
let items = hist;
// Filtrera om man börjat skriva
if(val.length >= 2){
items = hist.filter(h => h.address.toLowerCase().includes(val));
}
if(items.length === 0){ dd.style.display='none'; return; }
dd.innerHTML = '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center"><span>Senaste sökningar</span><button onclick="event.stopPropagation();clearAddressHistory()" style="background:none;border:none;font-size:10px;color:#94a3b8;cursor:pointer;font-family:inherit;padding:0">Rensa</button></div>'
+ items.map(h => {
const parts = h.address.split(',');
const street = parts[0]?.trim() || h.address;
const rest = parts.slice(1).join(',').trim();
const ago = timeAgo(h.ts);
return '<div onclick="selectAddressHistory(\''+h.address.replace(/'/g,"\\'")+'\','+h.lat+','+h.lng+')" style="padding:10px 12px;cursor:pointer;border-bottom:1px solid #f8f9fa;transition:background .1s;display:flex;align-items:center;gap:10px" onmouseover="this.style.background=\'#f8f9fa\'" onmouseout="this.style.background=\'#fff\'">'
+'<svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:#94a3b8;stroke-width:2;flex-shrink:0"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>'
+'<div style="flex:1;min-width:0"><div style="font-size:13px;font-weight:500;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+street+'</div>'
+(rest?'<div style="font-size:11px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+rest+'</div>':'')
+'</div>'
+'<span style="font-size:10px;color:#cbd5e1;flex-shrink:0">'+ago+'</span>'
+'</div>';
}).join('');
dd.style.display = 'block';
}
function hideAddressHistory(){
const dd = document.getElementById('addressHistoryDropdown');
if(dd) dd.style.display = 'none';
}
function selectAddressHistory(address, lat, lng){
document.getElementById('faltAddressInput').value = address;
hideAddressHistory();
gMap.setCenter({lat, lng});
gMap.setZoom(19);
placeMarkerAndSolar(lat, lng, address);
saveAddressHistory(address, lat, lng);
}
function clearAddressHistory(){
localStorage.removeItem('faltAddrHistory');
hideAddressHistory();
}
function timeAgo(ts){
const diff = Date.now() - ts;
const min = Math.floor(diff/60000);
if(min < 1) return 'nu';
if(min < 60) return min+' min';
const hrs = Math.floor(min/60);
if(hrs < 24) return hrs+' tim';
const days = Math.floor(hrs/24);
return days+' d';
}
function placeMarkerAndSolar(lat, lng, address){
if(gMarker) gMarker.setMap(null);
gMarker = new google.maps.Marker({
position:{lat, lng},
map:gMap,
animation:google.maps.Animation.DROP,
title:address
});
gMap.setCenter({lat, lng});
// Fetch Solar API data
fetchSolarData(lat, lng, address);
}
function reverseGeocodeAndSolar(lat, lng){
const geocoder = new google.maps.Geocoder();
geocoder.geocode({location:{lat, lng}}, function(results, status){
const addr = (status === 'OK' && results[0]) ? results[0].formatted_address : lat.toFixed(5)+', '+lng.toFixed(5);
document.getElementById('faltAddressInput').value = addr;
gMap.setZoom(19);
placeMarkerAndSolar(lat, lng, addr);
saveAddressHistory(addr, lat, lng);
});
}
function faltUseMyLocation(){
if(!navigator.geolocation){ alert('Geolokalisering stöds inte i din webbläsare'); return; }
navigator.geolocation.getCurrentPosition(function(pos){
reverseGeocodeAndSolar(pos.coords.latitude, pos.coords.longitude);
}, function(err){
alert('Kunde inte hämta position: '+err.message);
}, {enableHighAccuracy:true});
}
async function fetchSolarData(lat, lng, address){
const panel = document.getElementById('solarDataPanel');
panel.innerHTML = '<div style="text-align:center;padding:30px;color:#94a3b8"><div class="spinner" style="display:inline-block;width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#024550;border-radius:50%;animation:bgspin .6s linear infinite"></div><p style="margin-top:8px;font-size:13px">Hämtar soldata...</p></div>';
try {
const res = await fetch('https://solar.googleapis.com/v1/buildingInsights:findClosest?location.latitude='+lat+'&location.longitude='+lng+'&requiredQuality=HIGH&key='+GOOGLE_MAPS_KEY);
if(!res.ok){
const err = await res.json().catch(()=>({}));
if(res.status === 404){
lastSolarData = null;
// Fallback: try PVGIS/estimation
await fetchPvgisFallback(lat, lng, address, panel);
} else {
panel.innerHTML = '<div style="padding:18px;color:#ef4444;font-size:13px">Fel: '+(err.error?.message || 'HTTP '+res.status)+'</div>';
}
return;
}
const data = await res.json();
lastSolarData = data;
panel.innerHTML = renderSolarResults(data, address, lat, lng);
// Store solar kWh and auto-lookup resident
const configs = data.solarPotential?.solarPanelConfigs || [];
window._lastSolarKwh = configs.length ? Math.round(configs[configs.length-1].yearlyEnergyDcKwh||0) : 0;
lookupHittaForPanel(address);
} catch(e){
panel.innerHTML = '<div style="padding:18px;color:#ef4444;font-size:13px">Nätverksfel: '+e.message+'</div>';
}
}
async function fetchPvgisFallback(lat, lng, address, panel){
panel.innerHTML = '<div style="text-align:center;padding:30px;color:#94a3b8"><div class="spinner" style="display:inline-block;width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#f59e0b;border-radius:50%;animation:bgspin .6s linear infinite"></div><p style="margin-top:8px;font-size:13px">Google Solar saknar data — beräknar uppskattning...</p></div>';
try {
const res = await fetch('/api/pvgis-proxy.php?lat='+lat+'&lon='+lng);
if(res.ok){
const data = await res.json();
if(data.success){
panel.innerHTML = renderPvgisResults(data, address, lat, lng);
window._lastSolarKwh = data.yearly_kwh || 0;
lookupHittaForPanel(address);
return;
}
}
} catch(e){}
// Total fallback
panel.innerHTML = renderSolarNoData(address, lat, lng);
lookupHittaForPanel(address);
}
function renderPvgisResults(data, address, lat, lng){
const sys = data.system || {};
const panels = sys.panels || 0;
const kWh = data.yearly_kwh || 0;
const source = data.source === 'PVGIS' ? 'PVGIS (EU)' : 'Uppskattning (SMHI-data)';
const sourceColor = data.source === 'PVGIS' ? '#0284c7' : '#ca8a04';
const note = data.note || '';
// Monthly chart bars
let monthlyHtml = '';
if(data.monthly && data.monthly.length) {
const maxM = Math.max(...data.monthly.map(m=>m.kWh));
const months = ['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
monthlyHtml = '<div style="margin-top:16px"><div style="font-size:12px;font-weight:700;color:#1a1a1a;margin-bottom:8px">Månadsproduktion (kWh)</div>'
+'<div style="display:flex;align-items:flex-end;gap:3px;height:80px">'
+data.monthly.map((m,i)=>{
const h = maxM > 0 ? Math.round((m.kWh/maxM)*70) : 0;
return '<div style="flex:1;display:flex;flex-direction:column;align-items:center">'
+'<div style="font-size:9px;color:#64748b;margin-bottom:2px">'+m.kWh+'</div>'
+'<div style="width:100%;height:'+h+'px;background:linear-gradient(180deg,#f59e0b,#eab308);border-radius:3px 3px 0 0"></div>'
+'<div style="font-size:9px;color:#94a3b8;margin-top:2px">'+months[i]+'</div>'
+'</div>';
}).join('')
+'</div></div>';
}
return '<div style="padding:18px">'
+'<div style="font-weight:700;font-size:14px;margin-bottom:4px">'+address+'</div>'
+'<div style="color:#94a3b8;font-size:11px;margin-bottom:8px">'+lat.toFixed(5)+', '+lng.toFixed(5)+'</div>'
+'<div style="display:inline-block;padding:3px 10px;background:'+sourceColor+'15;color:'+sourceColor+';border-radius:6px;font-size:11px;font-weight:600;margin-bottom:14px">'+source+'</div>'
+(note?'<div style="font-size:11px;color:#64748b;margin-bottom:12px">'+note+'</div>':'')
// Key metrics (2 boxes)
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">'
+'<div style="background:#ecfdf5;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#10b981;font-weight:600;text-transform:uppercase">Uppsk. paneler</div><div style="font-size:22px;font-weight:700;color:#166534">'+panels+'</div><div style="font-size:10px;color:#64748b">'+sys.panel_watt+'W / st</div></div>'
+'<div style="background:#eff6ff;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#3b82f6;font-weight:600;text-transform:uppercase">Produktion/år</div><div style="font-size:22px;font-weight:700;color:#1e40af">'+kWh.toLocaleString('sv-SE')+' <span style="font-size:12px">kWh</span></div></div>'
+'</div>'
// Monthly chart
+monthlyHtml
// Actions
+'<div style="display:flex;gap:8px;margin-top:16px">'
+'<button class="dummy-btn" style="flex:1;justify-content:center;font-size:12px" onclick="addAsProspect(\''+address.replace(/'/g,"\\'")+'\','+lat+','+lng+')">+ Prospekt</button>'
+'<a href="'+hittaUrl(address)+'" target="_blank" class="dummy-btn secondary" style="flex:1;justify-content:center;font-size:12px;text-decoration:none">Hitta.se</a>'
+'</div>'
+'<div id="hittaResult" style="margin-top:10px;font-size:12px;color:#94a3b8">Söker boende...</div>'
+'<div id="energyEstimate"></div>'
+'</div>';
}
function renderSolarNoData(address, lat, lng){
return '<div style="padding:18px">'
+'<div style="font-weight:700;font-size:14px;margin-bottom:8px">'+address+'</div>'
+'<div style="color:#94a3b8;font-size:12px;margin-bottom:12px">'+lat.toFixed(5)+', '+lng.toFixed(5)+'</div>'
+'<div style="padding:16px;background:#fef9c3;border-radius:8px;font-size:13px;color:#854d0e">Ingen soldata tillgänglig för denna plats.</div>'
+'<button class="dummy-btn" style="margin-top:12px;width:100%;justify-content:center" onclick="addAsProspect(\''+address.replace(/'/g,"\\'")+'\','+lat+','+lng+')">+ Lägg till som prospekt</button>'
+'<div id="hittaResult" style="margin-top:10px"><button onclick="lookupHittaForPanel(\''+address.replace(/'/g,"\\'")+'\');this.disabled=true;this.textContent=\'Söker...\'" style="padding:6px 12px;border:1px solid #dbeafe;border-radius:6px;font-size:12px;cursor:pointer;background:#eff6ff;font-family:inherit;color:#3b82f6;width:100%">Vem bor här?</button></div>'
+'<div id="energyEstimate"></div>'
+'</div>';
}
function renderSolarResults(data, address, lat, lng){
const si = data.solarPotential || {};
const maxPanels = si.maxArrayPanelsCount || 0;
const maxArea = si.maxArrayAreaMeters2 ? si.maxArrayAreaMeters2.toFixed(0) : '-';
const maxSunHrs = si.maxSunshineHoursPerYear ? si.maxSunshineHoursPerYear.toFixed(0) : '-';
const roofArea = si.wholeRoofStats?.areaMeters2 ? si.wholeRoofStats.areaMeters2.toFixed(0) : '-';
// Best config
const configs = si.solarPanelConfigs || [];
const best = configs[configs.length - 1];
const bestKwh = best ? (best.yearlyEnergyDcKwh||0).toFixed(0) : '-';
const bestPanels = best ? best.panelsCount : maxPanels;
// Roof segments
let segmentsHtml = '';
const segments = si.roofSegmentStats || [];
const directions = {0:'N',45:'NE',90:'Ö',135:'SO',180:'S',225:'SV',270:'V',315:'NV',360:'N'};
const segCount = segments.length;
const segRows = segments.map((seg, i) => {
const az = seg.azimuthDegrees || 0;
let dir = 'N';
for(const [deg, label] of Object.entries(directions)){
if(az >= (deg-22.5) && az < (Number(deg)+22.5)) dir = label;
}
const pitch = seg.pitchDegrees ? seg.pitchDegrees.toFixed(0) : '-';
const area = seg.stats?.areaMeters2 ? seg.stats.areaMeters2.toFixed(0) : '-';
const sun = seg.stats?.sunshineQuantiles?.[10] ? seg.stats.sunshineQuantiles[10].toFixed(0) : '-';
return '<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid #f1f5f9;font-size:12px">'
+'<span>Yta '+(i+1)+' ('+dir+')</span>'
+'<span>'+pitch+'° • '+area+' m² • '+sun+' h/år</span>'
+'</div>';
});
if(segCount > 0){
if(segCount <= 3){
segmentsHtml = segRows.join('');
} else {
// Visa 2 först, resten expanderbart
const rndId = 'seg_'+Date.now();
segmentsHtml = segRows.slice(0,2).join('')
+'<div id="'+rndId+'" style="display:none">'+segRows.slice(2).join('')+'</div>'
+'<button onclick="const el=document.getElementById(\''+rndId+'\');if(el.style.display===\'none\'){el.style.display=\'block\';this.textContent=\'Dölj\';}else{el.style.display=\'none\';this.textContent=\'Visa alla '+segCount+' ytor\';}" style="padding:4px 10px;background:none;border:1px solid #e5e7eb;border-radius:6px;font-size:11px;color:#3b82f6;cursor:pointer;font-family:inherit;margin-top:4px;font-weight:600">Visa alla '+segCount+' ytor</button>';
}
}
return '<div style="padding:18px">'
+'<div style="font-weight:700;font-size:14px;margin-bottom:4px">'+address+'</div>'
+'<div style="color:#94a3b8;font-size:11px;margin-bottom:16px">'+lat.toFixed(5)+', '+lng.toFixed(5)+'</div>'
// Key metrics
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">'
+'<div style="background:#ecfdf5;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#10b981;font-weight:600;text-transform:uppercase">Max paneler</div><div style="font-size:22px;font-weight:700;color:#166534">'+maxPanels+'</div></div>'
+'<div style="background:#eff6ff;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#3b82f6;font-weight:600;text-transform:uppercase">Produktion/år</div><div style="font-size:22px;font-weight:700;color:#1e40af">'+Number(bestKwh).toLocaleString('sv-SE')+' <span style="font-size:12px">kWh</span></div></div>'
+'<div style="background:#faf5ff;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#a855f7;font-weight:600;text-transform:uppercase">Takyta</div><div style="font-size:22px;font-weight:700;color:#6d28d9">'+roofArea+' <span style="font-size:12px">m²</span></div></div>'
+'<div style="background:#fef9c3;padding:12px;border-radius:8px;text-align:center"><div style="font-size:10px;color:#ca8a04;font-weight:600;text-transform:uppercase">Soltimmar/år</div><div style="font-size:22px;font-weight:700;color:#854d0e">'+maxSunHrs+'</div></div>'
+'</div>'
// Roof segments
+(segmentsHtml ? '<div style="margin-bottom:16px"><div style="font-size:12px;font-weight:700;color:#1a1a1a;margin-bottom:6px">Takytor <span style="font-weight:400;color:#64748b">('+segCount+' ytor — lutning • yta • sol)</span></div>'+segmentsHtml+'</div>' : '')
// Actions
+'<div style="display:flex;gap:8px">'
+'<button class="dummy-btn" style="flex:1;justify-content:center;font-size:12px" onclick="addAsProspect(\''+address.replace(/'/g,"\\'")+'\','+lat+','+lng+')">+ Prospekt</button>'
+'<a href="'+hittaUrl(address)+'" target="_blank" class="dummy-btn secondary" style="flex:1;justify-content:center;font-size:12px;text-decoration:none">Hitta.se</a>'
+'</div>'
+'<div id="hittaResult" style="margin-top:10px;font-size:12px;color:#94a3b8">Söker boende...</div>'
+'<div id="energyEstimate"></div>'
+'</div>';
}
function hittaUrl(addr){
// Clean address for hitta.se: remove ", Sweden", postal codes, keep street + city
let clean = addr.replace(/,?\s*Sweden$/i,'').replace(/,?\s*Sverige$/i,'');
// Remove postal code (5 digits with optional space: "856 44" or "85644")
clean = clean.replace(/,?\s*\d{3}\s?\d{2}\s*/g, ', ');
// Clean up multiple commas/spaces
clean = clean.replace(/,\s*,/g,',').replace(/,\s*$/,'').replace(/^\s*,/,'').trim();
return 'https://www.hitta.se/s%C3%B6k?vad='+encodeURIComponent(clean);
}
let lastSolarData = null;
let faltListFilter = 'all';
let checkinIdx = null;
let checkinStatus = null;
function addAsProspect(address, lat, lng){
if(faltProspects.some(p => Math.abs(p.lat-lat)<0.0001 && Math.abs(p.lng-lng)<0.0001)){
alert('Denna adress finns redan i rutten');
return;
}
const parts = address.split(',');
const solar = lastSolarData || {};
const si = solar.solarPotential || {};
const idx = faltProspects.length;
faltProspects.push({
id: Date.now(),
addr: parts[0]?.trim()||address,
city: parts.slice(1).join(',').trim()||'',
lat:lat, lng:lng,
status: 'unvisited',
note: '',
created: new Date().toISOString().split('T')[0],
// Solar
maxPanels: si.maxArrayPanelsCount || null,
yearlyKwh: si.solarPanelConfigs?.length ? Math.round(si.solarPanelConfigs[si.solarPanelConfigs.length-1].yearlyEnergyDcKwh||0) : null,
roofArea: si.wholeRoofStats?.areaMeters2 ? Math.round(si.wholeRoofStats.areaMeters2) : null,
solarScore: null,
// Lead info
kundNamn: '', kundTel: '', kundEmail: '', product: '',
checkinTime: null,
photos: [],
});
// Calculate solar score if we have data already
if(si.maxArrayPanelsCount) {
faltProspects[idx].solarScore = calcSolarScore(si);
}
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
// Auto-fetch solar data in background if not already available
if(!si.maxArrayPanelsCount) {
fetchSolarScoreForProspect(idx, lat, lng);
}
}
function calcSolarScore(si) {
// Score 1-5 based on yearly kWh potential
const configs = si.solarPanelConfigs || [];
const best = configs.length ? configs[configs.length-1] : null;
const kWh = best ? (best.yearlyEnergyDcKwh || 0) : 0;
const panels = si.maxArrayPanelsCount || 0;
const sunHrs = si.maxSunshineHoursPerYear || 0;
// kWh per panel efficiency
const efficiency = panels > 0 ? kWh / panels : 0;
// Score: <200kWh/panel=1, 200-300=2, 300-400=3, 400-500=4, 500+=5
if(efficiency >= 500) return 5;
if(efficiency >= 400) return 4;
if(efficiency >= 300) return 3;
if(efficiency >= 200) return 2;
return 1;
}
// === HITTA.SE CACHE ===
const hittaCache = {};
const HITTA_CACHE_TTL = 10 * 60 * 1000; // 10 minuter
function hittaCacheKey(address) {
return address.trim().toLowerCase().replace(/\s+/g,' ');
}
function getHittaCache(address) {
const key = hittaCacheKey(address);
const entry = hittaCache[key];
if(entry && (Date.now() - entry.ts) < HITTA_CACHE_TTL) return entry.data;
return null;
}
function setHittaCache(address, data) {
// Sortera boende äldst först (högst ålder överst)
if(data && data.persons && data.persons.length > 1){
data.persons.sort((a,b) => (parseInt(b.age)||0) - (parseInt(a.age)||0));
}
hittaCache[hittaCacheKey(address)] = { data: data, ts: Date.now() };
}
// === ENERGIBERÄKNING ===
// Beräknar uppskattad årsförbrukning baserat på boyta
// Svensk genomsnittsvilla: eluppvärmning ~130 kWh/m²/år, värmepump/fjärrvärme ~50 kWh/m²/år
// Elpris: ~1.50 kr/kWh (snitt inkl nätavgift, skatt, elhandel)
const ENERGY_ELECTRIC_HEAT = 130; // kWh/m²/år med direktverkande el
const ENERGY_HEATPUMP = 50; // kWh/m²/år med värmepump/fjärrvärme
const ELECTRICITY_PRICE = 1.50; // kr/kWh snitt Sverige
const SOLAR_SELF_USE = 0.70; // 70% egenanvändning (resten säljs billigare)
const SOLAR_SELL_PRICE = 0.50; // kr/kWh vid överskottsförsäljning
function estimateEnergyConsumption(livingAreaStr) {
// Parsa boyta från Hitta.se format, t.ex. "120 m²" eller "85m²"
if(!livingAreaStr) return null;
const m = livingAreaStr.match(/(\d+)/);
if(!m) return null;
const area = parseInt(m[1]);
if(area < 20 || area > 1000) return null;
const elHeat = Math.round(area * ENERGY_ELECTRIC_HEAT);
const heatPump = Math.round(area * ENERGY_HEATPUMP);
return {
livingAreaM2: area,
withElectricHeat: elHeat,
withHeatPump: heatPump,
// Kostnader per år
costElHeat: Math.round(elHeat * ELECTRICITY_PRICE),
costHeatPump: Math.round(heatPump * ELECTRICITY_PRICE)
};
}
function buildEnergyEstimateHtml(energyEst, solarKwh) {
if(!energyEst) return '';
const area = energyEst.livingAreaM2;
const consumption = energyEst.withElectricHeat;
const consumptionHP = energyEst.withHeatPump;
let html = '<div style="margin-top:16px;padding:14px;background:linear-gradient(135deg,#fef3c7,#fde68a);border-radius:10px;border:1px solid #f59e0b33">'
+'<div style="font-size:11px;font-weight:700;color:#92400e;text-transform:uppercase;margin-bottom:8px">Energianalys ('+area+' m²)</div>';
// Förbrukning
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px">'
+'<span style="color:#78350f">Förbrukning (elvärme):</span>'
+'<span style="font-weight:700;color:#92400e">'+consumption.toLocaleString('sv-SE')+' kWh/år</span></div>';
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px">'
+'<span style="color:#78350f">Förbrukning (värmepump):</span>'
+'<span style="font-weight:600;color:#a16207">'+consumptionHP.toLocaleString('sv-SE')+' kWh/år</span></div>';
if(solarKwh && solarKwh > 0) {
// Beräkna besparingar
const coverageElHeat = Math.min(100, Math.round((solarKwh / consumption) * 100));
const coverageHP = Math.min(100, Math.round((solarKwh / consumptionHP) * 100));
// Ekonomisk besparing: egenanvändning sparar elpris, överskott säljs
const selfUseKwh = Math.min(solarKwh * SOLAR_SELF_USE, consumption);
const sellKwh = solarKwh - selfUseKwh;
const yearlySaving = Math.round(selfUseKwh * ELECTRICITY_PRICE + sellKwh * SOLAR_SELL_PRICE);
html += '<div style="border-top:1px solid #f59e0b44;margin:8px 0"></div>';
// Solproduktion vs förbrukning
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:12px">'
+'<span style="color:#166534;font-weight:600">Solproduktion:</span>'
+'<span style="font-weight:700;color:#166534">'+solarKwh.toLocaleString('sv-SE')+' kWh/år</span></div>';
// Coverage bar
html += '<div style="margin:6px 0">'
+'<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px">'
+'<span style="color:#78350f">Täcker (elvärme)</span>'
+'<span style="font-weight:700;color:'+(coverageElHeat >= 70 ? '#059669' : coverageElHeat >= 40 ? '#ca8a04' : '#dc2626')+'">'+coverageElHeat+'%</span></div>'
+'<div style="background:#fff8e1;border-radius:6px;height:8px;overflow:hidden">'
+'<div style="height:100%;border-radius:6px;background:'+(coverageElHeat >= 70 ? 'linear-gradient(90deg,#10b981,#059669)' : coverageElHeat >= 40 ? 'linear-gradient(90deg,#eab308,#ca8a04)' : 'linear-gradient(90deg,#f87171,#dc2626)')+';width:'+coverageElHeat+'%"></div>'
+'</div></div>';
html += '<div style="margin:6px 0">'
+'<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px">'
+'<span style="color:#78350f">Täcker (värmepump)</span>'
+'<span style="font-weight:700;color:'+(coverageHP >= 70 ? '#059669' : coverageHP >= 40 ? '#ca8a04' : '#dc2626')+'">'+coverageHP+'%</span></div>'
+'<div style="background:#fff8e1;border-radius:6px;height:8px;overflow:hidden">'
+'<div style="height:100%;border-radius:6px;background:'+(coverageHP >= 70 ? 'linear-gradient(90deg,#10b981,#059669)' : coverageHP >= 40 ? 'linear-gradient(90deg,#eab308,#ca8a04)' : 'linear-gradient(90deg,#f87171,#dc2626)')+';width:'+Math.min(100,coverageHP)+'%"></div>'
+'</div></div>';
// Besparing i SEK
html += '<div style="border-top:1px solid #f59e0b44;margin:8px 0"></div>'
+'<div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px">'
+'<span style="color:#166534;font-weight:700">Besparing/år:</span>'
+'<span style="font-weight:800;color:#059669;font-size:15px">'+yearlySaving.toLocaleString('sv-SE')+' kr</span></div>'
+'<div style="font-size:10px;color:#92400e;opacity:0.7">Baserat på '+ELECTRICITY_PRICE.toFixed(2)+' kr/kWh, 70% egenanvändning</div>';
}
html += '</div>';
return html;
}
function solarScoreLabel(score) {
if(!score) return '';
const labels = {1:'Låg',2:'OK',3:'Bra',4:'Mycket bra',5:'Utmärkt'};
const colors = {1:'#ef4444',2:'#eab308',3:'#22c55e',4:'#10b981',5:'#059669'};
const suns = '☀️'.repeat(Math.min(score, 5));
return '<span style="color:'+colors[score]+';font-weight:600" title="Solpotential: '+labels[score]+'">'+suns+' '+labels[score]+'</span>';
}
let solarFetchQueue = [];
let solarFetching = false;
async function fetchSolarScoreForProspect(idx, lat, lng) {
solarFetchQueue.push({idx, lat, lng});
if(solarFetching) return;
solarFetching = true;
while(solarFetchQueue.length > 0) {
const job = solarFetchQueue.shift();
try {
const res = await fetch('https://solar.googleapis.com/v1/buildingInsights:findClosest?location.latitude='+job.lat+'&location.longitude='+job.lng+'&requiredQuality=HIGH&key='+GOOGLE_MAPS_KEY);
if(res.ok) {
const data = await res.json();
const si = data.solarPotential || {};
if(faltProspects[job.idx]) {
faltProspects[job.idx].maxPanels = si.maxArrayPanelsCount || null;
const configs = si.solarPanelConfigs || [];
faltProspects[job.idx].yearlyKwh = configs.length ? Math.round(configs[configs.length-1].yearlyEnergyDcKwh||0) : null;
faltProspects[job.idx].roofArea = si.wholeRoofStats?.areaMeters2 ? Math.round(si.wholeRoofStats.areaMeters2) : null;
faltProspects[job.idx].solarScore = calcSolarScore(si);
faltProspects[job.idx].solarSource = 'google';
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
}
} else if(res.status === 404 && faltProspects[job.idx]) {
// Google Solar has no data — try PVGIS/estimation fallback
try {
const fbRes = await fetch('/api/pvgis-proxy.php?lat='+job.lat+'&lon='+job.lng);
if(fbRes.ok) {
const fbData = await fbRes.json();
if(fbData.success) {
faltProspects[job.idx].maxPanels = fbData.system?.panels || null;
faltProspects[job.idx].yearlyKwh = fbData.yearly_kwh || null;
faltProspects[job.idx].solarScore = fbData.solar_score || null;
faltProspects[job.idx].solarSource = fbData.source || 'estimate';
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
}
}
} catch(fe) {}
}
} catch(e) {}
// Rate limit: wait 200ms between requests
if(solarFetchQueue.length > 0) await new Promise(r => setTimeout(r, 200));
}
solarFetching = false;
}
function renderFaltMarkers(){
faltGMarkers.forEach(m=>m.setMap(null));
faltGMarkers=[];
if(!gMap) return;
faltProspects.forEach((p,i)=>{
const color=faltStatusColors[p.status];
const pinLabel = String(i+1);
const pinFontSize = pinLabel.length > 2 ? 11 : 14;
// Solar score — gul sol
const score = p.solarScore;
const sunYellow = 'rgb(251,191,36)';
const sunAmber = 'rgb(245,158,11)';
const sunBrown = 'rgb(113,63,18)';
const scoreBadge = score ? '<g transform="translate(30,8)">'
+'<line x1="0" y1="-10" x2="0" y2="-7" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="0" y1="7" x2="0" y2="10" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="-10" y1="0" x2="-7" y2="0" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="7" y1="0" x2="10" y2="0" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="-7" y1="-7" x2="-5" y2="-5" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="5" y1="-5" x2="7" y2="-7" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="-7" y1="7" x2="-5" y2="5" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<line x1="5" y1="5" x2="7" y2="7" stroke="'+sunAmber+'" stroke-width="1.5" stroke-linecap="round"/>'
+'<circle r="6" fill="'+sunYellow+'" stroke="white" stroke-width="1.5"/>'
+'<text y="4" text-anchor="middle" fill="'+sunBrown+'" font-family="sans-serif" font-size="9" font-weight="800">'+score+'</text>'
+'</g>' : '';
const kwhLabel = p.yearlyKwh ? '<text x="18" y="36" text-anchor="middle" fill="'+color+'" font-family="sans-serif" font-size="7" font-weight="700">'+(p.yearlyKwh>=1000?(p.yearlyKwh/1000).toFixed(0)+'k':p.yearlyKwh)+'kWh</text>' : '';
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="'+(score?46:36)+'" height="'+(p.yearlyKwh?48:44)+'" viewBox="0 0 '+(score?46:36)+' '+(p.yearlyKwh?48:44)+'">'
+'<path d="M18 0C8 0 0 8 0 18c0 13 18 26 18 26s18-13 18-26C36 8 28 0 18 0z" fill="'+color+'" stroke="white" stroke-width="2"/>'
+'<text x="18" y="22" text-anchor="middle" fill="white" font-family="Inter,sans-serif" font-size="'+pinFontSize+'" font-weight="700">'+pinLabel+'</text>'
+scoreBadge+kwhLabel
+'</svg>';
const marker = new google.maps.Marker({
position:{lat:p.lat,lng:p.lng},
map:gMap,
icon:{
url:'data:image/svg+xml;charset=UTF-8,'+encodeURIComponent(svg),
scaledSize:new google.maps.Size(score?46:36, p.yearlyKwh?48:44),
anchor:new google.maps.Point(18, p.yearlyKwh?48:44),
},
title:pinLabel+' '+p.addr,
});
marker.addListener('click', function(){
faltActiveIdx = i;
renderFaltList();
setTimeout(() => {
const activeEl = document.querySelector('.falt-item-active');
if(activeEl) activeEl.scrollIntoView({block:'nearest',behavior:'smooth'});
}, 50);
let solarInfo = '';
if(p.maxPanels || p.solarScore) {
solarInfo = '<div style="display:flex;gap:6px;margin:6px 0;font-size:10px;flex-wrap:wrap;align-items:center">';
if(p.solarScore) solarInfo += '<span style="background:#f0fdf4;padding:3px 8px;border-radius:6px;font-size:11px">' + solarScoreLabel(p.solarScore) + '</span>';
if(p.maxPanels) solarInfo += '<span style="background:#ecfdf5;padding:2px 6px;border-radius:4px;color:#166534">'+p.maxPanels+' paneler</span>';
if(p.yearlyKwh) solarInfo += '<span style="background:#eff6ff;padding:2px 6px;border-radius:4px;color:#1e40af">'+p.yearlyKwh.toLocaleString('sv-SE')+' kWh/år</span>';
if(p.roofArea) solarInfo += '<span style="background:#faf5ff;padding:2px 6px;border-radius:4px;color:#6d28d9">'+p.roofArea+' m²</span>';
solarInfo += '</div>';
}
let leadInfo = '';
if(p.kundNamn) leadInfo = '<div style="margin:6px 0;padding:8px;background:#f0fdf4;border-radius:6px;font-size:12px">'
+'<strong>'+p.kundNamn+'</strong>'+(p.kundTel?' • '+p.kundTel:'')
+(p.product?' • <span style="color:#10b981">'+p.product+'</span>':'')
+'</div>';
const isUnvisited = p.status === 'unvisited';
const html='<div style="font-family:Inter,sans-serif;min-width:260px">'
+'<div style="display:flex;justify-content:space-between;align-items:start">'
+'<div><strong style="font-size:13px">'+p.addr+'</strong><br><span style="font-size:12px;color:#64748b">'+p.city+'</span></div>'
+'<span style="padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:'+color+'20;color:'+color+';white-space:nowrap">'+faltStatusLabels[p.status]+'</span>'
+'</div>'
+solarInfo+leadInfo
+'<div id="popupHitta_'+i+'" style="font-size:12px;color:#94a3b8;margin:4px 0">Söker boende...</div>'
+(p.note?'<div style="font-size:12px;color:#64748b;font-style:italic;margin:4px 0">'+p.note+'</div>':'')
+'<div style="display:flex;gap:6px;margin-top:8px">'
+(isUnvisited
? '<button onclick="openCheckin('+i+')" style="flex:1;padding:10px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Checka in</button>'
: '<button onclick="openCheckin('+i+')" style="flex:1;padding:8px;background:#f1f5f9;color:#334155;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit">Uppdatera</button>')
+((p.quoteIds && p.quoteIds.length) || p.quoteId ? '<button onclick="showProspectQuotes('+i+')" style="flex:1;padding:10px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Kalkyler ('+ ((p.quoteIds||[]).length || (p.quoteId?1:0)) +')</button>' : '')
+'</div></div>';
gInfoWindow.setContent(html);
gInfoWindow.open(gMap, marker);
// Fetch resident name
lookupHittaForPopup(p.addr + (p.city ? ', '+p.city : ''), i);
});
faltGMarkers.push(marker);
});
}
function setFaltStatus(id,status){
faltProspects[id].status=status;
if(!faltProspects[id].checkinTime) faltProspects[id].checkinTime = new Date().toISOString();
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
gInfoWindow.close();
}
function saveProspectNote(id, note){
faltProspects[id].note = note;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltList();
}
// === CHECK-IN MODAL ===
function openCheckin(idx){
checkinIdx = idx;
checkinStatus = null;
const p = faltProspects[idx];
document.getElementById('checkinTitle').textContent = 'Incheckning — Hus #'+(idx+1);
document.getElementById('checkinAddr').innerHTML = p.addr + (p.city?', '+p.city:'') + '<span id="ciHittaStatus" style="margin-left:8px;font-size:11px;color:#94a3b8"></span>';
document.getElementById('checkinStep0').style.display = 'block';
document.getElementById('checkinStep1').style.display = 'block';
document.getElementById('checkinStep2').style.display = 'none';
document.getElementById('checkinStep2b').style.display = 'none';
if(document.getElementById('checkinStep2c')) document.getElementById('checkinStep2c').style.display = 'none';
// Pre-fill if revisiting
document.getElementById('ciName').value = p.kundNamn || '';
document.getElementById('ciPhone').value = p.kundTel || '';
document.getElementById('ciEmail').value = p.kundEmail || '';
document.getElementById('ciProduct').value = p.product || '';
document.getElementById('ciNote').value = p.note || '';
document.getElementById('ciNoteSimple').value = p.note || '';
document.getElementById('checkinModal').style.display = 'flex';
gInfoWindow.close();
// Auto-lookup name from Hitta.se
if(!p.kundNamn){
lookupHitta(p.addr + (p.city ? ', '+p.city : ''), idx);
}
}
async function lookupHittaForPopup(address, idx){
try {
let data = getHittaCache(address);
if(!data) {
const res = await fetch('/api/hitta-lookup.php?address='+encodeURIComponent(address));
data = await res.json();
setHittaCache(address, data);
}
const el = document.getElementById('popupHitta_'+idx);
if(!el) return;
if(data.persons && data.persons.length > 0){
let livingAreaStr = null;
// Hämta fastighetsinfo från första personen (gäller hela fastigheten)
const propDetails = [];
const fp = data.persons[0];
if(fp.livingArea) { propDetails.push(fp.livingArea); livingAreaStr = fp.livingArea; }
if(fp.household) propDetails.push(fp.household);
el.innerHTML = '<div style="padding:6px 8px;background:#f0fdf4;border-radius:6px;border:1px solid #dcfce7">'
+(propDetails.length ? '<div style="font-size:11px;color:#64748b;margin-bottom:4px;padding-bottom:4px;border-bottom:1px solid #dcfce7">'+propDetails.join(' · ')+'</div>' : '')
+'<div style="font-size:10px;font-weight:600;color:#10b981;margin-bottom:2px">BOENDE</div>'
+ data.persons.map(n => {
let info = '<div style="font-size:13px;font-weight:600;color:#166534">'+n.name+'</div>';
let details = [];
if(n.age) details.push(n.age + ' år');
if(details.length) info += '<div style="font-size:11px;color:#64748b;margin-top:1px">'+details.join(' · ')+'</div>';
if(n.phone && n.phone.length) info += '<div style="font-size:11px;color:#0284c7;margin-top:1px">'+n.phone.join(', ')+'</div>';
return info;
}).join('<div style="border-top:1px solid #dcfce7;margin:4px 0"></div>')
+'</div>';
// Visa kort energiuppskattning i popup om vi har boyta + soldata
if(livingAreaStr && faltProspects[idx]) {
const p = faltProspects[idx];
const energyEst = estimateEnergyConsumption(livingAreaStr);
if(energyEst && p.yearlyKwh) {
const coverage = Math.min(100, Math.round((p.yearlyKwh / energyEst.withElectricHeat) * 100));
const selfUse = Math.min(p.yearlyKwh * SOLAR_SELF_USE, energyEst.withElectricHeat);
const sell = p.yearlyKwh - selfUse;
const saving = Math.round(selfUse * ELECTRICITY_PRICE + sell * SOLAR_SELL_PRICE);
el.innerHTML += '<div style="margin-top:4px;padding:5px 8px;background:#fef9c3;border-radius:6px;border:1px solid #fde68a;font-size:11px">'
+'<span style="color:#92400e;font-weight:600">Energi:</span> '
+'<span style="color:#78350f">'+energyEst.withElectricHeat.toLocaleString('sv-SE')+' kWh/år</span>'
+' · <span style="color:#166534;font-weight:600">Sol täcker '+coverage+'%</span>'
+' · <span style="color:#059669;font-weight:700">Spar '+saving.toLocaleString('sv-SE')+' kr/år</span>'
+'</div>';
} else if(energyEst) {
el.innerHTML += '<div style="margin-top:4px;padding:5px 8px;background:#fef9c3;border-radius:6px;border:1px solid #fde68a;font-size:11px">'
+'<span style="color:#92400e;font-weight:600">Uppsk. förbrukning:</span> '
+'<span style="color:#78350f">'+energyEst.withElectricHeat.toLocaleString('sv-SE')+' kWh/år (elvärme)</span>'
+'</div>';
}
}
} else {
el.textContent = 'Inga boende hittade';
}
} catch(e){
const el = document.getElementById('popupHitta_'+idx);
if(el) el.textContent = '';
}
}
async function lookupHittaForPanel(address){
try {
let data = getHittaCache(address);
if(!data) {
const res = await fetch('/api/hitta-lookup.php?address='+encodeURIComponent(address));
data = await res.json();
setHittaCache(address, data);
}
const el = document.getElementById('hittaResult');
if(!el) return;
let livingAreaStr = null;
if(data.persons && data.persons.length > 0){
// Fastighetsinfo (boyta, hushållstyp) — gäller hela fastigheten
const fp0 = data.persons[0];
const propInfo = [];
if(fp0.livingArea) { propInfo.push(fp0.livingArea); livingAreaStr = fp0.livingArea; }
if(fp0.household) propInfo.push(fp0.household);
el.innerHTML = (propInfo.length ? '<div style="font-size:11px;color:#64748b;margin-bottom:6px;padding:4px 8px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb">'+propInfo.join(' · ')+'</div>' : '')
+'<div style="font-size:12px;font-weight:600;color:#1a1a1a;margin-bottom:4px">Boende:</div>'
+ data.persons.map(p => {
let html = '<div style="font-size:13px;color:#334155;padding:2px 0;font-weight:600">'+p.name+'</div>';
let details = [];
if(p.age) details.push(p.age + ' år');
if(details.length) html += '<div style="font-size:11px;color:#64748b;padding:0 0 4px">'+details.join(' · ')+'</div>';
if(p.phone && p.phone.length) html += '<div style="font-size:11px;color:#0284c7;padding:0 0 4px">'+p.phone.join(', ')+'</div>';
return html;
}).join('');
} else {
el.textContent = 'Inga boende hittade på Hitta.se';
}
// Visa energianalys om vi har boyta
const energyEl = document.getElementById('energyEstimate');
if(energyEl && livingAreaStr) {
const energyEst = estimateEnergyConsumption(livingAreaStr);
if(energyEst) {
energyEl.innerHTML = buildEnergyEstimateHtml(energyEst, window._lastSolarKwh || 0);
}
}
} catch(e){
const el = document.getElementById('hittaResult');
if(el) el.textContent = '';
}
}
async function lookupHitta(address, idx){
const status = document.getElementById('ciHittaStatus');
if(status) status.textContent = 'Söker på Hitta.se...';
try {
let data = getHittaCache(address);
if(!data) {
const res = await fetch('/api/hitta-lookup.php?address='+encodeURIComponent(address));
data = await res.json();
setHittaCache(address, data);
}
if(data.persons && data.persons.length > 0){
const names = data.persons.map(p=>p.name);
if(status) status.innerHTML = '<span style="color:#10b981">Hittade '+names.length+' person'+(names.length>1?'er':'')+'</span>';
// Show name picker
const nameField = document.getElementById('ciName');
if(!nameField.value){
if(names.length === 1){
nameField.value = names[0];
faltProspects[idx].hittaNames = names;
} else {
// Multiple - show picker
nameField.value = names[0];
faltProspects[idx].hittaNames = names;
}
}
// Show property info + name buttons
if(status && data.persons.length > 0){
const fp0 = data.persons[0];
const propInfo = [];
if(fp0.livingArea) propInfo.push(fp0.livingArea);
if(fp0.household) propInfo.push(fp0.household);
status.innerHTML = (propInfo.length ? '<div style="font-size:11px;color:#64748b;margin-top:4px;padding:4px 8px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb">'+propInfo.join(' · ')+'</div>' : '')
+'<div style="margin-top:6px;display:flex;flex-direction:column;gap:4px">'
+ data.persons.map(p => {
const ageStr = p.age ? '<span style="font-size:10px;color:#64748b;margin-left:6px">'+p.age+' år</span>' : '';
return '<button onclick="document.getElementById(\'ciName\').value=\''+p.name.replace(/'/g,"\\'")+'\';this.style.background=\'#dcfce7\'" style="padding:5px 10px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;background:#fff;font-family:inherit;color:#1a1a1a;text-align:left">'+p.name+ageStr+'</button>';
}).join('')
+ '</div>';
}
} else {
if(status) status.textContent = 'Inga namn hittade';
}
} catch(e){
if(status) status.textContent = '';
}
}
function closeCheckin(){
document.getElementById('checkinModal').style.display = 'none';
}
function checkinResult(status){
checkinStatus = status;
document.getElementById('checkinStep0').style.display = 'none';
document.getElementById('checkinStep1').style.display = 'none';
document.getElementById('checkinStep2').style.display = 'none';
document.getElementById('checkinStep2b').style.display = 'none';
document.getElementById('checkinStep2c').style.display = 'none';
if(status === 'interested'){
document.getElementById('checkinStep2').style.display = 'block';
document.getElementById('ciName').focus();
} else if(status === 'callback'){
document.getElementById('checkinStep2c').style.display = 'block';
// Set default date to tomorrow
const tomorrow = new Date(Date.now() + 86400000);
document.getElementById('ciCallbackDate').value = tomorrow.toISOString().slice(0, 10);
} else {
document.getElementById('checkinStep2b').style.display = 'block';
}
}
async function checkinDirectToKalkyl(){
const p = faltProspects[checkinIdx];
if((p.quoteIds && p.quoteIds.length) || p.quoteId){ closeCheckin(); showProspectQuotes(checkinIdx); return; }
p.status = 'interested';
p.product = 'Solceller';
p.checkinTime = new Date().toISOString();
// Fetch Hitta data if not already cached
const address = p.addr + (p.city ? ', '+p.city : '');
try {
let data = getHittaCache(address);
if(!data){
const res = await fetch('/api/hitta-lookup.php?address='+encodeURIComponent(address));
data = await res.json();
setHittaCache(address, data);
}
if(data.persons && data.persons.length > 0){
p.hittaNames = data.persons.map(pp=>pp.name);
p.hittaPersons = data.persons;
if(!p.kundNamn) p.kundNamn = data.persons[0].name || '';
}
} catch(e){}
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
closeCheckin();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
// Gå direkt till konfiguratorn (utan kundinformation-modal)
pendingKalkylCustomer = {
prospectIdx: checkinIdx,
name: p.kundNamn || '',
email: p.kundEmail || '',
phone: p.kundTel || '',
address: address,
ownerType: '1',
maxDeduction: 50000,
owner1: { name: p.kundNamn || '', pnr: '' },
owner2: null,
product: 'Solceller',
solarData: {
yearlyKwh: p.yearlyKwh || null,
maxPanels: p.maxPanels || null,
roofArea: p.roofArea || null,
solarScore: p.solarScore || null
},
created: new Date().toISOString()
};
kalkylOrigin = 'faltsalj';
// Navigera direkt till konfiguratorvyn
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
document.querySelectorAll('.page-content').forEach(p=>{p.classList.remove('active');p.style.display='';});
const kalkylPage = document.getElementById('page-konfigurator');
if(kalkylPage) kalkylPage.classList.add('active');
const navItem = document.querySelector('[data-page="konfigurator"]');
if(navItem) navItem.classList.add('active');
showKalkylConfig();
populateKalkylFromCustomer();
}
function saveCheckinLead(){
const p = faltProspects[checkinIdx];
p.status = 'interested';
p.kundNamn = document.getElementById('ciName').value.trim();
p.kundTel = document.getElementById('ciPhone').value.trim();
p.kundEmail = document.getElementById('ciEmail').value.trim();
p.product = document.getElementById('ciProduct').value;
p.note = document.getElementById('ciNote').value.trim();
p.checkinTime = new Date().toISOString();
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
closeCheckin();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
// Auto-advance: go to next unvisited
goToNextUnvisited(checkinIdx);
}
function saveCheckinSimple(){
const p = faltProspects[checkinIdx];
p.status = checkinStatus;
p.note = document.getElementById('ciNoteSimple').value.trim();
p.checkinTime = new Date().toISOString();
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
closeCheckin();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
goToNextUnvisited(checkinIdx);
}
async function saveCheckinCallback(){
const p = faltProspects[checkinIdx];
const date = document.getElementById('ciCallbackDate').value;
const time = document.getElementById('ciCallbackTime').value || '10:00';
const note = document.getElementById('ciCallbackNote').value.trim();
if(!date){ alert('Välj ett datum för återbesöket'); return; }
p.status = 'callback';
p.note = note;
p.callbackDate = date;
p.callbackTime = time;
p.checkinTime = new Date().toISOString();
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
// Create Google Calendar event if logged in
if(gAccessToken){
const startTime = date + 'T' + time + ':00';
const endHour = parseInt(time.split(':')[0]) + 1;
const endTime = date + 'T' + String(endHour).padStart(2,'0') + ':' + time.split(':')[1] + ':00';
const addr = p.addr + (p.city ? ', ' + p.city : '');
await createCalendarEvent(
'Återbesök: ' + addr,
(p.kundNamn ? 'Kund: ' + p.kundNamn + '\n' : '') + (note ? 'Notering: ' + note : ''),
addr,
startTime, endTime
);
} else {
// Fallback: open Google Calendar URL
const startDt = date.replace(/-/g,'') + 'T' + time.replace(':','') + '00';
const endH = parseInt(time.split(':')[0]) + 1;
const endDt = date.replace(/-/g,'') + 'T' + String(endH).padStart(2,'0') + time.slice(2).replace(':','') + '00';
const addr = p.addr + (p.city ? ', ' + p.city : '');
const gcUrl = 'https://calendar.google.com/calendar/event?action=TEMPLATE'
+ '&text=' + encodeURIComponent('Återbesök: ' + addr)
+ '&dates=' + startDt + '/' + endDt
+ '&location=' + encodeURIComponent(addr)
+ '&details=' + encodeURIComponent(note || 'Planerat återbesök');
window.open(gcUrl, '_blank');
}
closeCheckin();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
goToNextUnvisited(checkinIdx);
}
function goToNextUnvisited(afterIdx){
// Find next unvisited prospect after current
for(let j = afterIdx+1; j < faltProspects.length; j++){
if(faltProspects[j].status === 'unvisited'){
focusProspect(j);
return;
}
}
// Wrap around
for(let j = 0; j < afterIdx; j++){
if(faltProspects[j].status === 'unvisited'){
focusProspect(j);
return;
}
}
}
function updateFaltStats(){
const total=faltProspects.length;
const visited=faltProspects.filter(p=>p.status!=='unvisited').length;
const interested=faltProspects.filter(p=>p.status==='interested'||p.status==='kalkyl'||p.status==='offert').length;
const notInt=faltProspects.filter(p=>p.status==='not_interested').length;
const notHome=faltProspects.filter(p=>p.status==='not_home').length;
document.getElementById('faltTotal').textContent=total;
document.getElementById('faltVisited').textContent=visited;
document.getElementById('faltVisitedPct').textContent=total?Math.round(visited/total*100)+'%':'0%';
document.getElementById('faltInterested').textContent=interested;
document.getElementById('faltNotInt').textContent=notInt;
document.getElementById('faltNotHome').textContent=notHome;
}
function renderFaltList(){
const list=document.getElementById('faltList');
if(!list) return;
// Update filter button styles
['All','Plan','Lead'].forEach(f=>{
const btn=document.getElementById('flt'+f);
if(!btn) return;
const active = (f==='All'&&faltListFilter==='all')||(f==='Plan'&&faltListFilter==='unvisited')||(f==='Lead'&&faltListFilter==='interested');
btn.style.background=active?'#024550':'#fff';
btn.style.color=active?'#fff':'#64748b';
});
if(faltProspects.length===0){
list.innerHTML='<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Klicka på hus på kartan och tryck "+ Lägg till" för att bygga din rutt.</div>';
document.getElementById('faltListCount').textContent='(0)';
return;
}
let filtered = faltProspects;
if(faltListFilter==='unvisited') filtered = faltProspects.filter(p=>p.status==='unvisited');
else if(faltListFilter==='interested') filtered = faltProspects.filter(p=>p.status==='interested'||p.status==='kalkyl'||p.status==='offert');
list.innerHTML=filtered.map((p,fi)=>{
const color=faltStatusColors[p.status];
const idx=faltProspects.findIndex(x=>x.id===p.id);
const routeNum = fi+1;
const isActive = idx === faltActiveIdx;
const photoCount = (p.photos||[]).length;
let extra = '';
if(p.kundNamn) extra = '<div style="font-size:11px;color:#10b981;font-weight:600;margin-top:1px">'+p.kundNamn+(p.product?' • '+p.product:'')+'</div>';
if(p.solarScore) extra += '<div style="font-size:10px;margin-top:1px">'+solarScoreLabel(p.solarScore)+' <span style="color:#64748b">'+(p.yearlyKwh?p.yearlyKwh.toLocaleString('sv-SE')+' kWh/år':'')+'</span></div>';
else if(p.maxPanels) extra += '<div style="font-size:10px;color:#64748b;margin-top:1px">'+p.maxPanels+' paneler • '+(p.yearlyKwh?p.yearlyKwh.toLocaleString('sv-SE'):'-')+' kWh</div>';
if(p.callbackDate) extra += '<div style="font-size:10px;color:#3b82f6;margin-top:1px">Återbesök: '+p.callbackDate+' '+(p.callbackTime||'')+'</div>';
// Photo thumbnails
let thumbsHtml = '';
if(photoCount > 0){
thumbsHtml = '<div style="display:flex;gap:3px;margin-top:3px;overflow:hidden">'
+ (p.photos||[]).slice(0,3).map((ph,pi) =>
'<img src="'+ph.thumb+'" onclick="event.stopPropagation();viewProspectPhotos('+idx+')" style="width:28px;height:28px;border-radius:4px;object-fit:cover;cursor:pointer;border:1px solid #e5e7eb">'
).join('')
+ (photoCount>3?'<div style="width:28px;height:28px;border-radius:4px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:9px;color:#64748b;font-weight:600;cursor:pointer" onclick="event.stopPropagation();viewProspectPhotos('+idx+')">+'+( photoCount-3)+'</div>':'')
+ '</div>';
}
return '<div class="falt-item'+(isActive?' falt-item-active':'')+'" data-idx="'+idx+'" draggable="true" ondragstart="faltDragStart(event,'+idx+')" ondragover="faltDragOver(event)" ondrop="faltDrop(event,'+idx+')" ondragend="faltDragEnd(event)" style="gap:6px;flex-wrap:wrap;cursor:grab'+(isActive?';background:#f0f9ff;border-left:3px solid #024550':'')+'" onclick="focusProspect('+idx+')">'
+'<div style="display:flex;flex-direction:column;align-items:center;gap:2px;flex-shrink:0;cursor:grab" title="Dra för att ändra ordning">'
+'<svg viewBox="0 0 24 24" style="width:12px;height:12px;fill:#cbd5e1;stroke:none"><circle cx="8" cy="4" r="2"/><circle cx="16" cy="4" r="2"/><circle cx="8" cy="12" r="2"/><circle cx="16" cy="12" r="2"/><circle cx="8" cy="20" r="2"/><circle cx="16" cy="20" r="2"/></svg>'
+'<div style="width:22px;height:22px;border-radius:50%;background:'+color+';color:#fff;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center">'+routeNum+'</div>'
+'</div>'
+'<div class="falt-item-info" style="flex:1;min-width:0">'
+'<div class="falt-item-addr" style="font-size:12px">'+p.addr+'</div>'
+extra
+(p.note?'<div style="font-size:10px;color:#94a3b8;font-style:italic;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+p.note+'</div>':'')
+thumbsHtml
+'</div>'
+'<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">'
+'<div style="display:flex;gap:3px">'
+'<button onclick="event.stopPropagation();captureProspectPhoto('+idx+')" style="background:#f0f9ff;border:1px solid #bfdbfe;border-radius:6px;cursor:pointer;padding:3px 6px;display:flex;align-items:center;gap:3px;font-size:10px;font-weight:600;color:#1e40af;font-family:inherit" title="Ta bild / ladda upp"><svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>Bild</button>'
+'<button onclick="event.stopPropagation();openStreetView('+idx+')" style="background:#f5f3ff;border:1px solid #ddd6fe;border-radius:6px;cursor:pointer;padding:3px 6px;display:flex;align-items:center;gap:3px;font-size:10px;font-weight:600;color:#6d28d9;font-family:inherit" title="Street View"><svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:2"><circle cx="12" cy="5" r="3"/><path d="M12 8v4"/><path d="M8 20l4-8 4 8"/></svg>SV</button>'
+(photoCount?'<button onclick="event.stopPropagation();viewProspectPhotos('+idx+')" style="background:#ecfdf5;border:1px solid #bbf7d0;border-radius:6px;cursor:pointer;padding:3px 6px;display:flex;align-items:center;gap:2px;font-size:10px;font-weight:600;color:#166534;font-family:inherit"><svg viewBox="0 0 24 24" style="width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>'+photoCount+'</button>':'')
+'</div>'
+'<div style="display:flex;align-items:center;gap:4px">'
+'<div style="padding:1px 6px;border-radius:5px;font-size:9px;font-weight:600;background:'+color+'15;color:'+color+';white-space:nowrap">'+faltStatusLabels[p.status]+'</div>'
+'<button onclick="event.stopPropagation();removeProspect('+idx+')" style="background:none;border:none;cursor:pointer;color:#cbd5e1;font-size:13px;padding:0;line-height:1" title="Ta bort">×</button>'
+'</div>'
+'</div></div>';
}).join('');
document.getElementById('faltListCount').textContent='('+faltProspects.length+')';
}
// Drag & drop för ruttordning
let faltDragIdx = null;
function faltDragStart(e, idx){
faltDragIdx = idx;
e.dataTransfer.effectAllowed = 'move';
e.target.closest('.falt-item').style.opacity = '0.4';
}
function faltDragOver(e){
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const item = e.target.closest('.falt-item');
if(item) item.style.borderTop = '2px solid #024550';
}
function faltDrop(e, targetIdx){
e.preventDefault();
const item = e.target.closest('.falt-item');
if(item) item.style.borderTop = '';
if(faltDragIdx === null || faltDragIdx === targetIdx) return;
const moved = faltProspects.splice(faltDragIdx, 1)[0];
faltProspects.splice(targetIdx > faltDragIdx ? targetIdx - 1 : targetIdx, 0, moved);
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
faltDragIdx = null;
renderFaltList();
renderFaltMarkers();
renderLeadsTable();
}
function faltDragEnd(e){
faltDragIdx = null;
document.querySelectorAll('.falt-item').forEach(el => {
el.style.opacity = '';
el.style.borderTop = '';
});
}
function focusProspect(idx){
const p = faltProspects[idx];
if(!p || !gMap) return;
faltActiveIdx = idx;
gMap.setCenter({lat:p.lat, lng:p.lng});
gMap.setZoom(19);
if(faltGMarkers[idx]) google.maps.event.trigger(faltGMarkers[idx], 'click');
renderFaltList();
// Scrolla till aktivt prospekt i listan
setTimeout(() => {
const activeEl = document.querySelector('.falt-item-active');
if(activeEl) activeEl.scrollIntoView({block:'nearest',behavior:'smooth'});
}, 50);
}
function removeProspect(idx){
faltProspects.splice(idx, 1);
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
}
// === PROSPECT PHOTOS ===
function captureProspectPhoto(idx){
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.capture = 'environment'; // Bakre kameran på mobil
input.multiple = true;
input.onchange = async (e) => {
const files = Array.from(e.target.files);
if(!files.length) return;
const p = faltProspects[idx];
if(!p.photos) p.photos = [];
for(const file of files){
try {
const dataUrl = await readFileAsDataUrl(file);
const thumb = await resizeImage(dataUrl, 200);
// Ladda upp original till servern
let fullUrl = dataUrl;
try {
const upResp = await fetch('/api/upload-photo.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ image: dataUrl, name: file.name })
});
const upData = await upResp.json();
if(upData.success && upData.url) fullUrl = upData.url;
} catch(ue){ console.warn('Upload failed, keeping base64:', ue); }
p.photos.push({
full: fullUrl,
thumb: thumb,
name: file.name,
date: new Date().toISOString(),
type: 'camera'
});
} catch(err){ console.error('Photo error:', err); }
}
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltList();
};
input.click();
}
function readFileAsDataUrl(file){
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function resizeImage(dataUrl, maxSize){
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const scale = Math.min(maxSize/img.width, maxSize/img.height, 1);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/jpeg', 0.7));
};
img.src = dataUrl;
});
}
let svPanorama = null;
let svProspectIdx = null;
let svPhotoCount = 0;
let svRouteMode = false;
function startStreetViewRoute(){
if(faltProspects.length === 0){ alert('Lägg till prospekt först'); return; }
svRouteMode = true;
openStreetView(0);
}
function openStreetView(idx){
const p = faltProspects[idx];
if(!p.photos) p.photos = [];
svProspectIdx = idx;
svPhotoCount = 0;
// Skapa modal med inbäddad Street View
let modal = document.getElementById('svModal');
if(modal) modal.remove();
modal = document.createElement('div');
modal.id = 'svModal';
modal.style.cssText = 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.9);display:flex;flex-direction:column';
// Rutt-navigation
const total = faltProspects.length;
const num = idx + 1;
let routeNav = '';
if(svRouteMode || total > 1){
routeNav = '<div style="display:flex;align-items:center;gap:6px">'
+'<button id="svPrevProspect" onclick="svGoProspect(-1)" style="width:32px;height:32px;background:rgba(255,255,255,.1);border:none;color:#fff;border-radius:6px;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center'+(idx===0?';opacity:.3;pointer-events:none':'')+'">‹</button>'
+'<span style="color:#fff;font-size:13px;font-weight:700;min-width:40px;text-align:center">'+num+'/'+total+'</span>'
+'<button id="svNextProspect" onclick="svGoProspect(1)" style="width:32px;height:32px;background:rgba(255,255,255,.1);border:none;color:#fff;border-radius:6px;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center'+(idx>=total-1?';opacity:.3;pointer-events:none':'')+'">›</button>'
+'</div>';
}
modal.innerHTML = '<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:#1e293b;flex-shrink:0;gap:8px;flex-wrap:wrap">'
+'<div style="display:flex;align-items:center;gap:10px;min-width:0;flex:1">'
+routeNav
+'<div style="min-width:0;flex:1"><div style="color:#fff;font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+p.addr+'</div>'
+'<div style="color:#94a3b8;font-size:11px">'+(p.kundNamn||'')+(p.photos.length?' — '+p.photos.length+' bilder':'')+'</div>'
+'<div id="svLookingAt" style="display:none;margin-top:4px"></div>'
+'</div>'
+'</div>'
+'<div style="display:flex;align-items:center;gap:6px">'
+'<button onclick="svRemoveCurrentProspect()" style="padding:8px 14px;background:rgba(239,68,68,.2);color:#fca5a5;border:1px solid rgba(239,68,68,.3);border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:5px"><svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>Ta bort</button>'
+'<span id="svCaptureCount" style="color:#94a3b8;font-size:12px"></span>'
+'<button id="svCaptureBtn" onclick="captureStreetViewImage()" style="padding:8px 16px;background:#3b82f6;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px;transition:all .15s"><svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>Ta bild</button>'
+'<button onclick="closeStreetViewModal()" style="background:rgba(255,255,255,.1);border:none;color:#fff;width:36px;height:36px;border-radius:8px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button>'
+'</div></div>'
+'<div id="svPanoContainer" style="flex:1;position:relative"></div>'
+'<div id="svThumbs" style="display:flex;gap:6px;padding:10px 16px;background:#1e293b;overflow-x:auto;flex-shrink:0;min-height:56px;align-items:center"></div>';
document.body.appendChild(modal);
// Initiera Street View Panorama
const panoEl = document.getElementById('svPanoContainer');
svPanorama = new google.maps.StreetViewPanorama(panoEl, {
position: {lat: p.lat, lng: p.lng},
pov: {heading: 0, pitch: 10},
zoom: 1,
addressControl: false,
fullscreenControl: false,
motionTracking: false,
motionTrackingControl: false,
linksControl: true,
panControl: true,
zoomControl: true,
enableCloseButton: false
});
// Lyssna på position-ändringar (navigering i SV)
let svGeoTimer = null;
svPanorama.addListener('position_changed', function(){
clearTimeout(svGeoTimer);
svGeoTimer = setTimeout(() => {
const pos = svPanorama.getPosition();
if(!pos) return;
const pPos = faltProspects[svProspectIdx];
const dist = Math.sqrt(Math.pow(pos.lat()-pPos.lat,2)+Math.pow(pos.lng()-pPos.lng,2)) * 111000;
// Om vi rört oss >30m från prospektet, reverse geocode
if(dist > 30){
svReverseGeocode(pos.lat(), pos.lng());
} else {
const el = document.getElementById('svLookingAt');
if(el) el.style.display = 'none';
}
}, 800);
});
// Kolla om Street View finns
const svService = new google.maps.StreetViewService();
svService.getPanorama({location: {lat: p.lat, lng: p.lng}, radius: 100}, function(data, status){
if(status !== 'OK'){
panoEl.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#94a3b8;font-size:16px">Ingen Street View — <button onclick="svGoProspect(1)" style="margin-left:8px;padding:6px 14px;background:#3b82f6;color:#fff;border:none;border-radius:8px;cursor:pointer;font-family:inherit;font-size:14px">Nästa</button></div>';
document.getElementById('svCaptureBtn').disabled = true;
document.getElementById('svCaptureBtn').style.opacity = '0.4';
}
});
updateSvThumbs();
}
async function captureStreetViewImage(){
if(!svPanorama || svProspectIdx === null) return;
const btn = document.getElementById('svCaptureBtn');
const origHtml = btn.innerHTML;
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;animation:spin 1s linear infinite"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Sparar...';
btn.disabled = true;
try {
const pov = svPanorama.getPov();
const pos = svPanorama.getPosition();
const heading = Math.round(pov.heading * 10) / 10;
const pitch = Math.round(pov.pitch * 10) / 10;
// Hämta bild via Static Street View API (pålitligt)
const fov = Math.round(180 / Math.pow(2, svPanorama.getZoom()));
const panoId = svPanorama.getPano();
let svUrl = 'https://maps.googleapis.com/maps/api/streetview?size=640x360';
if(panoId) svUrl += '&pano='+encodeURIComponent(panoId);
else svUrl += '&location='+pos.lat()+','+pos.lng();
svUrl += '&heading='+heading+'&pitch='+pitch+'&fov='+fov+'&key='+GOOGLE_MAPS_KEY;
// Ladda bilden via img-element (undviker CORS-problem med fetch)
const dataUrl = await new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function(){
const cv = document.createElement('canvas');
cv.width = this.naturalWidth;
cv.height = this.naturalHeight;
cv.getContext('2d').drawImage(this, 0, 0);
try { resolve(cv.toDataURL('image/png')); }
catch(e){ resolve(cv.toDataURL('image/jpeg', 0.95)); }
};
img.onerror = () => {
// Sista fallback: direkt fetch
fetch(svUrl).then(r => r.blob()).then(b => {
if(b.size < 5000) reject(new Error('Ingen bild'));
else {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(b);
}
}).catch(reject);
};
img.src = svUrl;
});
const thumb = await resizeImage(dataUrl, 200);
// Ladda upp original till servern — spara URL istället för megabyte base64 i localStorage
let fullUrl = dataUrl;
try {
const upResp = await fetch('/api/upload-photo.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ image: dataUrl, name: 'sv_h'+Math.round(heading)+'_p'+Math.round(pitch) })
});
const upData = await upResp.json();
if(upData.success && upData.url) fullUrl = upData.url;
} catch(ue){ console.warn('Upload failed, keeping base64:', ue); }
const p = faltProspects[svProspectIdx];
p.photos.push({
full: fullUrl,
thumb: thumb,
name: 'sv_h'+Math.round(heading)+'_p'+Math.round(pitch)+'.jpg',
date: new Date().toISOString(),
type: 'streetview',
heading: heading,
pitch: pitch
});
svPhotoCount++;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltList();
updateSvThumbs();
// Flash-effekt
const flash = document.createElement('div');
flash.style.cssText = 'position:absolute;inset:0;background:#fff;z-index:10;opacity:0.8;transition:opacity .3s';
document.getElementById('svPanoContainer').appendChild(flash);
setTimeout(()=>flash.style.opacity='0', 50);
setTimeout(()=>flash.remove(), 400);
} catch(e){
console.error('SV capture error:', e);
} finally {
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg> Ta bild';
btn.disabled = false;
}
}
function updateSvThumbs(){
const container = document.getElementById('svThumbs');
if(!container || svProspectIdx === null) return;
const p = faltProspects[svProspectIdx];
const photos = (p.photos||[]).filter(ph => ph.type === 'streetview');
const countEl = document.getElementById('svCaptureCount');
if(countEl) countEl.textContent = photos.length > 0 ? photos.length + ' bilder tagna' : '';
if(photos.length === 0){
container.innerHTML = '<span style="color:#64748b;font-size:12px">Rotera vyn och klicka "Ta bild" för att spara</span>';
return;
}
container.innerHTML = photos.map((ph,i) => {
const allIdx = (p.photos||[]).indexOf(ph);
return '<div style="position:relative;flex-shrink:0">'
+'<img src="'+ph.thumb+'" style="width:50px;height:38px;border-radius:6px;object-fit:cover;border:2px solid #334155;cursor:pointer" onclick="event.stopPropagation();viewProspectPhotos('+svProspectIdx+')">'
+'<button onclick="event.stopPropagation();deleteProspectPhoto('+svProspectIdx+','+allIdx+');updateSvThumbs()" style="position:absolute;top:-4px;right:-4px;width:16px;height:16px;background:#ef4444;color:#fff;border:none;border-radius:50%;font-size:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1">×</button>'
+'</div>';
}).join('');
}
function svGoProspect(dir){
const newIdx = svProspectIdx + dir;
if(newIdx < 0 || newIdx >= faltProspects.length) return;
openStreetView(newIdx);
}
let svSpottedAddr = null;
let svSpottedLat = null;
let svSpottedLng = null;
function svReverseGeocode(lat, lng){
const geocoder = new google.maps.Geocoder();
geocoder.geocode({location:{lat,lng}}, function(results, status){
const el = document.getElementById('svLookingAt');
if(!el) return;
if(status === 'OK' && results[0]){
const addr = results[0].formatted_address.replace(/, Sverige$/, '').replace(/, Sweden$/, '');
// Kolla om denna adress redan finns som prospekt
const existIdx = faltProspects.findIndex(pp => {
if(!pp.addr) return false;
// Kolla adress-match eller nära koordinater (<25m)
const addrMatch = addr.toLowerCase().includes(pp.addr.split(',')[0].toLowerCase());
const coordDist = Math.sqrt(Math.pow(lat-pp.lat,2)+Math.pow(lng-pp.lng,2)) * 111000;
return addrMatch || coordDist < 25;
});
const exists = existIdx >= 0;
const existNum = exists ? existIdx + 1 : 0;
svSpottedAddr = addr;
svSpottedLat = lat;
svSpottedLng = lng;
el.style.display = 'flex';
el.style.cssText = 'display:flex;align-items:center;gap:8px;margin-top:4px;flex-wrap:wrap';
el.innerHTML = '<span style="color:#fbbf24;font-size:12px;font-weight:600">('+addr+')</span>'
+(exists
? '<span style="padding:2px 8px;background:rgba(16,185,129,.15);border:1px solid rgba(16,185,129,.3);border-radius:6px;color:#10b981;font-size:11px;font-weight:600">Finns i rutt (#'+existNum+')</span>'
: '<button onclick="svAddSpottedProspect()" style="padding:3px 10px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:4px"><svg viewBox="0 0 24 24" style="width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Lägg till</button>');
} else {
el.style.display = 'none';
}
});
}
function svAddSpottedProspect(){
if(!svSpottedAddr) return;
// Extrahera gatuadress och stad
const parts = svSpottedAddr.split(',').map(s=>s.trim());
const addr = parts[0] || svSpottedAddr;
const city = parts[1] || '';
const newP = {
id: Date.now(),
addr: addr,
city: city,
lat: svSpottedLat,
lng: svSpottedLng,
status: 'unvisited',
created: new Date().toISOString(),
photos: []
};
faltProspects.push(newP);
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
// Visa bekräftelse
const el = document.getElementById('svLookingAt');
if(el) el.innerHTML = '<span style="color:#10b981;font-size:12px;font-weight:600">Tillagd som prospekt #'+faltProspects.length+'</span>';
svSpottedAddr = null;
}
function svRemoveCurrentProspect(){
if(svProspectIdx === null) return;
const p = faltProspects[svProspectIdx];
if(!p) return;
faltProspects.splice(svProspectIdx, 1);
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
// Gå till nästa prospekt eller stäng
if(faltProspects.length === 0){
closeStreetViewModal();
return;
}
const nextIdx = svProspectIdx >= faltProspects.length ? faltProspects.length - 1 : svProspectIdx;
openStreetView(nextIdx);
}
function closeStreetViewModal(){
const modal = document.getElementById('svModal');
if(modal) modal.remove();
svPanorama = null;
svProspectIdx = null;
svRouteMode = false;
}
function blobToDataUrl(blob){
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function viewProspectPhotos(idx){
const p = faltProspects[idx];
if(!p.photos || p.photos.length === 0) return;
const modal = document.createElement('div');
modal.id = 'photoViewerModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:9999;display:flex;flex-direction:column';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
let currentPhoto = 0;
const render = () => {
const ph = p.photos[currentPhoto];
modal.innerHTML =
// Topbar
'<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;flex-shrink:0">'
+'<button onclick="document.getElementById(\'photoViewerModal\').remove()" style="padding:8px 16px;background:rgba(255,255,255,.1);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" style="width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg> Tillbaka</button>'
+'<span style="color:#fff;font-size:13px;font-weight:600">'+(currentPhoto+1)+' / '+p.photos.length
+(ph.type==='streetview'?' — Street View':' — Foto')+'</span>'
+'<button onclick="event.stopPropagation();deleteProspectPhoto('+idx+','+currentPhoto+')" style="padding:8px 16px;background:rgba(220,38,38,.8);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Ta bort</button>'
+'</div>'
// Bild
+'<div style="flex:1;display:flex;align-items:center;justify-content:center;padding:0 20px;position:relative;min-height:0">'
+(p.photos.length>1?'<button onclick="event.stopPropagation()" id="pvPrev" style="position:absolute;left:10px;top:50%;transform:translateY(-50%);width:44px;height:44px;background:rgba(255,255,255,.15);color:#fff;border:none;border-radius:50%;font-size:20px;cursor:pointer;z-index:2;display:flex;align-items:center;justify-content:center">←</button>':'')
+'<img src="'+ph.full+'" style="max-width:100%;max-height:100%;border-radius:8px;object-fit:contain">'
+(p.photos.length>1?'<button onclick="event.stopPropagation()" id="pvNext" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);width:44px;height:44px;background:rgba(255,255,255,.15);color:#fff;border:none;border-radius:50%;font-size:20px;cursor:pointer;z-index:2;display:flex;align-items:center;justify-content:center">→</button>':'')
+'</div>'
// Info + thumbnails
+'<div style="padding:10px 20px;flex-shrink:0;display:flex;align-items:center;gap:12px;overflow-x:auto">'
+ p.photos.map((thumb,ti) =>
'<img src="'+thumb.thumb+'" onclick="event.stopPropagation()" data-ti="'+ti+'" style="width:48px;height:36px;border-radius:6px;object-fit:cover;cursor:pointer;border:2px solid '+(ti===currentPhoto?'#3b82f6':'rgba(255,255,255,.2)')+';flex-shrink:0">'
).join('')
+'</div>';
// Nav events
if(p.photos.length>1){
const prev = modal.querySelector('#pvPrev');
const next = modal.querySelector('#pvNext');
if(prev) prev.onclick = (e) => { e.stopPropagation(); currentPhoto = (currentPhoto-1+p.photos.length)%p.photos.length; render(); };
if(next) next.onclick = (e) => { e.stopPropagation(); currentPhoto = (currentPhoto+1)%p.photos.length; render(); };
}
// Thumbnail click
modal.querySelectorAll('img[data-ti]').forEach(img => {
img.onclick = (e) => { e.stopPropagation(); currentPhoto = parseInt(img.dataset.ti); render(); };
});
};
render();
document.body.appendChild(modal);
// Keyboard nav
const keyHandler = (e) => {
if(!document.getElementById('photoViewerModal')) { document.removeEventListener('keydown', keyHandler); return; }
if(e.key==='ArrowLeft'){ currentPhoto = (currentPhoto-1+p.photos.length)%p.photos.length; render(); }
else if(e.key==='ArrowRight'){ currentPhoto = (currentPhoto+1)%p.photos.length; render(); }
else if(e.key==='Escape'){ document.getElementById('photoViewerModal')?.remove(); document.removeEventListener('keydown', keyHandler); }
};
document.addEventListener('keydown', keyHandler);
}
function deleteProspectPhoto(idx, photoIdx){
faltProspects[idx].photos.splice(photoIdx, 1);
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
// Close viewer and re-render
const modals = document.querySelectorAll('div[style*="position:fixed"][style*="z-index:9999"]');
modals.forEach(m => { if(m.id !== 'svHelper') m.remove(); });
renderFaltList();
if(faltProspects[idx].photos.length > 0) viewProspectPhotos(idx);
}
function clearAllProspects(){
if(!confirm('Rensa alla '+faltProspects.length+' prospekt?')) return;
faltProspects = [];
currentRouteId = null;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
updateRouteInfoBar();
}
// === ROUTE MANAGEMENT ===
let currentRouteId = null;
let staffList = [];
async function loadStaffList(){
if(staffList.length) return staffList;
try {
const res = await fetch('/api/staff.php');
const data = await res.json();
if(data.success) staffList = data.staff || [];
} catch(e){}
return staffList;
}
function updateRouteInfoBar(){
const bar = document.getElementById('currentRouteInfo');
if(!bar) return;
if(currentRouteId){
bar.style.display = 'block';
bar.innerHTML = '<strong>Aktiv rutt #'+currentRouteId+'</strong> — Ändringar sparas automatiskt';
} else {
bar.style.display = 'none';
}
}
async function showSaveRouteModal(){
if(faltProspects.length === 0){
alert('Lägg till prospekt i rutten först');
return;
}
await loadStaffList();
const currentStaff = staffList.find(s => s.email === (localStorage.getItem('userEmail')||''));
const currentId = currentStaff ? currentStaff.id : 43; // default admin
const modal = document.createElement('div');
modal.id = 'routeSaveModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:500px;width:100%;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:20px 24px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center">'
+'<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin:0">'+(currentRouteId?'Uppdatera rutt':'Spara rutt')+'</h2>'
+'<button onclick="this.closest(\'#routeSaveModal\').remove()" style="background:none;border:none;cursor:pointer;padding:4px"><svg viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#94a3b8;fill:none;stroke-width:2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
+'</div>'
+'<div style="padding:24px">'
+'<div style="margin-bottom:16px">'
+'<label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:6px">RUTTNAMN</label>'
+'<input id="routeName" value="Rutt '+new Date().toLocaleDateString('sv-SE')+'" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">'
+'</div>'
+'<div style="margin-bottom:16px">'
+'<label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:6px">DATUM</label>'
+'<input id="routeDate" type="date" value="'+new Date().toISOString().split('T')[0]+'" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">'
+'</div>'
+'<div style="margin-bottom:16px">'
+'<label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:6px">TILLDELA TILL</label>'
+'<select id="routeAssignee" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">'
+'<option value="">Mig själv</option>'
+staffList.filter(s=>s.active!==0).map(s => '<option value="'+s.id+'"'+(s.id==currentId?' selected':'')+'>'+s.name+'</option>').join('')
+'</select>'
+'</div>'
+'<div style="margin-bottom:20px">'
+'<label style="font-size:12px;font-weight:600;color:#64748b;display:block;margin-bottom:6px">ANTECKNINGAR</label>'
+'<textarea id="routeNotes" rows="2" placeholder="T.ex. fokusområde, mål..." style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical;box-sizing:border-box"></textarea>'
+'</div>'
+'<div style="background:#f8fafc;border-radius:10px;padding:14px;margin-bottom:20px">'
+'<div style="font-size:12px;color:#64748b;margin-bottom:4px">Rutten innehåller</div>'
+'<div style="font-size:20px;font-weight:700;color:#1a1a1a">'+faltProspects.length+' prospekt</div>'
+'<div style="font-size:12px;color:#64748b">'+faltProspects.filter(p=>p.status==='interested').length+' leads, '+faltProspects.filter(p=>p.status==='unvisited').length+' obesökta</div>'
+'</div>'
+'<button onclick="saveRoute()" style="width:100%;padding:14px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:inherit">'+(currentRouteId?'Uppdatera rutt':'Spara rutt')+'</button>'
+'</div></div>';
document.body.appendChild(modal);
}
async function saveRoute(){
const name = document.getElementById('routeName').value.trim();
const date = document.getElementById('routeDate').value;
const assignee = document.getElementById('routeAssignee').value;
const notes = document.getElementById('routeNotes').value.trim();
const currentStaff = staffList.find(s => s.email === (localStorage.getItem('userEmail')||''));
const myId = currentStaff ? currentStaff.id : 43;
const body = {
name: name || 'Rutt ' + date,
created_by: myId,
assigned_to: assignee ? parseInt(assignee) : myId,
status: 'active',
prospects: faltProspects,
notes: notes || null,
route_date: date
};
if(currentRouteId) body.id = currentRouteId;
try {
const res = await fetch('/api/routes.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
const data = await res.json();
if(data.success){
currentRouteId = data.id || currentRouteId;
document.getElementById('routeSaveModal')?.remove();
updateRouteInfoBar();
const assigneeName = assignee ? (staffList.find(s=>s.id==assignee)?.name||'') : 'dig';
alert('Rutt sparad! '+(assignee && assignee != myId ? 'Tilldelad till '+assigneeName+'.' : ''));
} else {
alert('Fel: '+(data.error||'okänt'));
}
} catch(e){
alert('Fel: '+e.message);
}
}
async function showLoadRouteModal(){
const modal = document.createElement('div');
modal.id = 'routeLoadModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:600px;width:100%;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:20px 24px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center;flex-shrink:0">'
+'<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin:0">Sparade rutter</h2>'
+'<button onclick="this.closest(\'#routeLoadModal\').remove()" style="background:none;border:none;cursor:pointer;padding:4px"><svg viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#94a3b8;fill:none;stroke-width:2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
+'</div>'
+'<div id="routeListContent" style="padding:24px;overflow-y:auto;flex:1">'
+'<div style="text-align:center;padding:20px;color:#94a3b8"><div class="spinner" style="display:inline-block;width:24px;height:24px;border:3px solid #e5e7eb;border-top-color:#024550;border-radius:50%;animation:bgspin .6s linear infinite"></div><p style="margin-top:8px;font-size:13px">Laddar rutter...</p></div>'
+'</div></div>';
document.body.appendChild(modal);
// Fetch routes
try {
const res = await fetch('/api/routes.php');
const data = await res.json();
const container = document.getElementById('routeListContent');
if(!data.success || !data.routes || data.routes.length === 0){
container.innerHTML = '<div style="text-align:center;padding:30px;color:#94a3b8"><svg viewBox="0 0 24 24" style="width:40px;height:40px;stroke:currentColor;fill:none;stroke-width:1.5;margin-bottom:8px"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg><p style="font-size:14px;font-weight:600">Inga sparade rutter</p><p style="font-size:12px">Spara din nuvarande rutt med knappen "Spara rutt"</p></div>';
return;
}
const statusColors = {draft:'#94a3b8',active:'#10b981',completed:'#3b82f6',archived:'#64748b'};
const statusLabels = {draft:'Utkast',active:'Aktiv',completed:'Avklarad',archived:'Arkiverad'};
container.innerHTML = data.routes.map(r => {
const count = r.prospect_count || 0;
const col = statusColors[r.status]||'#94a3b8';
return '<div style="border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin-bottom:10px;cursor:pointer;transition:all .15s" onmouseover="this.style.borderColor=\'#3b82f6\';this.style.background=\'#f0f9ff\'" onmouseout="this.style.borderColor=\'#e5e7eb\';this.style.background=\'#fff\'">'
+'<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:8px">'
+'<div>'
+'<div style="font-weight:700;font-size:15px;color:#1a1a1a">'+r.name+'</div>'
+'<div style="font-size:12px;color:#64748b;margin-top:2px">'+(r.route_date||r.created_at?.split(' ')[0]||'')+'</div>'
+'</div>'
+'<span style="background:'+col+'20;color:'+col+';font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;text-transform:uppercase">'+(statusLabels[r.status]||r.status)+'</span>'
+'</div>'
+'<div style="display:flex;gap:16px;font-size:12px;color:#64748b;margin-bottom:10px">'
+'<span>'+count+' prospekt</span>'
+(r.assigned_to_name ? '<span>Tilldelad: <strong>'+r.assigned_to_name+'</strong></span>' : '')
+(r.created_by_name ? '<span>Skapad av: '+r.created_by_name+'</span>' : '')
+'</div>'
+(r.notes ? '<div style="font-size:12px;color:#64748b;margin-bottom:10px;font-style:italic">'+r.notes+'</div>' : '')
+'<div style="display:flex;gap:8px">'
+'<button onclick="loadRoute('+r.id+')" style="flex:1;padding:8px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">Ladda rutt</button>'
+'<button onclick="deleteRoute('+r.id+')" style="padding:8px 12px;background:#fee2e2;color:#dc2626;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">Ta bort</button>'
+'</div>'
+'</div>';
}).join('');
} catch(e){
document.getElementById('routeListContent').innerHTML = '<div style="padding:20px;color:#ef4444;text-align:center">Fel: '+e.message+'</div>';
}
}
async function loadRoute(id){
try {
const res = await fetch('/api/routes.php?id='+id);
const data = await res.json();
if(!data.success || !data.route){
alert('Kunde inte ladda rutt');
return;
}
const route = data.route;
faltProspects = route.prospects || [];
currentRouteId = route.id;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
document.getElementById('routeLoadModal')?.remove();
renderFaltMarkers();
renderFaltList();
updateFaltStats();
renderLeadsTable();
updateRouteInfoBar();
// Zoom to fit all prospects
if(gMap && faltProspects.length > 0){
const bounds = new google.maps.LatLngBounds();
faltProspects.forEach(p => bounds.extend({lat:p.lat,lng:p.lng}));
gMap.fitBounds(bounds, 50);
}
} catch(e){
alert('Fel: '+e.message);
}
}
async function deleteRoute(id){
if(!confirm('Ta bort denna sparade rutt?')) return;
try {
const res = await fetch('/api/routes.php?id='+id, {method:'DELETE'});
const data = await res.json();
if(data.success){
if(currentRouteId === id) { currentRouteId = null; updateRouteInfoBar(); }
document.getElementById('routeLoadModal')?.remove();
showLoadRouteModal(); // refresh list
}
} catch(e){ alert('Fel: '+e.message); }
}
/* === LEADS TABLE === */
function renderLeadsTable(){
const body=document.getElementById('faltKalkylBody');
if(!body)return;
const leads = faltProspects.filter(p=>p.status!=='unvisited');
if(leads.length===0){
body.innerHTML='<tr><td colspan="6" style="text-align:center;padding:30px;color:#94a3b8">Inga besök registrerade ännu. Checka in vid ett hus för att skapa leads.</td></tr>';
document.getElementById('fkCount').textContent='(0)';
return;
}
body.innerHTML=leads.map(p=>{
const color=faltStatusColors[p.status];
const idx = faltProspects.indexOf(p);
const time = p.checkinTime ? new Date(p.checkinTime).toLocaleString('sv-SE',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : p.created;
const noteIcon = p.note ? '<button onclick="editLeadNote('+idx+')" title="'+((p.note||'').replace(/"/g,'"'))+'" style="background:none;border:none;cursor:pointer;padding:2px"><svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:#3b82f6;stroke-width:2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>' : '<button onclick="editLeadNote('+idx+')" style="background:none;border:none;cursor:pointer;padding:2px;opacity:.3"><svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:none;stroke:#94a3b8;stroke-width:2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>';
const actionBtn = p.status==='interested' ? '<button class="fk-action" onclick="createKalkylFromLead('+idx+')">Kalkyl</button>'
: (p.status==='kalkyl'||p.status==='offert') && ((p.quoteIds && p.quoteIds.length) || p.quoteId) ? '<button class="fk-action" onclick="showProspectQuotes('+idx+')">Kalkyler ('+ ((p.quoteIds||[]).length || 1) +')</button>' : '';
return '<tr>'
+'<td><span style="display:inline-block;width:7px;height:7px;border-radius:50%;background:'+color+';margin-right:4px"></span><span class="fk-badge" style="background:'+color+'15;color:'+color+';font-size:10px;padding:2px 7px">'+faltStatusLabels[p.status]+'</span></td>'
+'<td class="addr-cell" title="'+p.addr+(p.city?', '+p.city:'')+'">'+p.addr+'</td>'
+'<td>'+(p.kundNamn||'-')+'</td>'
+'<td>'+(p.kundTel||'-')+'</td>'
+'<td style="font-size:11px;color:#64748b">'+time+'</td>'
+'<td style="white-space:nowrap">'+actionBtn+noteIcon+'</td>'
+'</tr>';
}).join('');
document.getElementById('fkCount').textContent='('+leads.length+' st)';
}
function createKalkylFromLead(idx){
const p = faltProspects[idx];
if((p.quoteIds && p.quoteIds.length) || p.quoteId){ showProspectQuotes(idx); return; }
const addr = p.addr + (p.city ? ', ' + p.city : '');
const modal = document.createElement('div');
modal.id = 'kalkylStep1Modal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
// Build Hitta.se persons section
const hPersons = p.hittaPersons || [];
let hittaHtml = '';
if(hPersons.length > 0){
const fp0 = hPersons[0];
const propInfo = [];
if(fp0.livingArea) propInfo.push(fp0.livingArea);
if(fp0.household) propInfo.push(fp0.household);
hittaHtml = '<div style="margin-bottom:18px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:14px 16px">'
+(propInfo.length ? '<div style="font-size:11px;color:#64748b;margin-bottom:8px;padding:6px 10px;background:#fff;border-radius:6px;border:1px solid #e5e7eb">'+propInfo.join(' · ')+'</div>' : '')
+'<div style="font-size:11px;font-weight:700;color:#059669;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Boende (Hitta.se)</div>'
+'<div style="display:flex;flex-direction:column;gap:6px">'
+ hPersons.map(hp => {
const ageStr = hp.age ? ' <span style="font-size:11px;color:#64748b;font-weight:400">'+hp.age+' år</span>' : '';
return '<button onclick="document.getElementById(\'ks1Name\').value=\''+hp.name.replace(/'/g,"\\'")+'\';document.getElementById(\'ks1OwnerName1\').value=\''+hp.name.replace(/'/g,"\\'")+'\';this.style.background=\'#dcfce7\'" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;cursor:pointer;background:#fff;font-family:inherit;color:#1a1a1a;text-align:left;font-weight:600;transition:background .15s"><span>'+hp.name+'</span>'+ageStr+'</button>';
}).join('')
+'</div></div>';
}
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:520px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:28px 32px">'
+'<h2 style="font-size:22px;font-weight:800;color:#1a1a1a;margin:0 0 20px">Kundinformation</h2>'
+ hittaHtml
// Namn
+'<div style="margin-bottom:16px">'
+'<label style="font-size:13px;font-weight:600;color:#334155;display:block;margin-bottom:6px">Namn <span style="color:#dc2626">*</span></label>'
+'<input id="ks1Name" type="text" value="'+(p.kundNamn||'').replace(/"/g,'"')+'" placeholder="Förnamn Efternamn" style="width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:inherit;box-sizing:border-box">'
+'</div>'
// E-post
+'<div style="margin-bottom:16px">'
+'<label style="font-size:13px;font-weight:600;color:#334155;display:block;margin-bottom:6px">E-post <span style="color:#dc2626">*</span></label>'
+'<input id="ks1Email" type="email" value="'+(p.kundEmail||'').replace(/"/g,'"')+'" placeholder="kund@email.se" style="width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:inherit;box-sizing:border-box">'
+'</div>'
// Telefon
+'<div style="margin-bottom:16px">'
+'<label style="font-size:13px;font-weight:600;color:#334155;display:block;margin-bottom:6px">Telefon</label>'
+'<input id="ks1Phone" type="tel" value="'+(p.kundTel||'').replace(/"/g,'"')+'" placeholder="070-123 45 67" style="width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:inherit;box-sizing:border-box">'
+'</div>'
// Adress
+'<div style="margin-bottom:16px">'
+'<label style="font-size:13px;font-weight:600;color:#334155;display:block;margin-bottom:6px">Adress</label>'
+'<input id="ks1Addr" type="text" value="'+addr.replace(/"/g,'"')+'" placeholder="Gatuadress, Stad" style="width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:inherit;box-sizing:border-box">'
+'</div>'
// Fastighetsägare
+'<div style="margin-bottom:6px">'
+'<label style="font-size:13px;font-weight:600;color:#334155;display:block;margin-bottom:6px">Fastighetsägare</label>'
+'<select id="ks1OwnerType" onchange="updateKs1MaxDeduction()" style="width:100%;padding:12px 16px;border:1.5px solid #e5e7eb;border-radius:10px;font-size:15px;font-family:inherit;box-sizing:border-box;background:#fff">'
+'<option value="1">1 privatperson</option>'
+'<option value="2">2 privatpersoner</option>'
+'<option value="brf">Företag/BRF</option>'
+'</select>'
+'</div>'
+'<div id="ks1MaxDeduction" style="font-size:12px;color:#64748b;margin-bottom:20px">Max Grön teknik-avdrag: 50 000 kr</div>'
// Personuppgifter för skattereduktion
+'<div id="ks1OwnersBox" style="background:#f8f9fa;border:1px solid #e5e7eb;border-radius:12px;padding:20px;margin-bottom:16px">'
+'<h3 style="font-size:15px;font-weight:700;margin:0 0 16px">Personuppgifter för skattereduktion</h3>'
+'<div id="ks1Owner1">'
+'<div style="font-size:12px;font-weight:600;color:#64748b;margin-bottom:8px">Ägare</div>'
+'<input id="ks1OwnerName1" type="text" value="'+(p.kundNamn||'').replace(/"/g,'"')+'" placeholder="Namn" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box;margin-bottom:8px">'
+'<input id="ks1OwnerPnr1" type="text" placeholder="Personnummer (ÅÅÅÅMMDDXXXX)" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">'
+'</div>'
+'<div id="ks1Owner2" style="display:none;margin-top:14px;padding-top:14px;border-top:1px solid #e5e7eb">'
+'<div style="font-size:12px;font-weight:600;color:#64748b;margin-bottom:8px">Ägare 2</div>'
+'<input id="ks1OwnerName2" type="text" placeholder="Namn" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box;margin-bottom:8px">'
+'<input id="ks1OwnerPnr2" type="text" placeholder="Personnummer (ÅÅÅÅMMDDXXXX)" style="width:100%;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;box-sizing:border-box">'
+'</div>'
+'</div>'
// Checkbox: begränsad skattereduktion
+'<label style="display:flex;gap:10px;align-items:flex-start;margin-bottom:24px;cursor:pointer">'
+'<input type="checkbox" id="ks1LimitedDeduction" style="margin-top:3px;width:18px;height:18px;accent-color:#166534">'
+'<div><div style="font-size:13px;font-weight:600;color:#1a1a1a">Kunden har mindre än 50 000 kr i skattereduktion totalt</div>'
+'<div style="font-size:12px;color:#64748b;margin-top:2px">Markera om kunden redan utnyttjat sin skattereduktion eller har begränsat utrymme</div></div>'
+'</label>'
// Nästa-knapp
+'<button onclick="submitKalkylStep1('+idx+')" style="width:100%;padding:14px;background:#166534;color:#fff;border:none;border-radius:12px;font-size:16px;font-weight:700;cursor:pointer;font-family:inherit;transition:background .15s" onmouseover="this.style.background=\'#14532d\'" onmouseout="this.style.background=\'#166534\'">Nästa: Skapa kalkyl</button>'
+'</div></div>';
document.body.appendChild(modal);
}
function updateKs1MaxDeduction(){
const type = document.getElementById('ks1OwnerType').value;
const el = document.getElementById('ks1MaxDeduction');
const owner2 = document.getElementById('ks1Owner2');
if(type === '2'){
el.textContent = 'Max Grön teknik-avdrag: 100 000 kr (50 000 kr per person)';
owner2.style.display = '';
} else if(type === 'brf'){
el.textContent = 'Företag/BRF: Inget grönt teknik-avdrag';
owner2.style.display = 'none';
} else {
el.textContent = 'Max Grön teknik-avdrag: 50 000 kr';
owner2.style.display = 'none';
}
}
// Spara kundinfo och gå till kalkyl steg 2
let pendingKalkylCustomer = null;
let kalkylImages = []; // {url, type, selected:bool, created}
function submitKalkylStep1(idx){
const name = document.getElementById('ks1Name').value.trim();
const email = document.getElementById('ks1Email').value.trim();
if(!name){ alert('Namn krävs'); return; }
if(!email){ alert('E-post krävs'); return; }
const ownerType = document.getElementById('ks1OwnerType').value;
let maxDeduction = 50000;
if(ownerType === '2') maxDeduction = 100000;
if(ownerType === 'brf') maxDeduction = 0;
if(document.getElementById('ks1LimitedDeduction').checked){
const custom = prompt('Ange kvarvarande skattereduktion (kr):', maxDeduction);
if(custom !== null) maxDeduction = parseInt(custom) || 0;
}
pendingKalkylCustomer = {
prospectIdx: idx,
name: name,
email: email,
phone: document.getElementById('ks1Phone').value.trim(),
address: document.getElementById('ks1Addr').value.trim(),
ownerType: ownerType,
maxDeduction: maxDeduction,
owner1: {
name: document.getElementById('ks1OwnerName1').value.trim(),
pnr: document.getElementById('ks1OwnerPnr1').value.trim()
},
owner2: ownerType === '2' ? {
name: document.getElementById('ks1OwnerName2')?.value.trim() || '',
pnr: document.getElementById('ks1OwnerPnr2')?.value.trim() || ''
} : null,
product: faltProspects[idx]?.product || '',
solarData: {
yearlyKwh: faltProspects[idx]?.yearlyKwh || null,
maxPanels: faltProspects[idx]?.maxPanels || null,
roofArea: faltProspects[idx]?.roofArea || null,
solarScore: faltProspects[idx]?.solarScore || null
},
created: new Date().toISOString()
};
// Uppdatera prospect
const p = faltProspects[idx];
p.kundNamn = name;
p.kundEmail = email;
p.kundTel = document.getElementById('ks1Phone').value.trim();
p.kalkylStatus = 'started';
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
// Stäng modal
document.getElementById('kalkylStep1Modal')?.remove();
// Navigera till Ny Kalkyl-sidan
document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
document.querySelectorAll('.page-content').forEach(p=>{p.classList.remove('active');p.style.display='';});
const kalkylPage = document.getElementById('page-konfigurator');
if(kalkylPage) kalkylPage.classList.add('active');
const navItem = document.querySelector('[data-page="konfigurator"]');
if(navItem) navItem.classList.add('active');
// Visa konfiguratorvyn med kundinfo
showKalkylConfig();
populateKalkylFromCustomer();
}
function populateKalkylFromCustomer(){
if(!pendingKalkylCustomer) return;
const c = pendingKalkylCustomer;
const p = faltProspects[c.prospectIdx];
const banner = document.getElementById('kalkylCustomerBanner');
if(!banner) return;
const sd = c.solarData || {};
const hasSolar = sd.yearlyKwh || sd.maxPanels;
// Hitta.se boende-knappar
const hPersons = (p && p.hittaPersons) || [];
let hittaHtml = '';
if(hPersons.length > 0){
const fp0 = hPersons[0];
const propInfo = [];
if(fp0.livingArea) propInfo.push(fp0.livingArea);
if(fp0.household) propInfo.push(fp0.household);
hittaHtml = '<div style="margin-bottom:12px">'
+(propInfo.length ? '<div style="font-size:11px;color:#64748b;margin-bottom:6px;padding:4px 8px;background:#f8fafc;border-radius:6px;border:1px solid #e5e7eb">'+propInfo.join(' · ')+'</div>' : '')
+'<div style="font-size:10px;font-weight:700;color:#059669;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Boende — klicka för att fylla i</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:4px">'
+ hPersons.map(hp => {
const ageStr = hp.age ? ' · '+hp.age+' år' : '';
return '<button onclick="fillKalkylCustomerName(\''+hp.name.replace(/'/g,"\\'")+'\')" style="padding:5px 10px;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;cursor:pointer;background:#f0fdf4;font-family:inherit;color:#166534;font-weight:600;transition:background .15s" onmouseover="this.style.background=\'#dcfce7\'" onmouseout="this.style.background=\'#f0fdf4\'">'+hp.name+'<span style="font-weight:400;color:#64748b;font-size:11px">'+ageStr+'</span></button>';
}).join('')
+'</div></div>';
}
banner.style.cssText = 'background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:18px 20px;margin-bottom:20px';
banner.innerHTML = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;cursor:pointer" onclick="toggleKalkylCustomerEdit()">'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#024550;stroke-width:2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'
+'<span style="font-size:14px;font-weight:700;color:#1a1a1a">Kundinformation</span>'
+'<span id="kcNamePreview" style="font-size:13px;color:#64748b;font-weight:400;margin-left:8px">'+(c.name||'Ej ifylld')+'</span>'
+'</div>'
+'<svg id="kcChevron" viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#94a3b8;stroke-width:2;transition:transform .2s"><polyline points="6 9 12 15 18 9"/></svg>'
+'</div>'
+'<div id="kcEditSection" style="display:none">'
+ hittaHtml
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Namn</label>'
+'<input id="kcName" type="text" value="'+(c.name||'').replace(/"/g,'"')+'" placeholder="Förnamn Efternamn" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Telefon</label>'
+'<input id="kcPhone" type="tel" value="'+(c.phone||'').replace(/"/g,'"')+'" placeholder="070-123 45 67" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">E-post</label>'
+'<input id="kcEmail" type="email" value="'+(c.email||'').replace(/"/g,'"')+'" placeholder="kund@email.se" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Adress</label>'
+'<input id="kcAddr" type="text" value="'+(c.address||'').replace(/"/g,'"')+'" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px">'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Ägare</label>'
+'<select id="kcOwnerType" onchange="updateKcOwner();syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box;background:#fff">'
+'<option value="1"'+(c.ownerType==='1'?' selected':'')+'>1 privatperson</option>'
+'<option value="2"'+(c.ownerType==='2'?' selected':'')+'>2 privatpersoner</option>'
+'<option value="brf"'+(c.ownerType==='brf'?' selected':'')+'>Företag/BRF</option>'
+'</select></div>'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Personnr ägare 1</label>'
+'<input id="kcPnr1" type="text" value="'+((c.owner1&&c.owner1.pnr)||'').replace(/"/g,'"')+'" placeholder="ÅÅÅÅMMDDXXXX" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div id="kcOwner2Box" style="'+(c.ownerType==='2'?'':'display:none')+'"><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:3px">Personnr ägare 2</label>'
+'<input id="kcPnr2" type="text" value="'+((c.owner2&&c.owner2.pnr)||'').replace(/"/g,'"')+'" placeholder="ÅÅÅÅMMDDXXXX" onchange="syncKalkylCustomer()" style="width:100%;padding:8px 10px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'</div>'
// Solar data
+(hasSolar ? '<div style="margin-top:8px;padding:10px 12px;background:#f0f9ff;border:1px solid #bfdbfe;border-radius:8px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">'
+'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;flex-shrink:0"><input type="checkbox" id="useSolarData" checked onchange="applySolarDataToCalc()" style="width:16px;height:16px;accent-color:#2563eb"><span style="font-size:12px;font-weight:600;color:#1e40af">Soldata från FältSälj</span></label>'
+'<div style="display:flex;gap:12px;font-size:11px;color:#334155">'
+(sd.yearlyKwh ? '<span><strong>'+sd.yearlyKwh.toLocaleString('sv-SE')+'</strong> kWh/år</span>' : '')
+(sd.maxPanels ? '<span>Max <strong>'+sd.maxPanels+'</strong> paneler</span>' : '')
+(sd.roofArea ? '<span><strong>'+sd.roofArea+'</strong> m² tak</span>' : '')
+(sd.solarScore ? '<span>Score: <strong>'+sd.solarScore+'</strong>/5</span>' : '')
+'</div></div>' : '')
+'</div>';
// Förvälj solceller
const catSel = document.getElementById('categorySelect');
if(catSel){
catSel.value = 'solceller';
changeCategory();
}
// Applicera soldata
if(hasSolar) setTimeout(applySolarDataToCalc, 100);
// Initiera prospektbilder
initKalkylPhotos();
}
function toggleKalkylCustomerEdit(){
const sec = document.getElementById('kcEditSection');
const chev = document.getElementById('kcChevron');
if(!sec) return;
if(sec.style.display === 'none'){
sec.style.display = 'block';
if(chev) chev.style.transform = 'rotate(180deg)';
} else {
sec.style.display = 'none';
if(chev) chev.style.transform = '';
}
}
function fillKalkylCustomerName(name){
const el = document.getElementById('kcName');
if(el) el.value = name;
// Uppdatera preview
const prev = document.getElementById('kcNamePreview');
if(prev) prev.textContent = name;
syncKalkylCustomer();
}
function updateKcOwner(){
const type = document.getElementById('kcOwnerType')?.value;
const box = document.getElementById('kcOwner2Box');
if(box) box.style.display = type === '2' ? '' : 'none';
}
function syncKalkylCustomer(){
if(!pendingKalkylCustomer) return;
const c = pendingKalkylCustomer;
c.name = document.getElementById('kcName')?.value.trim() || '';
c.phone = document.getElementById('kcPhone')?.value.trim() || '';
c.email = document.getElementById('kcEmail')?.value.trim() || '';
c.address = document.getElementById('kcAddr')?.value.trim() || '';
c.ownerType = document.getElementById('kcOwnerType')?.value || '1';
let maxD = 50000;
if(c.ownerType === '2') maxD = 100000;
if(c.ownerType === 'brf') maxD = 0;
c.maxDeduction = maxD;
c.owner1 = { name: c.name, pnr: document.getElementById('kcPnr1')?.value.trim() || '' };
c.owner2 = c.ownerType === '2' ? { name: '', pnr: document.getElementById('kcPnr2')?.value.trim() || '' } : null;
// Uppdatera preview
const prev = document.getElementById('kcNamePreview');
if(prev) prev.textContent = c.name || 'Ej ifylld';
// Uppdatera prospect
const p = faltProspects[c.prospectIdx];
if(p){
p.kundNamn = c.name;
p.kundTel = c.phone;
p.kundEmail = c.email;
localStorage.setItem('faltProspects', JSON.stringify(faltProspects));
}
// Uppdatera kalkyl owner count
updateSolarCalc();
}
// === KALKYL PHOTOS (inbäddad bildgenerering) ===
let kpExpanded = false;
let kpUploadedImage = null; // base64 original för bildgenerering
let kpSelectedProduct = null;
let kpSelectedSub = null;
let kpSelectedCount = null;
let kpLastResultUrl = null;
let kpProspectOriginals = []; // original full-quality bilder
let kpActiveProspectIdx = -1; // vilken prospektbild som är vald som bas
function initKalkylPhotos(){
kalkylImages = [];
kpExpanded = false;
kpUploadedImage = null;
kpSelectedProduct = null;
kpSelectedSub = null;
kpSelectedCount = null;
kpLastResultUrl = null;
kpProspectOriginals = [];
kpActiveProspectIdx = -1;
const sec = document.getElementById('kalkylPhotosSection');
if(!sec) return;
// Ladda prospektbilder från fältsälj — ALLTID originalkvalitet
const c = pendingKalkylCustomer;
if(c && c.prospectIdx !== undefined){
const p = faltProspects[c.prospectIdx];
if(p && p.photos && p.photos.length > 0){
kpProspectOriginals = p.photos.map(ph => ({
full: ph.full || ph.url || ph.dataUrl,
thumb: ph.thumb || ph.full || ph.url || ph.dataUrl,
type: ph.type || 'photo',
created: ph.date || ph.created || new Date().toISOString()
})).filter(ph => ph.full);
}
}
sec.style.display = 'block';
renderKpProspectPhotos();
initKpOptions();
renderKalkylPhotos();
}
function renderKpProspectPhotos(){
const container = document.getElementById('kpProspectPhotos');
if(!container) return;
if(kpProspectOriginals.length === 0){
container.innerHTML = '<div style="padding:10px;text-align:center;color:#94a3b8;font-size:12px;border:1px dashed #e5e7eb;border-radius:8px;margin-bottom:8px">Inga prospektbilder. Ladda upp eller ta ett foto.</div>';
return;
}
container.innerHTML = '<div style="margin-bottom:10px">'
+'<label style="display:block;font-size:12px;font-weight:600;color:#059669;margin-bottom:6px">Prospektbilder — klicka för att använda som bas</label>'
+'<div style="display:flex;gap:8px;flex-wrap:wrap">'
+ kpProspectOriginals.map((ph, i) => {
const isActive = (i === kpActiveProspectIdx);
return '<div onclick="useProspectPhotoAsBase('+i+')" style="width:80px;height:60px;border-radius:8px;overflow:hidden;border:2px solid '+(isActive?'#059669':'#e5e7eb')+';cursor:pointer;position:relative;transition:border-color .15s" id="kpPPhoto_'+i+'" onmouseover="this.style.borderColor=\'#059669\'" onmouseout="this.style.borderColor=this.dataset.active===\'1\'?\'#059669\':\'#e5e7eb\'" data-active="'+(isActive?'1':'0')+'">'
+'<img src="'+ph.thumb+'" style="width:100%;height:100%;object-fit:cover">'
+'</div>';
}).join('')
+'</div></div>';
}
async function useProspectPhotoAsBase(idx){
const photo = kpProspectOriginals[idx];
if(!photo) return;
kpActiveProspectIdx = idx;
const prev = document.getElementById('kpBgPreview');
const prevImg = document.getElementById('kpBgPreviewImg');
if(prevImg) prevImg.src = photo.full;
if(prev) prev.style.display = 'block';
// Markera aktiv
document.querySelectorAll('[id^="kpPPhoto_"]').forEach(d => { d.style.borderColor='#e5e7eb'; d.dataset.active='0'; });
const el = document.getElementById('kpPPhoto_'+idx);
if(el){ el.style.borderColor = '#059669'; el.dataset.active = '1'; }
// Om URL (inte base64) → hämta som base64 för API:t
if(photo.full && !photo.full.startsWith('data:')){
try {
const resp = await fetch(photo.full);
const blob = await resp.blob();
kpUploadedImage = await blobToDataUrl(blob);
} catch(e){
console.error('Kunde inte hämta original:', e);
kpUploadedImage = photo.full;
}
} else {
kpUploadedImage = photo.full;
}
}
function handleKpBgUpload(input){
const file = input.files?.[0];
if(!file) return;
const reader = new FileReader();
reader.onload = function(e){
kpUploadedImage = e.target.result;
kpActiveProspectIdx = -1;
document.getElementById('kpBgPreviewImg').src = kpUploadedImage;
document.getElementById('kpBgPreview').style.display = 'block';
document.querySelectorAll('[id^="kpPPhoto_"]').forEach(d => { d.style.borderColor='#e5e7eb'; d.dataset.active='0'; });
};
reader.readAsDataURL(file);
}
function clearKpBgUpload(){
kpUploadedImage = null;
kpActiveProspectIdx = -1;
document.getElementById('kpBgPreview').style.display = 'none';
document.getElementById('kpBgFileInput').value = '';
document.getElementById('kpBgCameraInput').value = '';
document.querySelectorAll('[id^="kpPPhoto_"]').forEach(d => { d.style.borderColor='#e5e7eb'; d.dataset.active='0'; });
}
function initKpOptions(){
const grid = document.getElementById('kpOptionGrid');
if(!grid) return;
grid.innerHTML = Object.entries(bgProducts).map(([key, p]) =>
'<div onclick="selectKpProduct(\''+key+'\')" id="kpOpt_'+key+'" style="padding:10px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc">'
+'<div style="font-size:18px;margin-bottom:2px">'+p.icon+'</div>'
+'<div style="font-size:11px;font-weight:600;color:#1a1a1a">'+p.label+'</div>'
+'</div>'
).join('');
}
function selectKpProduct(key){
kpSelectedProduct = key;
kpSelectedSub = null;
kpSelectedCount = null;
const p = bgProducts[key];
document.getElementById('kpProjectType').value = p.label;
document.getElementById('kpPrompt').value = p.prompt;
document.querySelectorAll('#kpOptionGrid > div').forEach(d => { d.style.borderColor='#e5e7eb'; d.style.background='#fafbfc'; });
const sel = document.getElementById('kpOpt_'+key);
if(sel){ sel.style.borderColor='#024550'; sel.style.background='#f0fdfa'; }
const panel = document.getElementById('kpDetailsPanel');
const subGrid = document.getElementById('kpSubOptions');
if(p.subs && p.subs.length){
panel.style.display = 'block';
subGrid.innerHTML = p.subs.map((s, i) =>
'<div onclick="selectKpSub('+i+')" id="kpSub_'+i+'" style="padding:8px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc">'
+'<div style="font-size:11px;font-weight:600;color:#1a1a1a">'+s.label+'</div>'
+'</div>'
).join('');
} else {
panel.style.display = 'none';
}
const countPanel = document.getElementById('kpCountPanel');
if(p.hasCount && p.countOptions){
countPanel.style.display = 'block';
document.getElementById('kpCountLabel').textContent = p.countLabel || 'Antal';
document.getElementById('kpCountOptions').innerHTML = p.countOptions.map(n =>
'<div onclick="selectKpCount('+n+')" id="kpCount_'+n+'" style="padding:6px 10px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc;min-width:36px">'
+'<div style="font-size:12px;font-weight:600;color:#1a1a1a">'+n+'</div>'
+'</div>'
).join('');
// Auto-select panel count from kalkyl if solceller
if(key === 'solceller' && currentCalcState.panelCount){
setTimeout(() => selectKpCount(currentCalcState.panelCount), 50);
}
} else {
countPanel.style.display = 'none';
}
}
function selectKpSub(idx){
kpSelectedSub = idx;
updateKpPrompt();
document.querySelectorAll('#kpSubOptions > div').forEach(d => { d.style.borderColor='#e5e7eb'; d.style.background='#fafbfc'; });
const sel = document.getElementById('kpSub_'+idx);
if(sel){ sel.style.borderColor='#024550'; sel.style.background='#f0fdfa'; }
}
function selectKpCount(n){
kpSelectedCount = n;
updateKpPrompt();
document.querySelectorAll('#kpCountOptions > div').forEach(d => { d.style.borderColor='#e5e7eb'; d.style.background='#fafbfc'; });
const sel = document.getElementById('kpCount_'+n);
if(sel){ sel.style.borderColor='#024550'; sel.style.background='#f0fdfa'; }
}
function updateKpPrompt(){
const p = bgProducts[kpSelectedProduct];
if(!p) return;
let prompt = p.prompt;
if(kpSelectedSub !== null && p.subs[kpSelectedSub]){
prompt += ' ' + p.subs[kpSelectedSub].extra;
}
if(kpSelectedCount && p.hasCount){
const rows = kpSelectedCount <= 8 ? 2 : kpSelectedCount <= 18 ? 2 : 3;
const cols = Math.ceil(kpSelectedCount / rows);
prompt += ' CRITICAL: Place EXACTLY '+kpSelectedCount+' solar panels on the roof — arranged as '+rows+' rows x '+cols+' columns. There must be precisely '+kpSelectedCount+' panels total, not more and not less.';
}
document.getElementById('kpPrompt').value = prompt;
}
async function generateKpImage(){
const prompt = document.getElementById('kpPrompt').value.trim();
const projectType = document.getElementById('kpProjectType').value;
const extra = document.getElementById('kpExtraPrompt')?.value?.trim() || '';
if(!kpUploadedImage){ alert('Välj en prospektbild eller ladda upp ett foto av huset först'); return; }
if(!prompt || !projectType){ alert('Välj en produkttyp först'); return; }
const fullPrompt = kpUploadedImage
? prompt + (extra ? '. ' + extra : '')
: prompt + (extra ? '. Additional details: ' + extra : '') + '. Show the ENTIRE house from street level, wide angle, full building visible. Photorealistic, professional photography, Scandinavian architecture.';
const btn = document.getElementById('kpGenerateBtn');
btn.disabled = true;
btn.textContent = 'Genererar...';
document.getElementById('kpPlaceholder').style.display = 'none';
document.getElementById('kpResult').style.display = 'none';
document.getElementById('kpLoading').style.display = 'flex';
try {
const provider = document.getElementById('kpProvider').value;
const res = await fetch('/api/generate.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({
prompt: fullPrompt,
projectType: projectType,
baseImage: kpUploadedImage,
provider: provider
})
});
const data = await res.json();
if(data.error) throw new Error(data.error);
document.getElementById('kpLoading').style.display = 'none';
document.getElementById('kpResult').style.display = 'block';
kpLastResultUrl = data.base64 || data.imageUrl;
document.getElementById('kpResultImg').src = kpLastResultUrl;
} catch(err){
document.getElementById('kpLoading').style.display = 'none';
document.getElementById('kpPlaceholder').style.display = 'flex';
alert('Fel: '+err.message);
} finally {
btn.disabled = false;
btn.textContent = 'Generera bild';
}
}
function saveKpResultToGallery(){
if(!kpLastResultUrl) return;
// Spara före-bild om den inte redan finns
if(kpUploadedImage && !kalkylImages.find(i => i.url === kpUploadedImage)){
kalkylImages.push({ url: kpUploadedImage, type: 'before', selected: false, created: new Date().toISOString() });
}
// Spara efter-bild
kalkylImages.push({ url: kpLastResultUrl, type: 'after', selected: true, created: new Date().toISOString() });
renderKalkylPhotos();
// Reset result
document.getElementById('kpResult').style.display = 'none';
document.getElementById('kpPlaceholder').style.display = 'flex';
kpLastResultUrl = null;
}
function downloadKpResult(){
if(!kpLastResultUrl) return;
const a = document.createElement('a');
a.href = kpLastResultUrl;
a.download = 'prospekt-'+Date.now()+'.png';
a.click();
}
function resetKpResult(){
document.getElementById('kpResult').style.display = 'none';
document.getElementById('kpPlaceholder').style.display = 'flex';
kpLastResultUrl = null;
}
function toggleKalkylPhotos(){
kpExpanded = !kpExpanded;
const exp = document.getElementById('kpExpanded');
const thumbs = document.getElementById('kpThumbsCollapsed');
const chev = document.getElementById('kpChevron');
if(kpExpanded){
if(exp) exp.style.display = 'block';
if(thumbs) thumbs.style.display = 'none';
if(chev) chev.style.transform = 'rotate(180deg)';
} else {
if(exp) exp.style.display = 'none';
if(thumbs) thumbs.style.display = 'flex';
if(chev) chev.style.transform = '';
}
renderKalkylPhotos();
}
function renderKalkylPhotos(){
// Säkerställ att prospektbilder alltid visas
renderKpProspectPhotos();
const count = document.getElementById('kpCount');
const thumbs = document.getElementById('kpThumbsCollapsed');
const gallery = document.getElementById('kpGallery');
const empty = document.getElementById('kpEmpty');
const selected = kalkylImages.filter(i => i.selected);
const totalPhotos = kpProspectOriginals.length + kalkylImages.length;
if(count) count.textContent = totalPhotos > 0 ? '('+totalPhotos+' bilder'+(selected.length>0?', '+selected.length+' valda':'')+')' : '';
// Collapsed thumbnails — visa ALLA bilder (grön ram = vald)
if(thumbs){
// Visa prospektoriginal + kalkylbilder
const allThumbs = [];
kpProspectOriginals.forEach(ph => {
if(!kalkylImages.find(ki => ki.url === ph.full)){
allThumbs.push({url: ph.thumb, selected: false, source: 'prospect'});
}
});
kalkylImages.forEach(img => allThumbs.push({url: img.url, selected: img.selected, source: 'kalkyl'}));
if(allThumbs.length > 0){
thumbs.innerHTML = allThumbs.map(t =>
'<div style="width:56px;height:56px;border-radius:8px;overflow:hidden;border:2px solid '+(t.selected?'#059669':'#d1d5db')+';flex-shrink:0;opacity:'+(t.selected?'1':'.7')+'">'
+'<img src="'+t.url+'" style="width:100%;height:100%;object-fit:cover">'
+'</div>'
).join('')
+ (selected.length > 0 ? '<span style="font-size:11px;color:#059669;font-weight:600;margin-left:4px">'+selected.length+' valda</span>' : '');
thumbs.style.display = kpExpanded ? 'none' : 'flex';
} else {
thumbs.innerHTML = '<span style="font-size:12px;color:#94a3b8">Inga bilder — expandera för att lägga till</span>';
thumbs.style.display = kpExpanded ? 'none' : 'flex';
}
}
// Gallery (expanded, inside kpGallery)
if(gallery){
const typeLabels = {before:'Före',after:'Efter',photo:'Foto',generated:'Genererad'};
const typeBg = {before:'#fef3c7',after:'#dcfce7',photo:'#e0f2fe',generated:'#f3e8ff'};
const typeColor = {before:'#92400e',after:'#166534',photo:'#0369a1',generated:'#7c3aed'};
if(kalkylImages.length > 0){
if(empty) empty.style.display = 'none';
gallery.innerHTML = kalkylImages.map((img, i) => {
const lbl = typeLabels[img.type]||'Bild';
const bg = typeBg[img.type]||'#f1f5f9';
const clr = typeColor[img.type]||'#64748b';
return '<div style="position:relative;border-radius:8px;overflow:hidden;border:2px solid '+(img.selected?'#059669':'#e5e7eb')+'">'
+'<img src="'+img.url+'" style="width:100%;aspect-ratio:4/3;object-fit:cover;cursor:pointer" onclick="openLightbox(kalkylImages['+i+'].url)">'
+'<div style="position:absolute;top:4px;left:4px;display:flex;gap:3px">'
+'<button onclick="event.stopPropagation();toggleKalkylPhotoSelect('+i+')" style="width:22px;height:22px;background:'+(img.selected?'#059669':'rgba(0,0,0,.5)')+';color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:11px;display:flex;align-items:center;justify-content:center">'+(img.selected?'✓':'')+'</button>'
+'<span style="padding:2px 6px;border-radius:4px;font-size:9px;font-weight:700;background:'+bg+';color:'+clr+'">'+lbl+'</span>'
+'</div>'
+'<div style="position:absolute;top:4px;right:4px;display:flex;gap:3px">'
+'<button onclick="event.stopPropagation();downloadKalkylPhoto('+i+')" style="width:22px;height:22px;background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:10px;display:flex;align-items:center;justify-content:center" title="Ladda ner"><svg viewBox="0 0 24 24" style="width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></button>'
+'<button onclick="event.stopPropagation();removeKalkylPhoto('+i+')" style="width:22px;height:22px;background:rgba(239,68,68,.8);color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:12px;display:flex;align-items:center;justify-content:center">×</button>'
+'</div>'
+'</div>';
}).join('');
} else {
gallery.innerHTML = '';
if(empty) empty.style.display = 'block';
}
}
}
function toggleKalkylPhotoSelect(idx){
if(kalkylImages[idx]) kalkylImages[idx].selected = !kalkylImages[idx].selected;
renderKalkylPhotos();
}
function removeKalkylPhoto(idx){
kalkylImages.splice(idx, 1);
renderKalkylPhotos();
}
function downloadKalkylPhoto(idx){
const img = kalkylImages[idx];
if(!img) return;
const a = document.createElement('a');
a.href = img.url;
a.download = 'prospekt_'+(idx+1)+'.png';
a.target = '_blank';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function applySolarDataToCalc(){
if(!pendingKalkylCustomer) return;
const sd = pendingKalkylCustomer.solarData || {};
const use = document.getElementById('useSolarData')?.checked;
if(use && sd.maxPanels){
const slider = document.getElementById('solPanelCount');
if(slider){
// Clamp to slider range
let count = Math.min(80, Math.max(8, sd.maxPanels));
// Round to nearest even
count = Math.round(count / 2) * 2;
slider.value = count;
}
}
updateSolarCalc();
}
// Alias for backward compat
function renderFaltKalkyler(){ renderLeadsTable(); }
/* === LOGIN === */
const loginQuotes=[
'"Chase the vision, not the money; the money will end up following you." — Tony Hsieh',
'"If you are not taking care of your customer, your competitor will." — Bob Hooey',
'"Success is not final, failure is not fatal: it is the courage to continue that counts." — Winston Churchill',
'"The best way to predict the future is to create it." — Peter Drucker',
'"Don\'t find customers for your products, find products for your customers." — Seth Godin',
'"Every sale has five basic obstacles: no need, no money, no hurry, no desire, no trust." — Zig Ziglar',
'"Quality is remembered long after the price is forgotten." — Gucci',
'"Your most unhappy customers are your greatest source of learning." — Bill Gates',
'"Stop selling. Start helping." — Zig Ziglar',
'"Make a customer, not a sale." — Katherine Barchetti'
];
function setLoginGreeting(){
const h=new Date().getHours();
let g;
if(h>=23||h<5) g='Borde du inte sova nu?';
else if(h<12) g='God morgon';
else if(h<17) g='God eftermiddag';
else g='God kväll';
const big=document.getElementById('loginGreetingBig');
if(big)big.textContent=g;
const q=document.getElementById('loginQuote');
if(q)q.textContent=loginQuotes[Math.floor(Math.random()*loginQuotes.length)];
}
setLoginGreeting();
var gUserRole = '';
async function doLogin(e){
e.preventDefault();
var email=document.getElementById('loginEmail').value.trim();
var pass=document.getElementById('loginPassword').value;
const errEl = document.getElementById('loginError');
const pendEl = document.getElementById('loginPending');
if(errEl) errEl.style.display='none';
if(pendEl) pendEl.style.display='none';
if(!email || !pass){
if(errEl){ errEl.textContent='Ange e-post och lösenord'; errEl.style.display='block'; }
return;
}
try {
const res = await fetch('/api/auth.php?action=login', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({email:email, password:pass})
});
const data = await res.json();
if(data.error){
if(data.pending){
if(pendEl){ pendEl.textContent=data.error; pendEl.style.display='block'; }
} else {
if(errEl){ errEl.textContent=data.error; errEl.style.display='block'; }
}
return;
}
if(data.success && data.user){
gStaffId = data.user.id;
gUserEmail = data.user.email;
gUserName = data.user.name;
gUserRole = data.user.role || '';
gUserAvatar = data.user.avatar_url || '';
gMailSignature = data.user.mail_signature || '';
sessionStorage.setItem('gStaffId', gStaffId);
sessionStorage.setItem('gUserEmail', gUserEmail);
sessionStorage.setItem('gUserName', gUserName);
sessionStorage.setItem('gUserRole', gUserRole);
sessionStorage.setItem('gUserAvatar', gUserAvatar);
localStorage.setItem('mailSignature', gMailSignature);
enterApp(gUserEmail, gUserAvatar);
}
} catch(err){
if(errEl){ errEl.textContent='Anslutningsfel: '+err.message; errEl.style.display='block'; }
}
}
function enterApp(email, avatar) {
document.getElementById('loginScreen').classList.add('hidden');
document.getElementById('appSidebar').style.display='';
document.getElementById('appMain').style.display='';
var userInfo=document.querySelector('.sidebar-user-info strong');
if(userInfo&&email) userInfo.textContent=email;
var userSpan=document.querySelector('.sidebar-user-info span');
if(userSpan) userSpan.textContent = (typeof gUserName !== 'undefined' && gUserName) ? gUserName : 'Inloggad som';
if(avatar) {
var avatarEl = document.querySelector('.sidebar-user');
if(avatarEl && !document.getElementById('sidebarAvatar')) {
avatarEl.insertAdjacentHTML('afterbegin', '<img id="sidebarAvatar" src="'+avatar+'" style="width:36px;height:36px;border-radius:50%;object-fit:cover;margin-right:8px">');
}
}
if(typeof updateAuthUI === 'function') updateAuthUI();
// Restore role from sessionStorage if not set (Google login)
if(!gUserRole) gUserRole = sessionStorage.getItem('gUserRole') || '';
updateAdminVisibility();
checkPendingUsers();
applyNavPermissions(); if(typeof loadDashboard==="function") loadDashboard();
}
async function googleLoginFromScreen() {
const clientId = await initGoogleAuth();
if (!clientId) {
alert('Google Client ID inte konfigurerat. Kontakta admin.');
return;
}
const client = google.accounts.oauth2.initCodeClient({
client_id: clientId,
scope: LOGIN_SCOPES,
ux_mode: 'popup',
redirect_uri: window.location.origin,
callback: async (response) => {
if (response.error) { alert('Google login misslyckades: ' + response.error); return; }
try {
const res = await fetch('/api/google-token.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'exchange',
code: response.code,
redirectUri: window.location.origin
})
});
const data = await res.json();
if (data.error) { const errEl = document.getElementById("loginError"); const pendEl = document.getElementById("loginPending"); if(errEl) errEl.style.display="none"; if(pendEl) pendEl.style.display="none"; if(data.pending) { if(pendEl){ pendEl.textContent=data.error; pendEl.style.display="block"; } } else { if(errEl){ errEl.textContent=data.error; errEl.style.display="block"; } } return; }
gAccessToken = data.accessToken;
gTokenExpiry = Date.now() + (data.expiresIn * 1000);
gStaffId = data.user.id;
gUserEmail = data.user.email;
gUserName = data.user.name;
gUserAvatar = data.user.avatar_url || '';
gUserRole = data.user.role || '';
gMailSignature = data.user.mail_signature || '';
sessionStorage.setItem('gAccessToken', gAccessToken);
sessionStorage.setItem('gTokenExpiry', gTokenExpiry);
sessionStorage.setItem('gStaffId', gStaffId);
sessionStorage.setItem('gUserEmail', gUserEmail);
sessionStorage.setItem('gUserName', gUserName);
sessionStorage.setItem('gUserAvatar', gUserAvatar);
sessionStorage.setItem('gUserRole', gUserRole);
localStorage.setItem('mailSignature', gMailSignature);
enterApp(gUserEmail, gUserAvatar);
} catch(e) { alert('Anslutningsfel: ' + e.message); }
}
});
client.requestCode();
}
// Logout
document.querySelector('.sidebar-logout')?.addEventListener('click',function(){
sessionStorage.clear();
gUserRole = '';
gStaffId = '';
gUserEmail = '';
gUserName = '';
document.getElementById('loginScreen').classList.remove('hidden');
document.getElementById('appSidebar').style.display='none';
document.getElementById('appMain').style.display='none';
document.getElementById('loginEmail').value='';
document.getElementById('loginPassword').value='';
const errEl = document.getElementById('loginError');
const pendEl = document.getElementById('loginPending');
if(errEl) errEl.style.display='none';
if(pendEl) pendEl.style.display='none';
hideRegisterForm();
setLoginGreeting();
});
// Registrering
function showRegisterForm(){
document.getElementById('registerForm').style.display='block';
}
function hideRegisterForm(){
document.getElementById('registerForm').style.display='none';
document.getElementById('regError').style.display='none';
document.getElementById('regSuccess').style.display='none';
}
async function doRegister(){
const name = document.getElementById('regName').value.trim();
const email = document.getElementById('regEmail').value.trim();
const password = document.getElementById('regPassword').value;
const errEl = document.getElementById('regError');
const sucEl = document.getElementById('regSuccess');
errEl.style.display='none';
sucEl.style.display='none';
if(!name || !email || !password){
errEl.textContent='Fyll i alla fält'; errEl.style.display='block'; return;
}
if(password.length < 4){
errEl.textContent='Lösenordet måste vara minst 4 tecken'; errEl.style.display='block'; return;
}
try {
const res = await fetch('/api/auth.php?action=register', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({name, email, password})
});
const data = await res.json();
if(data.error){
errEl.textContent=data.error; errEl.style.display='block';
} else {
sucEl.textContent = data.message || 'Konto skapat! Väntar på godkännande från admin.';
sucEl.style.display='block';
document.getElementById('regName').value='';
document.getElementById('regEmail').value='';
document.getElementById('regPassword').value='';
}
} catch(err){
errEl.textContent='Anslutningsfel: '+err.message; errEl.style.display='block';
}
}
// Visa/dölj admin-meny baserat på roll
function updateAdminVisibility(){
const navAdmin = document.getElementById('navAdmin');
if(navAdmin){
navAdmin.style.display = (gUserRole === 'systemadmin' || gUserRole === 'admin' || gUserRole === 'saljchef') ? '' : 'none';
}
}
// Admin-sida
function switchAdminTab(tab){
document.querySelectorAll('[data-admin-tab]').forEach(t => {
const isActive = t.dataset.adminTab === tab;
t.style.borderBottomColor = isActive ? '#024550' : 'transparent';
t.style.color = isActive ? '#024550' : '#64748b';
});
document.getElementById('adminUsersPanel').style.display = tab==='users' ? '' : 'none';
document.getElementById('adminPendingPanel').style.display = tab==='pending' ? '' : 'none';
document.getElementById('adminPermissionsPanel').style.display = tab==='permissions' ? '' : 'none';
if(tab==='users') loadAdminUsers();
if(tab==='pending') loadAdminPending();
if(tab==='permissions') loadPermissions();
}
const PAGE_LABELS = {oversikt:'Dashboard',faltsalj:'FältSälj',kunder:'Kunder',projektering:'Projektering',projekt:'Affärer',konfigurator:'Kalkyler',produkter:'Produktkatalog',ekonomi:'Ekonomi',bildgen:'Bildgenerering',personal:'Personal',kalender:'Kalender',inkorg:'Inkorg',dagrapport:'Dagrapport',minlon:'Min Lön',settings:'Inställningar'};
const PAGE_KEYS = Object.keys(PAGE_LABELS);
const PERM_ROLES = ['admin','saljchef','saljare','installator','projektledare','ekonomi'];
let permData = {};
let permSelectedRole = 'saljare';
var menuOrder = [];
async function loadPermissions(){
try {
const res = await fetch('/api/permissions.php');
permData = await res.json();
renderPermRoleTabs();
renderPermMatrix();
await loadMenuOrder();
} catch(e){ console.error('loadPermissions error', e); }
}
async function loadMenuOrder(){
try {
const res = await fetch('/api/permissions.php?action=order&t='+Date.now());
menuOrder = await res.json();
renderMenuOrder();
} catch(e){ console.error('loadMenuOrder error', e); }
}
function renderMenuOrder(){
const container = document.getElementById('menuOrderList');
if(!container) return;
container.innerHTML = menuOrder.map((key, i) => {
const label = PAGE_LABELS[key] || key;
return `<div draggable="true" data-page-key="${key}" style="padding:8px 12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;color:#1a1a1a;cursor:grab;display:flex;align-items:center;gap:8px;user-select:none;transition:box-shadow .15s" onmousedown="this.style.cursor='grabbing'" onmouseup="this.style.cursor='grab'"><svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:#94a3b8;fill:none;stroke-width:2;flex-shrink:0"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>${label}</div>`;
}).join('');
// Add drag events
const items = container.querySelectorAll('[draggable]');
items.forEach(item => {
item.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', item.dataset.pageKey);
item.style.opacity = '0.4';
});
item.addEventListener('dragend', e => { item.style.opacity = '1'; });
item.addEventListener('dragover', e => {
e.preventDefault();
item.style.boxShadow = '0 -2px 0 #024550';
});
item.addEventListener('dragleave', e => { item.style.boxShadow = ''; });
item.addEventListener('drop', async e => {
e.preventDefault();
item.style.boxShadow = '';
const draggedKey = e.dataTransfer.getData('text/plain');
const targetKey = item.dataset.pageKey;
if(draggedKey === targetKey) return;
const oldIdx = menuOrder.indexOf(draggedKey);
const newIdx = menuOrder.indexOf(targetKey);
menuOrder.splice(oldIdx, 1);
menuOrder.splice(newIdx, 0, draggedKey);
renderMenuOrder();
await saveMenuOrder();
applyNavOrder();
});
});
}
async function saveMenuOrder(){
try {
await fetch('/api/permissions.php?action=order', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ order: menuOrder })
});
} catch(e){ console.error('saveMenuOrder error', e); }
}
function applyNavOrder(){
const nav = document.querySelector('.sidebar-nav');
if(!nav) return;
const items = Array.from(nav.querySelectorAll('.nav-item[data-page]'));
const adminItem = items.find(el => el.dataset.page === 'admin');
items.sort((a, b) => {
const ai = menuOrder.indexOf(a.dataset.page);
const bi = menuOrder.indexOf(b.dataset.page);
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
});
items.forEach(item => nav.appendChild(item));
if(adminItem) nav.appendChild(adminItem);
}
function renderPermRoleTabs(){
const container = document.getElementById('permRoleTabs');
container.innerHTML = PERM_ROLES.map(r => {
const active = r === permSelectedRole;
return `<button onclick="selectPermRole('${r}')" style="padding:8px 16px;border-radius:8px;font-size:13px;font-weight:${active?'600':'500'};cursor:pointer;font-family:inherit;border:1.5px solid ${active?'#024550':'#e5e7eb'};background:${active?'#024550':'#fff'};color:${active?'#fff':'#334155'};transition:all .15s">${ROLE_LABELS[r]}</button>`;
}).join('');
}
function selectPermRole(role){
permSelectedRole = role;
renderPermRoleTabs();
renderPermMatrix();
}
function renderPermMatrix(){
const container = document.getElementById('permMatrixContainer');
const rolePerms = permData[permSelectedRole] || {};
let html = '<div style="display:grid;grid-template-columns:1fr auto;gap:0;border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">';
html += '<div style="padding:10px 16px;background:#f8fafc;font-weight:600;font-size:13px;color:#64748b;border-bottom:1px solid #e5e7eb">Menyval</div>';
html += '<div style="padding:10px 16px;background:#f8fafc;font-weight:600;font-size:13px;color:#64748b;border-bottom:1px solid #e5e7eb;text-align:center">Synlig</div>';
PAGE_KEYS.forEach((key, i) => {
const visible = rolePerms[key] !== undefined ? rolePerms[key] : 1;
const bg = i % 2 === 0 ? '#fff' : '#fafbfc';
const border = i < PAGE_KEYS.length - 1 ? 'border-bottom:1px solid #f1f5f9;' : '';
html += `<div style="padding:10px 16px;font-size:14px;color:#1a1a1a;${border}background:${bg};display:flex;align-items:center;gap:10px">${PAGE_LABELS[key]}</div>`;
html += `<div style="padding:10px 16px;text-align:center;${border}background:${bg}">`;
html += `<label style="position:relative;display:inline-block;width:44px;height:24px;cursor:pointer">`;
html += `<input type="checkbox" ${visible?'checked':''} onchange="togglePerm('${key}',this.checked)" style="opacity:0;width:0;height:0">`;
html += `<span style="position:absolute;inset:0;background:${visible?'#10b981':'#e5e7eb'};border-radius:24px;transition:all .2s"></span>`;
html += `<span style="position:absolute;left:${visible?'22px':'2px'};top:2px;width:20px;height:20px;background:#fff;border-radius:50%;transition:all .2s;box-shadow:0 1px 3px rgba(0,0,0,.15)"></span>`;
html += '</label></div>';
});
html += '</div>';
container.innerHTML = html;
}
async function togglePerm(pageKey, visible){
if(!permData[permSelectedRole]) permData[permSelectedRole] = {};
permData[permSelectedRole][pageKey] = visible ? 1 : 0;
renderPermMatrix();
try {
await fetch('/api/permissions.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ role: permSelectedRole, permissions: { [pageKey]: visible ? 1 : 0 } })
});
} catch(e){ console.error('togglePerm error', e); }
if(permSelectedRole === gUserRole) applyNavPermissions(); if(typeof loadDashboard==="function") loadDashboard();
}
async function applyNavPermissions(){
try {
const res = await fetch('/api/permissions.php?role=' + encodeURIComponent(gUserRole || 'saljare') + '&t=' + Date.now());
const data = await res.json();
const perms = data.permissions || data;
const order = data.order || [];
if(gUserRole !== 'systemadmin') {
document.querySelectorAll('.nav-item[data-page]').forEach(el => {
const page = el.dataset.page;
if(page === 'admin') return;
if(perms[page] !== undefined) {
el.style.display = perms[page] ? '' : 'none';
}
});
}
if(order.length > 0) {
menuOrder = order;
applyNavOrder();
}
} catch(e){ console.error('applyNavPermissions error', e); }
}
const ROLE_LABELS = {systemadmin:'Systemadmin', admin:'Admin', saljchef:'Säljchef', saljare:'Säljare', installator:'Installatör', projektledare:'Projektledare', ekonomi:'Ekonomi', pending:'Väntande'};
const ROLE_OPTIONS = ['systemadmin','admin','saljchef','saljare','installator','projektledare','ekonomi'];
let allAdminUsers = [];
async function loadAdminUsers(){
try {
const res = await fetch('/api/staff.php?active=0&t='+Date.now());
const users = await res.json();
allAdminUsers = users.filter(u => u.approved == 1);
filterAdminUsers();
} catch(err){
console.error('loadAdminUsers error:', err);
}
}
function filterAdminUsers(){
const search = (document.getElementById('adminUserSearch')?.value || '').toLowerCase();
const roleFilter = document.getElementById('adminUserRoleFilter')?.value || '';
let filtered = allAdminUsers;
if(search) filtered = filtered.filter(u => (u.name||'').toLowerCase().includes(search) || (u.email||'').toLowerCase().includes(search));
if(roleFilter) filtered = filtered.filter(u => u.role === roleFilter);
const tbody = document.getElementById('adminUsersBody');
document.getElementById('adminUserCount').textContent = filtered.length + ' av ' + allAdminUsers.length + ' användare';
tbody.innerHTML = filtered.map(u => {
const roleOpts = ROLE_OPTIONS.filter(r => r !== 'systemadmin' || gUserRole === 'systemadmin').map(r => '<option value="'+r+'"'+(u.role===r?' selected':'')+'>'+ROLE_LABELS[r]+'</option>').join('');
const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString('sv-SE') : '—';
return '<tr style="border-bottom:1px solid #f1f5f9">'
+'<td style="padding:10px 12px;font-weight:500">'+escHtml(u.name)+'</td>'
+'<td style="padding:10px 12px;color:#64748b">'+escHtml(u.email)+'</td>'
+'<td style="padding:10px 12px"><select onchange="changeUserRole('+u.id+',this.value)" style="padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;background:#fff;cursor:pointer">'+roleOpts+'</select></td>'
+'<td style="padding:10px 12px;color:#64748b;font-size:12px">'+(u.phone||'—')+'</td>'
+'<td style="padding:10px 12px;text-align:center"><button onclick="toggleUserActive('+u.id+','+(u.active?0:1)+')" style="width:36px;height:20px;border-radius:10px;border:none;cursor:pointer;background:'+(u.active==1?'#059669':'#cbd5e1')+';position:relative;transition:background .2s"><span style="position:absolute;top:2px;'+(u.active==1?'right:2px':'left:2px')+';width:16px;height:16px;background:#fff;border-radius:50%;transition:all .2s"></span></button></td>'
+'<td style="padding:10px 12px;color:#94a3b8;font-size:12px">'+lastLogin+'</td>'
+(gUserRole==='systemadmin'?'<td style="padding:10px 12px;text-align:center"><button onclick="deleteUser('+u.id+',\''+escHtml(u.name)+'\')" style="background:none;border:none;cursor:pointer;color:#ef4444;font-size:14px" title="Ta bort">×</button></td>':'<td></td>')
+'</tr>';
}).join('');
}
async function loadAdminPending(){
try {
const res = await fetch('/api/staff.php?approved=0&t='+Date.now());
const pending = await res.json();
const listEl = document.getElementById('adminPendingList');
const emptyEl = document.getElementById('adminPendingEmpty');
const countEl = document.getElementById('adminPendingCount');
const badgeEl = document.getElementById('adminPendingBadge');
if(pending.length === 0){
emptyEl.style.display='block';
listEl.style.display='none';
if(countEl) countEl.style.display='none';
if(badgeEl) badgeEl.style.display='none';
return;
}
emptyEl.style.display='none';
listEl.style.display='flex';
if(countEl){ countEl.textContent=pending.length; countEl.style.display='inline'; }
if(badgeEl){ badgeEl.textContent=pending.length; badgeEl.style.display='inline'; }
listEl.innerHTML = pending.map(u => {
const createdD = u.created_at ? new Date(u.created_at) : null;
const created = createdD ? createdD.toLocaleDateString('sv-SE')+' kl '+createdD.toLocaleTimeString('sv-SE',{hour:'2-digit',minute:'2-digit'}) : '—';
const roleOpts = ROLE_OPTIONS.filter(r => r !== 'systemadmin' || gUserRole === 'systemadmin').map(r => '<option value="'+r+'"'+(r==='saljare'?' selected':'')+'>'+ROLE_LABELS[r]+'</option>').join('');
return '<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap">'
+'<div style="flex:1;min-width:200px">'
+'<div style="font-weight:600;font-size:15px;color:#1e293b">'+escHtml(u.name)+'</div>'
+'<div style="font-size:13px;color:#64748b;margin-top:2px">'+escHtml(u.email)+'</div>'
+'<div style="font-size:11px;color:#94a3b8;margin-top:4px">Registrerad: '+created+'</div>'
+'</div>'
+'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">'
+'<select id="pendingRole_'+u.id+'" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit;background:#fff">'+roleOpts+'</select>'
+'<button onclick="approveUser('+u.id+')" style="padding:6px 16px;background:#059669;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Godkänn</button>'
+(gUserRole==='systemadmin'?'<button onclick="rejectUser('+u.id+',\''+escHtml(u.name)+'\')" style="padding:6px 16px;background:#ef4444;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Neka</button>':'')
+'</div></div>';
}).join('');
} catch(err){
console.error('loadAdminPending error:', err);
}
}
async function approveUser(id){
const roleSelect = document.getElementById('pendingRole_'+id);
const role = roleSelect ? roleSelect.value : 'saljare';
try {
await fetch('/api/staff.php?id='+id, {
method:'PUT', headers:{'Content-Type':'application/json'},
body: JSON.stringify({approved:1, role:role, active:1})
});
// Skicka välkomstmail
try {
const userRes = await fetch('/api/staff.php?id='+id);
const user = await userRes.json();
if(user && user.email){
await fetch('/api/mail.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({
to: user.email,
subject: 'Välkommen till SolarGroup!',
body: '<h2>Välkommen '+escHtml(user.name)+'!</h2><p>Ditt konto har godkänts. Du kan nu logga in på <a href="https://prodconfig.wenesthosting.com">Solar Sales Suite</a>.</p><p>Din roll: <strong>'+ROLE_LABELS[role]+'</strong></p><br><p>Med vänliga hälsningar,<br>SolarGroup Admin</p>'
})
});
}
} catch(mailErr){ console.warn('Welcome mail failed:', mailErr); }
loadAdminPending();
loadAdminUsers();
} catch(err){ alert('Fel: '+err.message); }
}
async function rejectUser(id, name){
if(gUserRole !== 'systemadmin'){ alert('Endast systemadmin kan neka användare.'); return; }
if(!confirm('Neka och ta bort '+name+'?')) return;
try {
await fetch('/api/staff.php?id='+id, { method:'DELETE' });
loadAdminPending();
} catch(err){ alert('Fel: '+err.message); }
}
async function changeUserRole(id, role){
try {
await fetch('/api/staff.php?id='+id, {
method:'PUT', headers:{'Content-Type':'application/json'},
body: JSON.stringify({role:role})
});
} catch(err){ alert('Fel: '+err.message); }
}
async function toggleUserActive(id, active){
try {
await fetch('/api/staff.php?id='+id, {
method:'PUT', headers:{'Content-Type':'application/json'},
body: JSON.stringify({active:active})
});
loadAdminUsers();
} catch(err){ alert('Fel: '+err.message); }
}
async function deleteUser(id, name){
if(gUserRole !== 'systemadmin'){ alert('Endast systemadmin kan ta bort användare.'); return; }
if(!confirm('Ta bort '+name+'? Detta kan inte ångras.')) return;
try {
await fetch('/api/staff.php?id='+id, { method:'DELETE' });
loadAdminUsers();
} catch(err){ alert('Fel: '+err.message); }
}
function escHtml(s){ return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
// Ladda admin-data när sidan visas
function initAdminPage(){
const searchEl = document.getElementById('adminUserSearch');
if(searchEl) searchEl.value = '';
const roleEl = document.getElementById('adminUserRoleFilter');
if(roleEl) roleEl.value = '';
loadAdminUsers();
loadAdminPending();
}
// Kolla väntande vid start (för badge)
async function checkPendingUsers(){
if(gUserRole !== 'systemadmin' && gUserRole !== 'admin' && gUserRole !== 'saljchef') return;
try {
const res = await fetch('/api/staff.php?approved=0&t='+Date.now());
const pending = await res.json();
const badge = document.getElementById('adminPendingBadge');
if(badge){
if(pending.length > 0){
badge.textContent = pending.length;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
} catch(e){}
}
/* === BILDGENERERING === */
let bgUploadedImage = null;
let bgGeneratedImages = JSON.parse(localStorage.getItem('bgGallery') || '[]');
function handleBgUpload(input) {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
bgUploadedImage = canvas.toDataURL('image/jpeg', 0.85);
document.getElementById('bgPreviewImg').src = bgUploadedImage;
document.getElementById('bgPreview').style.display = 'block';
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
function clearBgUpload() {
bgUploadedImage = null;
document.getElementById('bgPreview').style.display = 'none';
document.getElementById('bgFileInput').value = '';
document.getElementById('bgCameraInput').value = '';
}
// === Bildgenerering — produktval med inbyggda prompts ===
const bgProducts = {
solceller: {
label: 'Solceller', icon: '☀️',
hasCount: true, countLabel: 'Antal paneler', countOptions: [6,8,10,12,14,16,18,20,24,28,32,36,40],
prompt: 'Install solar panels on the roof of this house. Professional installation, realistic mounting system visible.',
subs: [
{label:'Svarta paneler', extra:'All-black premium solar panels with black frames and black backsheet'},
{label:'Blå paneler', extra:'Traditional blue polycrystalline solar panels with silver frames'},
{label:'Integrerade (BIPV)', extra:'Building-integrated photovoltaic roof tiles that replace traditional roofing material, seamless look'},
]
},
takbyte: {
label: 'Takbyte', icon: '🏠',
prompt: 'Replace the roof on this house with new roofing material. Show the complete house with the new roof installed. Wide angle view showing the entire building.',
subs: [
{label:'Betongpannor röda', extra:'Red concrete roof tiles, traditional Scandinavian style'},
{label:'Betongpannor svarta', extra:'Black/dark grey concrete roof tiles, modern Scandinavian look'},
{label:'Plåttak svart', extra:'Standing seam black metal roof, sleek modern appearance'},
{label:'Tegelpannor', extra:'Classic clay/terracotta roof tiles, warm orange-red tones'},
]
},
fonster: {
label: 'Fönster', icon: '🪟',
prompt: 'Replace the windows on this house with new modern energy-efficient windows. Show the entire house with new windows installed.',
subs: [
{label:'Vita träfönster', extra:'White wooden windows with traditional Swedish style, double-pane'},
{label:'Svarta aluminium', extra:'Black aluminum frame windows, large glass surfaces, modern minimalist'},
{label:'Spröjsade', extra:'Windows with glazing bars (spröjs), classic Scandinavian cottage style'},
]
},
fasad: {
label: 'Fasadrenovering', icon: '🏗️',
prompt: 'Renovate the facade of this house. Show the complete building with the new facade finish.',
subs: [
{label:'Vit puts', extra:'Clean white stucco/plaster facade, modern Scandinavian minimalism'},
{label:'Gul träpanel', extra:'Traditional Swedish yellow painted wooden siding (Falu-style)'},
{label:'Grå träpanel', extra:'Modern grey-stained wooden cladding, contemporary Nordic style'},
{label:'Röd Falu', extra:'Classic Falu red (Falu rödfärg) painted wooden facade, iconic Swedish look'},
]
},
laddbox: {
label: 'Laddbox', icon: '⚡',
prompt: 'Add an EV charger/wallbox to this house. Show the charging station mounted on the wall near the driveway or garage.',
subs: [
{label:'Zaptec Go', extra:'Sleek white Zaptec Go wallbox charger mounted on wall'},
{label:'Garo Entity', extra:'Garo Entity Pro wallbox, dark grey, wall-mounted'},
{label:'Easee Home', extra:'Compact Easee Home charger in white, minimalist design'},
]
},
batteri: {
label: 'Batteri', icon: '🔋',
prompt: 'Add a home battery energy storage system to this house. Show the battery unit installed on an interior or exterior wall.',
subs: [
{label:'Huawei LUNA', extra:'Huawei LUNA 2000 battery stack, white modular units'},
{label:'SolarEdge Home', extra:'SolarEdge Home Battery, compact dark grey unit'},
{label:'Tesla Powerwall', extra:'Tesla Powerwall, sleek white wall-mounted unit'},
]
},
varmepump: {
label: 'Värmepump', icon: '🌡️',
prompt: 'Add an air source heat pump to this house. Show the outdoor unit installed on the side of the building.',
subs: [
{label:'Panasonic', extra:'Panasonic Aquarea outdoor unit, white, professional installation'},
{label:'Mitsubishi', extra:'Mitsubishi Ecodan outdoor heat pump unit'},
{label:'Nibe', extra:'Nibe air-to-water heat pump outdoor unit, Swedish brand'},
]
},
kombination: {
label: 'Kombination', icon: '🏡',
prompt: 'Complete renovation of this house showing multiple improvements installed together.',
subs: [
{label:'Sol + Tak', extra:'New roof with solar panels installed on top, complete makeover'},
{label:'Sol + Batteri + Laddbox', extra:'Solar panels on roof, battery storage and EV charger on wall'},
{label:'Helrenovering', extra:'New roof, new windows, new facade, solar panels - complete transformation'},
]
},
};
let bgSelectedProduct = null;
let bgSelectedSub = null;
let bgSelectedCount = null;
function initBgOptions() {
const grid = document.getElementById('bgOptionGrid');
if(!grid) return;
grid.innerHTML = Object.entries(bgProducts).map(([key, p]) =>
'<div onclick="selectBgProduct(\''+key+'\')" id="bgOpt_'+key+'" style="padding:14px;border:2px solid #e5e7eb;border-radius:10px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc">'
+'<div style="font-size:24px;margin-bottom:4px">'+p.icon+'</div>'
+'<div style="font-size:13px;font-weight:600;color:#1a1a1a">'+p.label+'</div>'
+'</div>'
).join('');
}
function selectBgProduct(key) {
bgSelectedProduct = key;
bgSelectedSub = null;
bgSelectedCount = null;
const p = bgProducts[key];
document.getElementById('bgProjectType').value = p.label;
document.getElementById('bgPrompt').value = p.prompt;
// Highlight selected
document.querySelectorAll('#bgOptionGrid > div').forEach(d => {
d.style.borderColor = '#e5e7eb'; d.style.background = '#fafbfc';
});
const sel = document.getElementById('bgOpt_'+key);
if(sel){ sel.style.borderColor = '#024550'; sel.style.background = '#f0fdfa'; }
// Show sub-options
const panel = document.getElementById('bgDetailsPanel');
const subGrid = document.getElementById('bgSubOptions');
if(p.subs && p.subs.length) {
panel.style.display = 'block';
subGrid.innerHTML = p.subs.map((s, i) =>
'<div onclick="selectBgSub('+i+')" id="bgSub_'+i+'" style="padding:10px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc">'
+'<div style="font-size:12px;font-weight:600;color:#1a1a1a">'+s.label+'</div>'
+'</div>'
).join('');
} else {
panel.style.display = 'none';
}
// Show count selector if product has it
const countPanel = document.getElementById('bgCountPanel');
if(p.hasCount && p.countOptions) {
countPanel.style.display = 'block';
document.getElementById('bgCountLabel').textContent = p.countLabel || 'Antal';
document.getElementById('bgCountOptions').innerHTML = p.countOptions.map(n =>
'<div onclick="selectBgCount('+n+')" id="bgCount_'+n+'" style="padding:8px 14px;border:2px solid #e5e7eb;border-radius:8px;cursor:pointer;text-align:center;transition:all .15s;background:#fafbfc;min-width:44px">'
+'<div style="font-size:13px;font-weight:600;color:#1a1a1a">'+n+'</div>'
+'</div>'
).join('');
} else {
countPanel.style.display = 'none';
}
}
function selectBgSub(idx) {
bgSelectedSub = idx;
const p = bgProducts[bgSelectedProduct];
const sub = p.subs[idx];
updateBgPrompt();
document.querySelectorAll('#bgSubOptions > div').forEach(d => {
d.style.borderColor = '#e5e7eb'; d.style.background = '#fafbfc';
});
const sel = document.getElementById('bgSub_'+idx);
if(sel){ sel.style.borderColor = '#024550'; sel.style.background = '#f0fdfa'; }
}
function selectBgCount(n) {
bgSelectedCount = n;
updateBgPrompt();
document.querySelectorAll('#bgCountOptions > div').forEach(d => {
d.style.borderColor = '#e5e7eb'; d.style.background = '#fafbfc';
});
const sel = document.getElementById('bgCount_'+n);
if(sel){ sel.style.borderColor = '#024550'; sel.style.background = '#f0fdfa'; }
}
function updateBgPrompt() {
const p = bgProducts[bgSelectedProduct];
if(!p) return;
let prompt = p.prompt;
if(bgSelectedSub !== null && p.subs[bgSelectedSub]) {
prompt += ' ' + p.subs[bgSelectedSub].extra;
}
if(bgSelectedCount && p.hasCount) {
const rows = bgSelectedCount <= 8 ? 2 : bgSelectedCount <= 18 ? 2 : 3;
const cols = Math.ceil(bgSelectedCount / rows);
prompt += ' CRITICAL: Place EXACTLY ' + bgSelectedCount + ' solar panels on the roof — arranged as ' + rows + ' rows x ' + cols + ' columns. There must be precisely ' + bgSelectedCount + ' panels total, not more and not less.';
}
document.getElementById('bgPrompt').value = prompt;
}
// Init options on page load
setTimeout(initBgOptions, 100);
async function generateBgImage() {
const prompt = document.getElementById('bgPrompt').value.trim();
const projectType = document.getElementById('bgProjectType').value;
const extra = document.getElementById('bgExtraPrompt')?.value?.trim() || '';
if (!prompt || !projectType) { alert('Välj en produkttyp först'); return; }
const fullPrompt = bgUploadedImage
? prompt + (extra ? '. ' + extra : '')
: prompt + (extra ? '. Additional details: ' + extra : '') + '. Show the ENTIRE house from street level, wide angle, full building visible. Photorealistic, professional photography, Scandinavian architecture.';
const btn = document.getElementById('bgGenerateBtn');
btn.disabled = true;
btn.textContent = 'Genererar...';
document.getElementById('bgPlaceholder').style.display = 'none';
document.getElementById('bgResult').style.display = 'none';
document.getElementById('bgLoading').style.display = 'flex';
try {
const provider = document.getElementById('bgProvider').value;
const res = await fetch('/api/generate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: fullPrompt,
projectType: projectType,
baseImage: bgUploadedImage,
provider: provider
})
});
const data = await res.json();
if (data.error) throw new Error(data.error);
document.getElementById('bgLoading').style.display = 'none';
document.getElementById('bgResult').style.display = 'block';
document.getElementById('bgResultImg').src = data.base64 || data.imageUrl;
bgGeneratedImages.unshift({
url: data.imageUrl,
base64: data.base64,
prompt: projectType + (bgSelectedSub !== null ? ' - ' + bgProducts[bgSelectedProduct].subs[bgSelectedSub].label : ''),
type: projectType,
date: new Date().toISOString().split('T')[0]
});
if (bgGeneratedImages.length > 20) bgGeneratedImages = bgGeneratedImages.slice(0, 20);
try { localStorage.setItem('bgGallery', JSON.stringify(bgGeneratedImages)); } catch(e) {}
renderBgGallery();
} catch (err) {
document.getElementById('bgLoading').style.display = 'none';
document.getElementById('bgPlaceholder').style.display = 'flex';
alert('Fel: ' + err.message);
} finally {
btn.disabled = false;
btn.textContent = 'Generera bild';
}
}
function downloadBgImage() {
const img = document.getElementById('bgResultImg');
if (!img.src) return;
const a = document.createElement('a');
a.href = img.src;
a.download = 'visualization-' + Date.now() + '.png';
a.click();
}
function resetBgResult() {
document.getElementById('bgResult').style.display = 'none';
document.getElementById('bgPlaceholder').style.display = 'flex';
}
function renderBgGallery() {
const grid = document.getElementById('bgGalleryGrid');
const container = document.getElementById('bgGallery');
if (!bgGeneratedImages.length) { container.style.display = 'none'; return; }
container.style.display = 'block';
grid.innerHTML = bgGeneratedImages.map((img, i) =>
'<div style="position:relative;cursor:pointer" onclick="document.getElementById(\'bgResult\').style.display=\'block\';document.getElementById(\'bgPlaceholder\').style.display=\'none\';document.getElementById(\'bgLoading\').style.display=\'none\';document.getElementById(\'bgResultImg\').src=\'' + (img.base64 || img.url) + '\'">' +
'<img src="' + (img.base64 || img.url) + '" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:8px;border:1px solid #e5e7eb">' +
'<div style="position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,.7));color:#fff;padding:6px 8px;border-radius:0 0 8px 8px;font-size:10px">' + img.type + ' • ' + img.date + '</div>' +
'</div>'
).join('');
}
renderBgGallery();
// === SETTINGS TABS ===
function switchSettingsTab(tab) {
document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
document.querySelector('[data-settings-tab="' + tab + '"]').classList.add('active');
document.getElementById('settings-' + tab).classList.add('active');
if (tab === 'google') loadGoogleSettings();
if (tab === 'ai') loadAiSettings();
if (tab === 'monday') loadMondaySettings(); if (tab === 'valuta') loadCurrencySettings();
}
// === GOOGLE SETTINGS ===
async function loadGoogleSettings() {
const uriEl = document.getElementById('googleRedirectUri');
if (uriEl) uriEl.textContent = window.location.origin;
try {
const res = await fetch('/api/settings.php?keys=google_client_id,google_client_secret,google_maps_key');
const data = await res.json();
if (data.settings) {
document.getElementById('settGoogleClientId').value = data.settings.google_client_id || '';
document.getElementById('settGoogleClientSecret').value = data.settings.google_client_secret_set ? '••••••••' : '';
if (data.settings.google_client_secret_set) {
document.getElementById('settGoogleSecretStatus').textContent = 'Konfigurerad';
}
document.getElementById('settGoogleMapsKey').value = data.settings.google_maps_key || '';
}
} catch(e) {}
}
async function saveGoogleSettings() {
const settings = {};
const clientId = document.getElementById('settGoogleClientId').value.trim();
const secret = document.getElementById('settGoogleClientSecret').value.trim();
const mapsKey = document.getElementById('settGoogleMapsKey').value.trim();
if (clientId) settings.google_client_id = clientId;
if (secret && !secret.startsWith('••')) settings.google_client_secret = secret;
if (mapsKey) settings.google_maps_key = mapsKey;
try {
const res = await fetch('/api/settings.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({settings})
});
const data = await res.json();
const msg = document.getElementById('googleSettingsMsg');
if (data.success) {
msg.innerHTML = '<span style="color:#10b981;font-weight:600">Sparat!</span>';
setTimeout(() => msg.textContent = '', 3000);
} else {
msg.innerHTML = '<span style="color:#ef4444">' + (data.error || 'Fel') + '</span>';
}
} catch(e) {
document.getElementById('googleSettingsMsg').innerHTML = '<span style="color:#ef4444">Nätverksfel</span>';
}
}
// === GOOGLE OAUTH + GMAIL + CALENDAR ===
const GOOGLE_SCOPES = 'openid email profile https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.send https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly';
const LOGIN_SCOPES = 'openid email profile';
async function initGoogleAuth() {
// Load GIS library if not present
if (!window.google?.accounts?.oauth2) {
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
document.head.appendChild(script);
await new Promise(r => script.onload = r);
}
// Get client ID from settings
try {
const res = await fetch('/api/settings.php?keys=google_client_id');
const data = await res.json();
return data.settings?.google_client_id || '';
} catch(e) { return ''; }
}
async function googleLogin() {
const clientId = await initGoogleAuth();
if (!clientId) {
alert('Google Client ID inte konfigurerat. Gå till Inställningar → Google API.');
return;
}
const client = google.accounts.oauth2.initCodeClient({
client_id: clientId,
scope: GOOGLE_SCOPES,
ux_mode: 'popup',
redirect_uri: window.location.origin,
callback: async (response) => {
if (response.error) { alert('Google login misslyckades: ' + response.error); return; }
try {
const res = await fetch('/api/google-token.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'exchange',
code: response.code,
redirectUri: window.location.origin
})
});
const data = await res.json();
if (data.error) { const errEl = document.getElementById("loginError"); const pendEl = document.getElementById("loginPending"); if(errEl) errEl.style.display="none"; if(pendEl) pendEl.style.display="none"; if(data.pending) { if(pendEl){ pendEl.textContent=data.error; pendEl.style.display="block"; } } else { if(errEl){ errEl.textContent=data.error; errEl.style.display="block"; } } return; }
gAccessToken = data.accessToken;
gTokenExpiry = Date.now() + (data.expiresIn * 1000);
gStaffId = data.user.id;
gUserEmail = data.user.email;
gUserName = data.user.name;
gUserAvatar = data.user.avatar_url || '';
gUserRole = data.user.role || '';
gMailSignature = data.user.mail_signature || '';
sessionStorage.setItem('gAccessToken', gAccessToken);
sessionStorage.setItem('gTokenExpiry', gTokenExpiry);
sessionStorage.setItem('gStaffId', gStaffId);
sessionStorage.setItem('gUserEmail', gUserEmail);
sessionStorage.setItem('gUserName', gUserName);
sessionStorage.setItem('gUserAvatar', gUserAvatar);
sessionStorage.setItem('gUserRole', gUserRole);
localStorage.setItem('mailSignature', gMailSignature);
updateAuthUI();
// Auto-load mail and calendar
if (document.getElementById('page-inkorg').classList.contains('active')) loadGmailInbox();
if (document.getElementById('page-kalender').classList.contains('active')) loadCalendarEvents();
} catch(e) { alert('Anslutningsfel: ' + e.message); }
}
});
client.requestCode();
}
async function ensureToken() {
if (!gAccessToken) return false;
if (Date.now() > gTokenExpiry - 60000) {
// Token expired or about to expire, refresh
try {
const res = await fetch('/api/google-token.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'refresh', staffId: gStaffId })
});
const data = await res.json();
if (data.accessToken) {
gAccessToken = data.accessToken;
gTokenExpiry = Date.now() + (data.expiresIn * 1000);
sessionStorage.setItem('gAccessToken', gAccessToken);
sessionStorage.setItem('gTokenExpiry', gTokenExpiry);
return true;
}
} catch(e) {}
return false;
}
return true;
}
function updateAuthUI() {
const loginRequired = !gAccessToken;
// Mail page
const mailLogin = document.getElementById('mailLogin');
const mailClient = document.getElementById('mailClient');
if (loginRequired) {
if (mailLogin) { mailLogin.style.display = 'block'; mailLogin.innerHTML = '<div style="text-align:center;padding:40px"><p style="font-size:16px;color:#334155;margin-bottom:16px">Logga in med Google för att ansluta din e-post och kalender</p><button onclick="googleLogin()" style="padding:12px 28px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit">Logga in med Google</button></div>'; }
if (mailClient) mailClient.style.display = 'none';
} else {
if (mailLogin) mailLogin.style.display = 'none';
if (mailClient) mailClient.style.display = 'block';
}
// Calendar page
const calSetup = document.getElementById('calSetup');
if (calSetup) {
if (loginRequired) {
calSetup.innerHTML = '<div style="text-align:center;padding:20px"><p style="font-size:16px;color:#334155;margin-bottom:16px">Logga in med Google för att visa din kalender</p><button onclick="googleLogin()" style="padding:12px 28px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit">Logga in med Google</button></div>';
} else {
calSetup.innerHTML = '<div style="display:flex;align-items:center;gap:12px"><div style="width:36px;height:36px;border-radius:50%;background:#f0fdf4;display:flex;align-items:center;justify-content:center"><span style="color:#10b981;font-size:18px">✓</span></div><div><strong style="font-size:14px">' + gUserName + '</strong><br><span style="font-size:12px;color:#64748b">' + gUserEmail + '</span></div></div>';
}
}
}
// === GMAIL CLIENT ===
let gmailLabel = 'INBOX';
let gmailNextPage = null;
let gmailCurrentId = null;
async function loadGmailInbox(pageToken) {
if (!await ensureToken()) { updateAuthUI(); return; }
const list = document.getElementById('mailList');
const loading = document.getElementById('mailListLoading');
if (loading) loading.style.display = 'block';
const search = document.getElementById('mailSearch')?.value || '';
const params = { action: 'list', accessToken: gAccessToken, label: gmailLabel, maxResults: 25 };
if (search) params.q = search;
if (pageToken) params.pageToken = pageToken;
try {
const res = await fetch('/api/gmail-proxy.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params)
});
const data = await res.json();
if (data.error === 'TOKEN_EXPIRED') { gAccessToken = ''; updateAuthUI(); return; }
if (data.error) throw new Error(data.error);
gmailNextPage = data.nextPageToken;
const count = document.getElementById('mailCount');
if (count) count.textContent = (data.resultSizeEstimate || 0) + ' meddelanden';
if (list) {
list.innerHTML = data.messages.map(m => {
const fromName = m.from.replace(/<.*>/, '').trim() || m.from;
const isActive = m.id === gmailCurrentId;
return '<div onclick="readGmail(\'' + m.id + '\')" style="padding:12px 16px;border-bottom:1px solid #f1f5f9;cursor:pointer;background:' + (isActive ? '#eff6ff' : (m.unread ? '#fefce8' : '#fff')) + ';transition:background .15s' + '">'
+ '<div style="display:flex;justify-content:space-between;align-items:start">'
+ '<strong style="font-size:13px;font-weight:' + (m.unread ? '700' : '500') + ';color:#1a1a1a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px">' + escHtml(fromName) + '</strong>'
+ '<span style="font-size:11px;color:#94a3b8;white-space:nowrap;margin-left:8px">' + formatMailDate(m.timestamp) + '</span>'
+ '</div>'
+ '<div style="font-size:13px;color:#334155;font-weight:' + (m.unread ? '600' : '400') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px">' + escHtml(m.subject) + '</div>'
+ '<div style="font-size:12px;color:#94a3b8;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:2px">' + escHtml(m.snippet) + '</div>'
+ '</div>';
}).join('');
}
// Pagination
const pag = document.getElementById('mailPagination');
if (pag) {
pag.innerHTML = gmailNextPage
? '<button onclick="loadGmailInbox(\'' + gmailNextPage + '\')" style="padding:6px 16px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit">Visa fler</button>'
: '';
}
} catch(e) {
if (list) list.innerHTML = '<div style="padding:20px;text-align:center;color:#ef4444;font-size:13px">' + e.message + '</div>';
}
if (loading) loading.style.display = 'none';
}
async function readGmail(id) {
if (!await ensureToken()) return;
gmailCurrentId = id;
// Highlight in list
loadGmailInbox();
const content = document.getElementById('mailReaderContent');
const empty = document.getElementById('mailReaderEmpty');
if (empty) empty.style.display = 'none';
if (content) { content.style.display = 'block'; content.innerHTML = '<div style="text-align:center;padding:40px;color:#94a3b8">Laddar...</div>'; }
try {
const res = await fetch('/api/gmail-proxy.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'read', accessToken: gAccessToken, id })
});
const data = await res.json();
if (data.error) throw new Error(data.error);
const fromName = data.from.replace(/<.*>/, '').trim();
const fromEmail = (data.from.match(/<(.+)>/) || ['', data.from])[1];
const dateStr = new Date(data.timestamp * 1000).toLocaleString('sv-SE');
content.innerHTML = '<div style="margin-bottom:20px">'
+ '<h2 style="font-size:18px;font-weight:700;color:#1a1a1a;margin-bottom:12px">' + escHtml(data.subject) + '</h2>'
+ '<div style="display:flex;justify-content:space-between;align-items:start;flex-wrap:wrap;gap:8px">'
+ '<div><strong style="font-size:14px">' + escHtml(fromName) + '</strong><br><span style="font-size:12px;color:#64748b">' + escHtml(fromEmail) + '</span>'
+ (data.cc ? '<br><span style="font-size:12px;color:#94a3b8">Cc: ' + escHtml(data.cc) + '</span>' : '') + '</div>'
+ '<span style="font-size:12px;color:#94a3b8">' + dateStr + '</span>'
+ '</div>'
+ '<div style="display:flex;gap:6px;margin-top:12px">'
+ '<button onclick="mailReply(\'' + id + '\')" style="padding:6px 14px;background:#024550;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit">Svara</button>'
+ '<button onclick="mailForward(\'' + id + '\')" style="padding:6px 14px;background:#f1f5f9;color:#334155;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit">Vidarebefordra</button>'
+ '<button onclick="mailTrash(\'' + id + '\')" style="padding:6px 14px;background:#fef2f2;color:#991b1b;border:1px solid #fecaca;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit">Radera</button>'
+ '</div>'
+ '</div>'
+ (data.attachments.length ? '<div style="padding:8px 12px;background:#f8fafc;border-radius:8px;margin-bottom:12px;border:1px solid #e5e7eb"><span style="font-size:12px;font-weight:600;color:#334155">Bilagor: </span>' + data.attachments.map(a => '<span style="font-size:12px;color:#3b82f6">' + escHtml(a.filename) + ' (' + formatSize(a.size) + ')</span>').join(', ') + '</div>' : '')
+ '<div style="border-top:1px solid #e5e7eb;padding-top:16px">'
+ '<iframe id="mailBodyFrame" srcdoc="" style="width:100%;min-height:400px;border:none" sandbox="allow-same-origin"></iframe>'
+ '</div>';
// Set iframe content
const iframe = document.getElementById('mailBodyFrame');
if (iframe) {
const bodyContent = data.bodyHtml || ('<pre style="white-space:pre-wrap;font-family:sans-serif">' + escHtml(data.bodyText) + '</pre>');
iframe.srcdoc = '<html><head><style>body{font-family:sans-serif;font-size:14px;color:#1a1a1a;margin:0;padding:0;line-height:1.5}img{max-width:100%}a{color:#0284c7}</style></head><body>' + bodyContent + '</body></html>';
}
// Store for reply
window._lastMailData = data;
} catch(e) {
content.innerHTML = '<div style="padding:20px;color:#ef4444">' + e.message + '</div>';
}
}
function mailCompose() {
document.getElementById('composeTitle').textContent = 'Nytt meddelande';
document.getElementById('composeTo').value = '';
document.getElementById('composeCc').value = '';
document.getElementById('composeSubject').value = '';
document.getElementById('composeBody').value = '';
document.getElementById('composeReplyUid').value = '0';
document.getElementById('composeReplyFolder').value = '';
document.getElementById('composeReplyTo').value = '';
document.getElementById('composeSignature').value = gMailSignature || '--\nMed vänliga hälsningar\n' + gUserName + '\nSolargroup';
document.getElementById('mailComposeModal').style.display = 'flex';
}
function mailReply(id) {
const d = window._lastMailData;
if (!d) return;
document.getElementById('composeTitle').textContent = 'Svara';
document.getElementById('composeTo').value = d.replyTo || d.from;
document.getElementById('composeCc').value = '';
document.getElementById('composeSubject').value = (d.subject.startsWith('Re:') ? '' : 'Re: ') + d.subject;
document.getElementById('composeBody').value = '\n\n--- Ursprungligt meddelande ---\nFrån: ' + d.from + '\nDatum: ' + new Date(d.timestamp*1000).toLocaleString('sv-SE') + '\n\n' + (d.bodyText || '');
document.getElementById('composeReplyUid').value = id;
document.getElementById('composeReplyTo').value = d.messageId || '';
document.getElementById('composeSignature').value = gMailSignature || '--\nMed vänliga hälsningar\n' + gUserName;
document.getElementById('mailComposeModal').style.display = 'flex';
}
function mailForward(id) {
const d = window._lastMailData;
if (!d) return;
document.getElementById('composeTitle').textContent = 'Vidarebefordra';
document.getElementById('composeTo').value = '';
document.getElementById('composeCc').value = '';
document.getElementById('composeSubject').value = 'Fwd: ' + d.subject;
document.getElementById('composeBody').value = '\n\n--- Vidarebefordrat meddelande ---\nFrån: ' + d.from + '\nTill: ' + d.to + '\nDatum: ' + new Date(d.timestamp*1000).toLocaleString('sv-SE') + '\nÄmne: ' + d.subject + '\n\n' + (d.bodyText || '');
document.getElementById('composeReplyUid').value = '0';
document.getElementById('composeReplyTo').value = '';
document.getElementById('composeSignature').value = gMailSignature || '--\nMed vänliga hälsningar\n' + gUserName;
document.getElementById('mailComposeModal').style.display = 'flex';
}
async function mailSend() {
if (!await ensureToken()) return;
const btn = document.getElementById('composeSendBtn');
btn.textContent = 'Skickar...'; btn.disabled = true;
try {
const sig = document.getElementById('composeSignature').value.trim();
// Save signature for next time
if (sig !== gMailSignature) {
gMailSignature = sig;
localStorage.setItem('mailSignature', sig);
fetch('/api/gmail-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'signature', staffId: gStaffId, signature: sig })
});
}
const replyTo = document.getElementById('composeReplyTo').value;
const res = await fetch('/api/gmail-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'send', accessToken: gAccessToken,
to: document.getElementById('composeTo').value,
cc: document.getElementById('composeCc').value,
subject: document.getElementById('composeSubject').value,
body: document.getElementById('composeBody').value,
signature: sig,
inReplyTo: replyTo,
references: replyTo,
threadId: replyTo ? (window._lastMailData?.threadId || '') : '',
})
});
const data = await res.json();
if (data.error) throw new Error(data.error);
mailCloseCompose();
loadGmailInbox();
} catch(e) {
alert('Kunde inte skicka: ' + e.message);
}
btn.textContent = 'Skicka'; btn.disabled = false;
}
async function mailTrash(id) {
if (!await ensureToken()) return;
try {
await fetch('/api/gmail-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'trash', accessToken: gAccessToken, id })
});
gmailCurrentId = null;
document.getElementById('mailReaderContent').style.display = 'none';
document.getElementById('mailReaderEmpty').style.display = 'flex';
loadGmailInbox();
} catch(e) { alert(e.message); }
}
function mailCloseCompose() { document.getElementById('mailComposeModal').style.display = 'none'; }
function mailRefresh() { loadGmailInbox(); }
function mailSwitchFolder() {
gmailLabel = document.getElementById('mailFolderSelect').value;
gmailCurrentId = null;
document.getElementById('mailReaderContent').style.display = 'none';
document.getElementById('mailReaderEmpty').style.display = 'flex';
loadGmailInbox();
}
async function loadGmailLabels() {
if (!await ensureToken()) return;
try {
const res = await fetch('/api/gmail-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ action: 'labels', accessToken: gAccessToken })
});
const data = await res.json();
const select = document.getElementById('mailFolderSelect');
if (select && data.labels) {
const labelNames = { INBOX: 'Inkorg', SENT: 'Skickat', DRAFTS: 'Utkast', TRASH: 'Papperskorg', SPAM: 'Skräppost', STARRED: 'Stjärnmärkt', IMPORTANT: 'Viktigt' };
const show = data.labels.filter(l => ['INBOX','SENT','DRAFTS','TRASH','SPAM','STARRED','IMPORTANT'].includes(l.id) || l.type === 'user');
select.innerHTML = show.map(l => '<option value="' + l.id + '"' + (l.id === gmailLabel ? ' selected' : '') + '>' + (labelNames[l.id] || l.name) + '</option>').join('');
}
} catch(e) {}
}
function mailDisconnect() {
gAccessToken = '';
sessionStorage.removeItem('gAccessToken');
sessionStorage.removeItem('gTokenExpiry');
updateAuthUI();
}
// === CALENDAR CLIENT ===
let calEvents = [];
async function loadCalendarEvents() {
if (!await ensureToken()) { updateAuthUI(); return; }
const listEl = document.getElementById('calEventsList') || document.getElementById('callbackList');
try {
const res = await fetch('/api/calendar-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'list', accessToken: gAccessToken,
timeMin: new Date().toISOString(),
timeMax: new Date(Date.now() + 30*86400000).toISOString(),
maxResults: 50
})
});
const data = await res.json();
if (data.error === 'TOKEN_EXPIRED') { gAccessToken = ''; updateAuthUI(); return; }
calEvents = data.events || [];
renderCalendarEvents();
} catch(e) {}
}
function renderCalendarEvents() {
const embed = document.getElementById('calEmbed');
const listEl = document.getElementById('calEventsList');
// Show event list
if (listEl) {
if (calEvents.length === 0) {
listEl.innerHTML = '<div style="padding:16px;background:#f8fafc;border-radius:10px;color:#94a3b8;text-align:center;font-size:13px">Inga kommande händelser</div>';
} else {
const eventColors = ['#16a34a','#3b82f6','#eab308','#a855f7','#ef4444','#06b6d4'];
listEl.innerHTML = calEvents.slice(0, 20).map((ev, i) => {
const start = ev.allDay ? ev.start : new Date(ev.start).toLocaleString('sv-SE', {weekday:'short', month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
const color = eventColors[i % eventColors.length];
return '<div style="display:flex;align-items:center;gap:12px;padding:10px 12px;background:#fff;border-radius:8px;border:1px solid #f1f5f9">'
+ '<div style="width:8px;height:8px;border-radius:50%;background:' + color + ';flex-shrink:0"></div>'
+ '<div style="flex:1;min-width:0">'
+ '<div style="font-size:13px;font-weight:600;color:#1a1a1a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(ev.summary) + '</div>'
+ '<div style="font-size:12px;color:#64748b">' + start + (ev.location ? ' · ' + escHtml(ev.location) : '') + '</div>'
+ '</div>'
+ '<a href="' + (ev.htmlLink || '#') + '" target="_blank" style="font-size:11px;color:#3b82f6;text-decoration:none;white-space:nowrap">Öppna</a>'
+ '</div>';
}).join('');
}
}
// Also render in the calendar embed area
if (embed) {
embed.style.display = 'block';
const calendarHtml = buildCalendarView();
embed.innerHTML = calendarHtml;
}
}
function buildCalendarView() {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const monthNames = ['Januari','Februari','Mars','April','Maj','Juni','Juli','Augusti','September','Oktober','November','December'];
const dayNames = ['Mån','Tis','Ons','Tor','Fre','Lör','Sön'];
const startDay = (firstDay + 6) % 7;
let html = '<div style="padding:16px">';
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3 style="font-size:18px;font-weight:700">' + monthNames[month] + ' ' + year + '</h3></div>';
html += '<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:1px;background:#e5e7eb;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden">';
dayNames.forEach(d => { html += '<div style="padding:8px 4px;text-align:center;font-size:11px;font-weight:600;color:#64748b;background:#f8fafc">' + d + '</div>'; });
for (let i = 0; i < startDay; i++) html += '<div style="padding:8px;background:#fafafa;min-height:60px"></div>';
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = year + '-' + String(month+1).padStart(2,'0') + '-' + String(day).padStart(2,'0');
const isToday = day === now.getDate();
const dayEvents = calEvents.filter(e => (e.start || '').startsWith(dateStr));
html += '<div style="padding:4px 6px;background:' + (isToday ? '#eff6ff' : '#fff') + ';min-height:60px;vertical-align:top">';
html += '<div style="font-size:12px;font-weight:' + (isToday ? '700' : '500') + ';color:' + (isToday ? '#024550' : '#334155') + ';margin-bottom:2px">' + day + '</div>';
dayEvents.slice(0, 2).forEach(e => {
html += '<div style="font-size:10px;padding:1px 4px;background:#dcfce7;border-radius:3px;margin-bottom:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#166534" title="' + escHtml(e.summary) + '">' + escHtml(e.summary) + '</div>';
});
if (dayEvents.length > 2) html += '<div style="font-size:10px;color:#94a3b8">+' + (dayEvents.length - 2) + ' till</div>';
html += '</div>';
}
html += '</div></div>';
return html;
}
async function createCalendarEvent(summary, description, location, startTime, endTime) {
if (!await ensureToken()) return null;
try {
const res = await fetch('/api/calendar-proxy.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
action: 'create', accessToken: gAccessToken,
summary, description, location, startTime, endTime
})
});
const data = await res.json();
if (data.success) { loadCalendarEvents(); return data.event; }
throw new Error(data.error);
} catch(e) { alert('Kunde inte skapa event: ' + e.message); return null; }
}
// === HELPERS ===
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function formatMailDate(ts) {
if (!ts) return '';
const d = new Date(ts * 1000);
const now = new Date();
if (d.toDateString() === now.toDateString()) return d.toLocaleTimeString('sv-SE', {hour:'2-digit', minute:'2-digit'});
if (d.getFullYear() === now.getFullYear()) return d.toLocaleDateString('sv-SE', {day:'numeric', month:'short'});
return d.toLocaleDateString('sv-SE');
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes/1024).toFixed(0) + ' KB';
return (bytes/1048576).toFixed(1) + ' MB';
}
// Auto-load on page navigation
// Mail/calendar hooks injected into existing navigateTo override below
// Init on load
updateAuthUI();
// === PERSONAL ===
var currentStaffData = null;
var staffDT = null;
var allStaffData = [];
async function loadStaff() {
var role = '';
var rf = document.getElementById('staffRoleFilter');
if (rf) role = rf.value || '';
var url = '/api/staff.php?active=0';
if (role) url += '&role=' + role;
try {
var res = await fetch(url);
var staff = await res.json();
if (!Array.isArray(staff)) staff = [];
allStaffData = staff;
var cnt = document.getElementById('staffCount');
if (cnt) cnt.textContent = staff.length + ' person' + (staff.length !== 1 ? 'er' : '');
buildStaffDT(staff);
} catch (e) {
console.error('loadStaff error:', e);
}
}
function buildStaffDT(staff) {
var rows = staff.map(function(s) {
var roleLbl = roleLabels[s.role] || s.role || '';
var roleClr = roleColors[s.role] || 'gray';
var sales = parseFloat(s.total_sales || 0);
return [
s.id,
s.name || '',
s.email || '',
roleLbl,
s.active == 1 ? 'Aktiv' : 'Inaktiv',
sales,
roleClr
];
});
if (staffDT) { staffDT.destroy(); staffDT = null; }
staffDT = $('#staffDT').DataTable({
data: rows,
columns: [
{ title:'Namn', render: function(d,t,r){ return '<strong>' + r[1] + '</strong>'; } },
{ title:'E-post', render: function(d,t,r){ return r[2]; } },
{ title:'Roll', render: function(d,t,r){ return '<span class="status-badge ' + r[6] + '">' + r[3] + '</span>'; } },
{ title:'Status', render: function(d,t,r){ return r[4] === 'Aktiv' ? '<span class="status-badge green">Aktiv</span>' : '<span class="status-badge gray">Inaktiv</span>'; }, width:'80px' },
{ title:'Försäljning', className:'dt-right', render: function(d,t,r){ return r[5] ? r[5].toLocaleString('sv-SE') + ' kr' : '-'; } }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ personal', infoEmpty:'Ingen personal',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Ingen personal hittades'
},
pageLength: 50,
lengthMenu: [25, 50, 100],
order: [[0,'asc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showStaffDetail(data[0]); });
}
});
}
// --- Staff Detail View ---
async function showStaffDetail(id) {
try {
const res = await fetch(STAFF_API + '?id=' + id);
const staff = await res.json();
if (staff.error) return;
currentStaffData = staff;
document.getElementById('staffListView').style.display = 'none';
document.getElementById('staffDetailView').style.display = 'block';
document.getElementById('staffDetailName').textContent = staff.name;
document.getElementById('staffDetailRole').textContent = roleLabels[staff.role] || staff.role;
document.getElementById('staffDetailRole').className = 'status-badge ' + (roleColors[staff.role] || 'gray');
switchStaffTab('info');
} catch(e) { console.error('showStaffDetail error:', e); }
}
function closeStaffDetail() {
document.getElementById('staffDetailView').style.display = 'none';
document.getElementById('staffListView').style.display = 'block';
currentStaffData = null;
}
function switchStaffTab(tab) {
document.querySelectorAll('.staff-tab').forEach(function(btn) {
var isActive = btn.dataset.tab === tab;
btn.style.borderBottomColor = isActive ? '#024550' : 'transparent';
btn.style.color = isActive ? '#024550' : '#64748b';
});
var panels = {info:'staffTabInfo',loner:'staffTabLoner',salj:'staffTabSalj',utveckling:'staffTabUtveckling',affarslista:'staffTabAffarslista'};
Object.keys(panels).forEach(function(k){ document.getElementById(panels[k]).style.display = k === tab ? 'block' : 'none'; });
if (!currentStaffData) return;
var sid = currentStaffData.id;
if (tab === 'info') renderStaffInfo(currentStaffData);
else if (tab === 'loner') loadStaffLoner(sid);
else if (tab === 'salj') loadStaffMeetings(sid, 'salj_samtal');
else if (tab === 'utveckling') loadStaffMeetings(sid, 'utvecklings_samtal');
else if (tab === 'affarslista') loadStaffDeals(sid);
}
function staffInfoField(label, value) {
return '<div><div style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px">' + label + '</div><div style="font-size:14px;color:#1a1a1a;font-weight:500">' + (value || '-') + '</div></div>';
}
function renderStaffInfo(s) {
document.getElementById('staffTabInfo').innerHTML =
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:800px">' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Personuppgifter</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Namn', s.name) +
staffInfoField('E-post', s.email) +
staffInfoField('Telefon', s.phone) +
staffInfoField('Titel', s.title) +
staffInfoField('Personnummer', s.personnummer) +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Anställning</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Roll', roleLabels[s.role] || s.role) +
staffInfoField('Startdatum', s.start_date) +
staffInfoField('Grundlön', s.grundlon ? parseFloat(s.grundlon).toLocaleString('sv-SE') + ' kr' : null) +
staffInfoField('Skattesats', (s.skatt_procent || '30') + '%') +
staffInfoField('Status', s.active == 1 ? 'Aktiv' : 'Inaktiv') +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Adress</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Adress', s.address) +
staffInfoField('Postnummer', s.zip) +
staffInfoField('Stad', s.city) +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">System</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Google', s.google_id ? '✓ Kopplad' : 'Ej kopplad') +
staffInfoField('Senast inloggning', s.last_login ? new Date(s.last_login).toLocaleDateString('sv-SE') : null) +
staffInfoField('Skapad', s.created_at ? new Date(s.created_at).toLocaleDateString('sv-SE') : null) +
'</div>' +
'<button onclick="editStaff(' + s.id + ')" style="margin-top:16px;padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">✎ Redigera</button>' +
'</div></div>';
}
// --- Löner ---
async function loadStaffLoner(staffId) {
var panel = document.getElementById('staffTabLoner');
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar löner...</div>';
try {
var res = await fetch('api/salary.php?action=my&staff_id=' + staffId);
var records = await res.json();
if (!records || !records.length) {
panel.innerHTML = '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga lönebesked registrerade</div>';
return;
}
var html = '<div style="max-width:900px">';
records.forEach(function(r) {
var brutto = parseFloat(r.brutto || 0);
var netto = parseFloat(r.netto || 0);
var skatt = parseFloat(r.skatt || 0);
html += '<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:16px 20px;margin-bottom:12px">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<div style="font-size:16px;font-weight:700;color:#024550">' + r.period + '</div>' +
'<div style="display:flex;gap:20px;font-size:13px">' +
'<span>Brutto: <strong>' + brutto.toLocaleString('sv-SE') + ' kr</strong></span>' +
'<span>Skatt: <strong style="color:#ef4444">' + skatt.toLocaleString('sv-SE') + ' kr</strong></span>' +
'<span>Netto: <strong style="color:#10b981">' + netto.toLocaleString('sv-SE') + ' kr</strong></span>' +
'</div></div>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;font-size:12px;color:#64748b">' +
'<div>Grundlön: ' + parseFloat(r.grundlon||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Provision: ' + parseFloat(r.provision||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Milersättning: ' + parseFloat(r.milersattning||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Traktamente: ' + parseFloat(r.traktamente||0).toLocaleString('sv-SE') + ' kr</div>' +
(r.ovriga_tillagg && parseFloat(r.ovriga_tillagg) ? '<div>Övriga: ' + parseFloat(r.ovriga_tillagg).toLocaleString('sv-SE') + ' kr</div>' : '') +
'</div>';
if (r.provisions && r.provisions.length) {
html += '<div style="margin-top:10px;padding-top:10px;border-top:1px solid #f1f5f9">' +
'<div style="font-size:11px;color:#94a3b8;margin-bottom:4px">Provisionsdetaljer:</div>';
r.provisions.forEach(function(p) {
html += '<div style="font-size:12px;display:flex;justify-content:space-between;padding:2px 0"><span>' + (p.description || p.deal_title || '#' + p.deal_id) + '</span><span style="font-weight:600">' + parseFloat(p.amount).toLocaleString('sv-SE') + ' kr</span></div>';
});
html += '</div>';
}
html += '</div>';
});
html += '</div>';
panel.innerHTML = html;
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
// --- Möten (Sälj samtal & Utvecklings samtal) ---
async function loadStaffMeetings(staffId, type) {
var panelId = type === 'salj_samtal' ? 'staffTabSalj' : 'staffTabUtveckling';
var panel = document.getElementById(panelId);
var label = type === 'salj_samtal' ? 'Sälj samtal' : 'Utvecklings samtal';
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar...</div>';
try {
var res = await fetch('api/staff-meetings.php?staff_id=' + staffId + '&type=' + type);
var meetings = await res.json();
var html = '<div style="max-width:800px">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">' +
'<h3 style="font-size:16px;font-weight:700;color:#024550;margin:0">' + label + '</h3>' +
'<button onclick="showMeetingForm(' + staffId + ',\'' + type + '\')" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">+ Nytt samtal</button></div>';
if (!meetings || !meetings.length) {
html += '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga ' + label.toLowerCase() + ' registrerade</div>';
} else {
meetings.forEach(function(m) {
var actions = '';
if (m.action_points) {
try {
var pts = JSON.parse(m.action_points);
if (Array.isArray(pts)) actions = pts.map(function(a){ return '<li style="font-size:12px;margin-bottom:4px">' + a + '</li>'; }).join('');
} catch(e) { actions = '<li style="font-size:12px">' + m.action_points + '</li>'; }
}
html += '<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:16px 20px;margin-bottom:12px">' +
'<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px">' +
'<div><div style="font-size:14px;font-weight:600;color:#1a1a1a">' + (m.title || label) + '</div>' +
'<div style="font-size:12px;color:#94a3b8;margin-top:2px">' + (m.meeting_date || '') + (m.conducted_by_name ? ' — ' + m.conducted_by_name : '') + '</div></div>' +
'<div style="display:flex;gap:4px">' +
'<button onclick="editMeetingInline(' + m.id + ',' + staffId + ',\'' + type + '\')" style="background:none;border:none;cursor:pointer;color:#3b82f6;font-size:14px" title="Redigera">✎</button>' +
'<button onclick="deleteMeeting(' + m.id + ',' + staffId + ',\'' + type + '\')" style="background:none;border:none;cursor:pointer;color:#ef4444;font-size:14px" title="Ta bort">✕</button>' +
'</div></div>' +
(m.notes ? '<div style="font-size:13px;color:#374151;margin-bottom:8px;white-space:pre-wrap">' + m.notes + '</div>' : '') +
(actions ? '<div style="margin-top:8px"><div style="font-size:11px;color:#94a3b8;margin-bottom:4px">Åtgärdspunkter:</div><ul style="margin:0;padding-left:20px">' + actions + '</ul></div>' : '') +
(m.next_meeting ? '<div style="margin-top:8px;font-size:12px;color:#3b82f6">Nästa möte: ' + m.next_meeting + '</div>' : '') +
'</div>';
});
}
html += '</div>';
panel.innerHTML = html;
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
function showMeetingForm(staffId, type, existing) {
var label = type === 'salj_samtal' ? 'Sälj samtal' : 'Utvecklings samtal';
var panelId = type === 'salj_samtal' ? 'staffTabSalj' : 'staffTabUtveckling';
var panel = document.getElementById(panelId);
var today = new Date().toISOString().split('T')[0];
var title = existing ? (existing.title || '') : '';
var date = existing ? (existing.meeting_date || today) : today;
var notes = existing ? (existing.notes || '') : '';
var actPts = '';
if (existing && existing.action_points) {
try { var arr = JSON.parse(existing.action_points); actPts = Array.isArray(arr) ? arr.join('\n') : existing.action_points; } catch(e) { actPts = existing.action_points; }
}
var nextM = existing ? (existing.next_meeting || '') : '';
var mid = existing ? existing.id : 0;
var formHtml = '<div id="meetingFormBox" style="background:#f8fafc;border:2px solid #024550;border-radius:12px;padding:20px;margin-bottom:20px">' +
'<h4 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">' + (mid ? 'Redigera' : 'Nytt') + ' ' + label + '</h4>' +
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">' +
'<div><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Titel</label><input type="text" id="mtgTitle" value="' + title.replace(/"/g,'"') + '" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>' +
'<div><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Datum</label><input type="date" id="mtgDate" value="' + date + '" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div></div>' +
'<div style="margin-bottom:12px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Anteckningar</label><textarea id="mtgNotes" rows="4" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;box-sizing:border-box">' + notes.replace(/</g,'<') + '</textarea></div>' +
'<div style="margin-bottom:12px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Åtgärdspunkter (en per rad)</label><textarea id="mtgActions" rows="3" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;box-sizing:border-box">' + actPts.replace(/</g,'<') + '</textarea></div>' +
'<div style="margin-bottom:16px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Nästa möte</label><input type="date" id="mtgNext" value="' + nextM + '" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit"></div>' +
'<div style="display:flex;gap:10px">' +
'<button onclick="saveMeeting(' + staffId + ',\'' + type + '\',' + mid + ')" style="padding:8px 20px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">Spara</button>' +
'<button onclick="loadStaffMeetings(' + staffId + ',\'' + type + '\')" style="padding:8px 20px;background:#e5e7eb;color:#374151;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">Avbryt</button>' +
'</div></div>';
var old = document.getElementById('meetingFormBox');
if (old) old.remove();
var header = panel.querySelector('h3');
if (header && header.parentElement) header.parentElement.insertAdjacentHTML('afterend', formHtml);
else panel.insertAdjacentHTML('afterbegin', formHtml);
}
async function saveMeeting(staffId, type, meetingId) {
var actLines = document.getElementById('mtgActions').value.trim().split('\n').filter(function(l){ return l.trim(); });
var body = {
title: document.getElementById('mtgTitle').value.trim(),
meeting_date: document.getElementById('mtgDate').value,
notes: document.getElementById('mtgNotes').value.trim(),
action_points: actLines.length ? JSON.stringify(actLines) : null,
next_meeting: document.getElementById('mtgNext').value || null
};
try {
if (meetingId) {
body.id = meetingId;
await fetch('api/staff-meetings.php?action=update', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
} else {
body.staff_id = staffId;
body.type = type;
body.conducted_by = gStaffId || null;
await fetch('api/staff-meetings.php', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
}
loadStaffMeetings(staffId, type);
} catch(e) { alert('Fel: ' + e.message); }
}
async function editMeetingInline(meetingId, staffId, type) {
try {
var res = await fetch('api/staff-meetings.php?id=' + meetingId);
var m = await res.json();
if (m) showMeetingForm(staffId, type, m);
} catch(e) { alert('Fel: ' + e.message); }
}
async function deleteMeeting(meetingId, staffId, type) {
if (!confirm('Ta bort detta samtal?')) return;
try {
await fetch('api/staff-meetings.php?id=' + meetingId, { method:'DELETE' });
loadStaffMeetings(staffId, type);
} catch(e) { alert('Fel: ' + e.message); }
}
// --- Staff Deals ---
var staffDealsDT = null;
var staffDealsRaw = [];
var staffDealsFiltered = [];
async function loadStaffDeals(staffId) {
var panel = document.getElementById('staffTabAffarslista');
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar affärer...</div>';
try {
var res = await fetch('api/deals.php?saljare_id=' + staffId + '&limit=200');
var data = await res.json();
staffDealsRaw = Array.isArray(data) ? data : (data.deals || []);
if (!staffDealsRaw.length) {
panel.innerHTML = '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga affärer</div>';
return;
}
renderStaffDealsPanel(staffId);
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
function renderStaffDealsPanel(staffId) {
var panel = document.getElementById('staffTabAffarslista');
// Collect unique months and statuses
var months = {};
var statuses = {};
staffDealsRaw.forEach(function(d) {
var ds = d.datum_salj || '';
if (ds.length >= 7) months[ds.substring(0, 7)] = true;
if (d.status) statuses[d.status] = true;
});
var monthKeys = Object.keys(months).sort().reverse();
var statusKeys = Object.keys(statuses).sort();
// Build filter bar
var html = '<div style="margin-bottom:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">' +
'<select id="sdMonth" onchange="applyStaffDealsFilter(' + staffId + ')" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">' +
'<option value="">Alla månader</option>';
monthKeys.forEach(function(m) {
var parts = m.split('-');
var label = parts[0] + ' ' + ['','Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'][parseInt(parts[1])];
html += '<option value="' + m + '">' + label + '</option>';
});
html += '</select>' +
'<select id="sdStatus" onchange="applyStaffDealsFilter(' + staffId + ')" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">' +
'<option value="">Alla statusar</option>';
statusKeys.forEach(function(s) {
html += '<option value="' + s + '">' + (DEAL_STATUS_LABELS[s] || s) + '</option>';
});
html += '</select>' +
'<button onclick="document.getElementById(\'sdMonth\').value=\'\';document.getElementById(\'sdStatus\').value=\'\';applyStaffDealsFilter(' + staffId + ')" style="padding:6px 12px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit">Rensa</button>' +
'<div id="sdSumBox" style="margin-left:auto;font-size:13px;font-weight:600;color:#024550"></div>' +
'</div>';
// Stats cards
html += '<div id="sdStats" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-bottom:16px"></div>';
// Table
html += '<table id="staffDealsDT" class="display" style="width:100%"><thead><tr>' +
'<th>Affärsnr</th><th>Kund</th><th>Status</th><th>Säljstatus</th><th style="text-align:right">Värde</th><th>Datum</th>' +
'</tr></thead><tbody></tbody></table>';
panel.innerHTML = html;
applyStaffDealsFilter(staffId);
}
function applyStaffDealsFilter(staffId) {
var selMonth = document.getElementById('sdMonth') ? document.getElementById('sdMonth').value : '';
var selStatus = document.getElementById('sdStatus') ? document.getElementById('sdStatus').value : '';
staffDealsFiltered = staffDealsRaw.filter(function(d) {
if (selMonth) {
var ds = d.datum_salj || '';
if (ds.substring(0, 7) !== selMonth) return false;
}
if (selStatus && d.status !== selStatus) return false;
return true;
});
// Compute sum
var totalSum = 0;
staffDealsFiltered.forEach(function(d) { totalSum += parseFloat(d.ordervarde_ink_moms || 0); });
var sumBox = document.getElementById('sdSumBox');
if (sumBox) sumBox.textContent = 'Summa: ' + totalSum.toLocaleString('sv-SE') + ' kr (' + staffDealsFiltered.length + ' affärer)';
// Compute stats from ALL data (unfiltered)
renderStaffDealStats();
// Build DataTable
buildStaffDealsDT(staffDealsFiltered);
}
function renderStaffDealStats() {
var el = document.getElementById('sdStats');
if (!el) return;
var all = staffDealsRaw;
if (!all.length) { el.innerHTML = ''; return; }
// Group by month
var byMonth = {};
var totalVal = 0;
all.forEach(function(d) {
var ds = d.datum_salj || '';
var m = ds.length >= 7 ? ds.substring(0, 7) : 'unknown';
if (!byMonth[m]) byMonth[m] = { count: 0, value: 0, orders: 0 };
byMonth[m].count++;
var v = parseFloat(d.ordervarde_ink_moms || 0);
byMonth[m].value += v;
totalVal += v;
if (d.status === 'order') byMonth[m].orders++;
});
var monthKeys = Object.keys(byMonth).filter(function(k){ return k !== 'unknown'; }).sort();
var numMonths = monthKeys.length || 1;
var avgPerMonth = all.length / numMonths;
var totalOrders = all.filter(function(d){ return d.status === 'order'; }).length;
var avgOrdersPerMonth = totalOrders / numMonths;
var avgValuePerMonth = totalVal / numMonths;
// Current month trend
var now = new Date();
var curKey = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
var curMonth = byMonth[curKey] || { count: 0, value: 0, orders: 0 };
var dayOfMonth = now.getDate();
var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
var projectedCount = dayOfMonth > 0 ? Math.round(curMonth.count / dayOfMonth * daysInMonth) : 0;
var projectedValue = dayOfMonth > 0 ? Math.round(curMonth.value / dayOfMonth * daysInMonth) : 0;
var trending = projectedCount > avgPerMonth;
var trendPct = avgPerMonth > 0 ? Math.round((projectedCount - avgPerMonth) / avgPerMonth * 100) : 0;
function statCard(title, value, sub, color) {
return '<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:12px 14px">' +
'<div style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.3px">' + title + '</div>' +
'<div style="font-size:20px;font-weight:700;color:' + (color || '#024550') + ';margin:4px 0">' + value + '</div>' +
(sub ? '<div style="font-size:11px;color:#64748b">' + sub + '</div>' : '') +
'</div>';
}
el.innerHTML =
statCard('Totalt', all.length + ' affärer', totalVal.toLocaleString('sv-SE') + ' kr') +
statCard('Snitt/mån', avgPerMonth.toFixed(1) + ' affärer', avgValuePerMonth.toLocaleString('sv-SE', {maximumFractionDigits:0}) + ' kr/mån') +
statCard('Order snitt/mån', avgOrdersPerMonth.toFixed(1), totalOrders + ' totalt') +
statCard('Denna månad', curMonth.count + ' affärer',
curMonth.value.toLocaleString('sv-SE') + ' kr') +
statCard('Prognos månad', projectedCount + ' affärer',
projectedValue.toLocaleString('sv-SE') + ' kr') +
statCard('Trend',
(trending ? '▲ +' : '▼ ') + trendPct + '%',
trending ? 'Bättre än snitt' : 'Under snitt',
trending ? '#10b981' : '#ef4444');
}
function buildStaffDealsDT(deals) {
var rows = deals.map(function(d) {
return [
d.id,
d.deal_number || '',
d.customer_name || '',
d.status || '',
d.salj_status || '',
parseFloat(d.ordervarde_ink_moms || 0),
d.datum_salj || ''
];
});
if (staffDealsDT) { staffDealsDT.destroy(); staffDealsDT = null; }
staffDealsDT = $('#staffDealsDT').DataTable({
data: rows,
columns: [
{ title:'Affärsnr', render: function(d,t,r){ return '<strong style="color:#024550">' + r[1] + '</strong>'; } },
{ title:'Kund', render: function(d,t,r){ return r[2]; } },
{ title:'Status', render: function(d,t,r){
var color = DEAL_STATUS_COLORS[r[3]] || '#94a3b8';
var label = DEAL_STATUS_LABELS[r[3]] || r[3] || '-';
return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+color+'">'+label+'</span>';
}},
{ title:'Säljstatus', render: function(d,t,r){ return r[4] || '-'; }, width:'90px' },
{ title:'Värde', className:'dt-right', render: function(d,t,r){
return r[5] ? r[5].toLocaleString('sv-SE') + ' kr' : '-';
}},
{ title:'Datum', render: function(d,t,r){ return '<span style="font-size:12px;color:#64748b">'+r[6]+'</span>'; }, width:'100px' }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ affärer', infoEmpty:'Inga affärer',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Inga affärer hittades'
},
pageLength: 25,
order: [[5,'desc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showDealDetail(data[0]); });
}
});
}
function showStaffModal(staffData) {
var modal = document.getElementById('staffModal');
document.getElementById('staffModalTitle').textContent = staffData ? 'Redigera personal' : 'Lägg till personal';
document.getElementById('staffEditId').value = staffData ? staffData.id : '';
document.getElementById('staffName').value = staffData ? staffData.name : '';
document.getElementById('staffEmail').value = staffData ? (staffData.email || '') : '';
document.getElementById('staffRole').value = staffData ? staffData.role : 'saljare';
document.getElementById('staffPhone').value = staffData ? (staffData.phone || '') : '';
document.getElementById('staffTitle').value = staffData ? (staffData.title || '') : '';
document.getElementById('staffPnr').value = staffData ? (staffData.personnummer || '') : '';
document.getElementById('staffAddress').value = staffData ? (staffData.address || '') : '';
document.getElementById('staffZip').value = staffData ? (staffData.zip || '') : '';
document.getElementById('staffCity').value = staffData ? (staffData.city || '') : '';
document.getElementById('staffStartDate').value = staffData ? (staffData.start_date || '') : '';
document.getElementById('staffGrundlon').value = staffData ? (staffData.grundlon || '') : '';
document.getElementById('staffSkatt').value = staffData ? (staffData.skatt_procent || '30') : '30';
document.getElementById('staffModalError').style.display = 'none';
modal.style.display = 'flex';
}
function closeStaffModal() {
document.getElementById('staffModal').style.display = 'none';
}
async function saveStaff() {
var id = document.getElementById('staffEditId').value;
var data = {
name: document.getElementById('staffName').value.trim(),
email: document.getElementById('staffEmail').value.trim(),
role: document.getElementById('staffRole').value,
phone: document.getElementById('staffPhone').value.trim() || null,
title: document.getElementById('staffTitle').value.trim() || null,
personnummer: document.getElementById('staffPnr').value.trim() || null,
address: document.getElementById('staffAddress').value.trim() || null,
zip: document.getElementById('staffZip').value.trim() || null,
city: document.getElementById('staffCity').value.trim() || null,
start_date: document.getElementById('staffStartDate').value || null,
grundlon: document.getElementById('staffGrundlon').value ? parseFloat(document.getElementById('staffGrundlon').value) : null,
skatt_procent: document.getElementById('staffSkatt').value ? parseFloat(document.getElementById('staffSkatt').value) : 30,
};
if (!data.name || !data.email) {
const err = document.getElementById('staffModalError');
err.textContent = 'Namn och e-post krävs';
err.style.display = 'block';
return;
}
try {
const url = id ? STAFF_API + '?id=' + id : STAFF_API;
const res = await fetch(url, {
method: id ? 'PUT' : 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
const result = await res.json();
if (!res.ok) {
const err = document.getElementById('staffModalError');
err.textContent = result.error || 'Något gick fel';
err.style.display = 'block';
return;
}
closeStaffModal();
loadStaff();
// Refresh detail view if open
if (currentStaffData && currentStaffData.id == id) {
showStaffDetail(parseInt(id));
}
} catch (e) {
var err = document.getElementById('staffModalError');
err.textContent = e.message;
err.style.display = 'block';
}
}
async function editStaff(id) {
try {
var res = await fetch(STAFF_API + '?id=' + id);
var staff = await res.json();
showStaffModal(staff);
} catch (e) {
alert('Kunde inte ladda: ' + e.message);
}
}
async function toggleStaffActive(id, currentActive) {
try {
await fetch(STAFF_API + '?id=' + id, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({active: currentActive == 1 ? 0 : 1}),
});
loadStaff();
} catch (e) {
alert('Fel: ' + e.message);
}
}
// Monday.com settings
async function loadMondaySettings(){
try {
const res = await fetch('/api/settings.php?keys=monday_api_token');
const data = await res.json();
const token = data.settings?.monday_api_token || '';
const input = document.getElementById('settMondayToken');
if(input && token) input.value = token;
if(token) loadMondayBoards();
} catch(e){}
}
async function testMondayConnection(){
const statusEl = document.getElementById('mondayConnectionStatus');
statusEl.innerHTML = '<span style="color:#64748b">Testar...</span>';
try {
const res = await fetch('/api/monday.php?action=test');
const data = await res.json();
if(data.success){
statusEl.innerHTML = '<span style="color:#059669">Ansluten som <strong>'+data.user.name+'</strong> ('+data.user.email+')</span>';
loadMondayBoards();
} else {
statusEl.innerHTML = '<span style="color:#ef4444">'+( data.error||'Anslutning misslyckades')+'</span>';
}
} catch(e){
statusEl.innerHTML = '<span style="color:#ef4444">Fel: '+e.message+'</span>';
}
}
async function saveMondaySettings(){
const token = document.getElementById('settMondayToken').value;
try {
await fetch('/api/settings.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({monday_api_token: token})
});
testMondayConnection();
} catch(e){ alert('Kunde inte spara: '+e.message); }
}
// === VALUTA ===
let _currencyRates = {};
async function loadCurrencySettings() {
try {
const res = await fetch('/api/settings.php?keys=currency_EUR,currency_USD,currency_DKK,currency_NOK,currency_updated');
const data = await res.json();
if(data.success) {
const s = data.settings;
if(s.currency_EUR) { _currencyRates.EUR = parseFloat(s.currency_EUR); var _el=document.getElementById('settCurrencyEUR'); if(_el) _el.value=s.currency_EUR; }
if(s.currency_USD) { _currencyRates.USD = parseFloat(s.currency_USD); var _el=document.getElementById('settCurrencyUSD'); if(_el) _el.value=s.currency_USD; }
if(s.currency_DKK) { _currencyRates.DKK = parseFloat(s.currency_DKK); var _el=document.getElementById('settCurrencyDKK'); if(_el) _el.value=s.currency_DKK; }
if(s.currency_NOK) { _currencyRates.NOK = parseFloat(s.currency_NOK); var _el=document.getElementById('settCurrencyNOK'); if(_el) _el.value=s.currency_NOK; }
if(s.currency_updated) {
const el = document.getElementById('eurRateInfo');
if(el) el.textContent = 'Senast uppdaterad: ' + s.currency_updated;
}
}
} catch(e) { console.error('loadCurrencySettings error', e); }
}
async function saveCurrencySettings() {
const eur = document.getElementById('settCurrencyEUR').value;
const usd = document.getElementById('settCurrencyUSD').value;
const dkk = document.getElementById('settCurrencyDKK').value;
const nok = document.getElementById('settCurrencyNOK').value;
const status = document.getElementById('currencySaveStatus');
try {
const res = await fetch('/api/settings.php', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ settings: {
currency_EUR: eur, currency_USD: usd, currency_DKK: dkk, currency_NOK: nok,
currency_updated: new Date().toISOString().slice(0,16).replace('T',' ')
}})
});
const data = await res.json();
if(data.success) {
_currencyRates = { EUR: parseFloat(eur)||0, USD: parseFloat(usd)||0, DKK: parseFloat(dkk)||0, NOK: parseFloat(nok)||0 };
status.innerHTML = '<span style="color:#059669">\u2714 Sparat!</span>';
setTimeout(() => status.textContent = '', 3000);
}
} catch(e) { status.innerHTML = '<span style="color:#dc2626">Fel: '+e.message+'</span>'; }
}
async function fetchLiveRates() {
const status = document.getElementById('currencySaveStatus');
status.innerHTML = '<span style="color:#0284c7">Hämtar kurser...</span>';
try {
const res = await fetch('https://api.frankfurter.dev/v1/latest?base=SEK&symbols=EUR,USD,DKK,NOK');
const data = await res.json();
if(data.rates) {
// API ger SEK->X, vi vill ha X->SEK (1/rate)
if(data.rates.EUR) document.getElementById('settCurrencyEUR').value = (1/data.rates.EUR).toFixed(4);
if(data.rates.USD) document.getElementById('settCurrencyUSD').value = (1/data.rates.USD).toFixed(4);
if(data.rates.DKK) document.getElementById('settCurrencyDKK').value = (1/data.rates.DKK).toFixed(4);
if(data.rates.NOK) document.getElementById('settCurrencyNOK').value = (1/data.rates.NOK).toFixed(4);
status.innerHTML = '<span style="color:#059669">\u2714 Kurser hämtade (Frankfurter API). Klicka Spara!</span>';
}
} catch(e) { status.innerHTML = '<span style="color:#dc2626">Kunde inte hämta: '+e.message+'</span>'; }
}
function convertToSEK(amount, currency) {
if(!currency || currency === 'SEK') return amount;
const rate = _currencyRates[currency];
return rate ? amount * rate : amount;
}
async function loadMondayBoards(){
const section = document.getElementById('mondayBoardsSection');
const list = document.getElementById('mondayBoardsList');
try {
const res = await fetch('/api/monday.php?action=boards');
const boards = await res.json();
if(boards.length === 0){ section.style.display='none'; return; }
section.style.display='';
list.innerHTML = boards.filter(b => b.state === 'active').map(b => {
const kind = b.board_kind === 'private' ? ' (privat)' : '';
return '<div style="padding:10px 14px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;display:flex;justify-content:space-between;align-items:center">'
+'<div><span style="font-size:14px;font-weight:500;color:#1a1a1a">'+b.name+'</span>'
+'<span style="font-size:11px;color:#94a3b8;margin-left:8px">'+b.items_count+' items'+kind+'</span></div>'
+'<button onclick="viewMondayBoard('+b.id+')" style="padding:4px 12px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit;color:#334155">Visa</button>'
+'</div>';
}).join('');
} catch(e){ console.error('loadMondayBoards error', e); }
}
async function viewMondayBoard(boardId){
try {
const res = await fetch('/api/monday.php?action=board&id='+boardId);
const board = await res.json();
if(!board) return;
let html = '<h3 style="margin:24px 0 8px">'+board.name+'</h3>';
html += '<p style="font-size:12px;color:#94a3b8;margin-bottom:12px">'+board.columns.length+' kolumner, '+board.groups.length+' grupper</p>';
html += '<div style="margin-bottom:16px"><strong style="font-size:13px">Grupper:</strong> <span style="font-size:13px;color:#64748b">'+board.groups.map(g=>g.title).join(', ')+'</span></div>';
html += '<table style="width:100%;border-collapse:collapse;font-size:12px;margin-bottom:16px"><thead><tr style="background:#f8fafc"><th style="text-align:left;padding:6px 10px;border-bottom:1px solid #e5e7eb">Kolumn</th><th style="text-align:left;padding:6px 10px;border-bottom:1px solid #e5e7eb">Typ</th></tr></thead><tbody>';
board.columns.forEach(c => {
html += '<tr><td style="padding:4px 10px;border-bottom:1px solid #f1f5f9">'+c.title+'</td><td style="padding:4px 10px;border-bottom:1px solid #f1f5f9;color:#64748b">'+c.type+'</td></tr>';
});
html += '</tbody></table>';
if(board.items_page && board.items_page.items && board.items_page.items.length > 0){
html += '<strong style="font-size:13px">Senaste items ('+board.items_page.items.length+'):</strong>';
html += '<div style="display:flex;flex-direction:column;gap:4px;margin-top:8px">';
board.items_page.items.slice(0,10).forEach(item => {
html += '<div style="padding:6px 10px;background:#fafbfc;border-radius:6px;font-size:12px;color:#1a1a1a">'+item.name+'</div>';
});
if(board.items_page.items.length > 10) html += '<div style="font-size:11px;color:#94a3b8;padding:4px 10px">...och '+(board.items_page.items.length-10)+' till</div>';
html += '</div>';
}
document.getElementById('mondayBoardsList').insertAdjacentHTML('beforeend', html);
} catch(e){ alert('Kunde inte ladda board: '+e.message); }
}
// AI Settings
async function loadAiSettings(){
try {
const res = await fetch('/api/settings.php?keys=openai_api_key,openai_model,gemini_api_key,gemini_model,default_ai_provider');
const data = await res.json();
const s = data.settings || {};
if(s.openai_api_key) document.getElementById('settOpenaiKey').value = s.openai_api_key;
if(s.openai_model) document.getElementById('settOpenaiModel').value = s.openai_model;
if(s.gemini_api_key) document.getElementById('settGeminiKey').value = s.gemini_api_key;
// gemini_model is fixed: gemini-2.5-flash-image
if(s.default_ai_provider) document.getElementById('settDefaultProvider').value = s.default_ai_provider;
} catch(e){ console.error('loadAiSettings', e); }
}
async function saveAiSettings(){
const settings = {
openai_api_key: document.getElementById('settOpenaiKey').value,
openai_model: document.getElementById('settOpenaiModel').value,
gemini_api_key: document.getElementById('settGeminiKey').value,
gemini_model: 'gemini-2.5-flash-image',
default_ai_provider: document.getElementById('settDefaultProvider').value,
};
try {
await fetch('/api/settings.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(settings)
});
document.getElementById('openaiTestStatus').innerHTML = '<span style="color:#059669">Sparad</span>';
document.getElementById('geminiTestStatus').innerHTML = '<span style="color:#059669">Sparad</span>';
setTimeout(() => {
document.getElementById('openaiTestStatus').innerHTML = '';
document.getElementById('geminiTestStatus').innerHTML = '';
}, 2000);
} catch(e){ alert('Fel: '+e.message); }
}
// === SALES PRODUCT MODAL ===
let _spSelectedVariant = 0;
let _spTillval = [];
let _spProduct = null;
let _spImages = [];
let _spCurrentImage = 0;
async function showProductModal(id) {
const p = catalogProducts.find(x => x.id === id);
if(!p) return;
_spProduct = p;
_spSelectedVariant = 0;
_spImages = [p.img, ...(p.gallery || [])].filter(Boolean);
_spCurrentImage = 0;
const isAdmin = (gUserRole === 'systemadmin' || gUserRole === 'admin' || gUserRole === 'saljchef');
const variants = (p.specs && p.specs.variants) || [];
const attributes = (p.specs && p.specs.attributes) || [];
const documents = p.documents || [];
// Detect variant label key
let variantLabelKey = '', variantLabelSuffix = '';
if(variants.length) {
const first = variants[0];
if(first.kwh !== undefined) { variantLabelKey = 'kwh'; variantLabelSuffix = ' kWh'; }
else if(first.paneler !== undefined) { variantLabelKey = 'paneler'; variantLabelSuffix = ' paneler'; }
else if(first.cm !== undefined) { variantLabelKey = 'cm'; variantLabelSuffix = ' cm'; }
else if(first.material !== undefined) { variantLabelKey = 'material'; variantLabelSuffix = ''; }
else { const keys = Object.keys(first); variantLabelKey = keys[0]; variantLabelSuffix = ''; }
}
// Width-only selector (foderkloss, tröskel, utv råplan etc)
let widthOnlyHtml = '';
if(p.specs && p.specs.type === 'width_variants') {
const wv = p.specs.variants;
widthOnlyHtml = '<div style="margin-bottom:16px">'
+'<div style="font-size:12px;font-weight:600;color:#334155;margin-bottom:8px">Välj variant</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:6px" id="spWidthOnlyChips">'
+ wv.map((v, i) => {
const label = v.label || (v.width_mm ? v.width_mm + ' mm' : (v.length_mm ? v.length_mm + ' mm' : ''));
const priceStr = v.price != null ? v.price.toFixed(2) + ' kr' : '–';
return '<button onclick="spSelectWidthOnly('+i+')" class="sp-wo-chip" data-idx="'+i+'" style="padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s;background:#f8f9fa;color:#334155;border:2px solid #e5e7eb;display:flex;flex-direction:column;align-items:center;gap:2px">'
+'<span>'+label+'</span>'
+'<span style="font-size:10px;font-weight:400;color:#64748b">'+priceStr+'</span>'
+'</button>';
}).join('')
+'</div></div>';
}
// Style + width selector (foder, smyg, fönsterbänk etc)
let styleWidthHtml = '';
if(p.specs && p.specs.type === 'style_width_variants') {
const sw = p.specs.styles;
styleWidthHtml = '<div style="margin-bottom:16px" id="spStyleWidth">'
+'<div style="font-size:12px;font-weight:600;color:#334155;margin-bottom:8px">Välj utförande</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px" id="spStyleChips">'
+ sw.map((s, i) => '<button onclick="spSelectStyle('+i+')" class="sp-style-chip" data-idx="'+i+'" style="padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s;'
+(i===0?'background:#024550;color:#fff;border:2px solid #024550':'background:#f8f9fa;color:#334155;border:2px solid #e5e7eb')
+'">'+s.style+(s.desc?' <span style="font-weight:400;opacity:.7;font-size:11px">('+s.desc+')</span>':'')+'</button>').join('')
+'</div>'
+'<div style="font-size:12px;font-weight:600;color:#334155;margin-bottom:4px">Välj bredd</div>'
+'<div id="spWidthChips" style="display:flex;flex-wrap:wrap;gap:6px"></div>'
+'<div id="spStyleResult" style="display:none;margin-top:10px"></div>'
+'</div>';
}
// Window sizes selector
let windowSizesHtml = '';
if(p.specs && p.specs.type === 'window_sizes') {
const ws = p.specs;
windowSizesHtml = '<div style="margin-bottom:16px" id="spWindowSizes">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px">Leverantör: '+ws.supplier+' | Modell: '+ws.model+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:12px 0">'
+'<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Bredd (dm)</label>'
+'<select id="spWinWidth" onchange="spUpdateWindowSize()" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;background:#fff">'
+'<option value="">Välj bredd...</option>'
+ ws.widths.map(w => '<option value="'+w+'">'+w+' dm ('+(w*10)+' cm)</option>').join('')
+'</select></div>'
+'<div><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Höjd (dm)</label>'
+'<select id="spWinHeight" onchange="spUpdateWindowSize()" style="width:100%;padding:10px 12px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit;background:#fff">'
+'<option value="">Välj höjd...</option>'
+ ws.heights.map(h => '<option value="'+h+'">'+h+' dm ('+(h*10)+' cm)</option>').join('')
+'</select></div>'
+'</div>'
+'<div style="margin:8px 0"><label style="font-size:12px;font-weight:600;color:#334155;display:block;margin-bottom:4px">Antal</label>'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<button onclick="spWinQty(-1)" style="width:36px;height:36px;border-radius:8px;border:1.5px solid #e5e7eb;background:#f8f9fa;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:inherit;color:#334155">−</button>'
+'<input id="spWinAntal" type="number" min="1" value="1" onchange="spUpdateWindowSize()" style="width:60px;padding:8px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:16px;font-weight:700;text-align:center;font-family:inherit">'
+'<button onclick="spWinQty(1)" style="width:36px;height:36px;border-radius:8px;border:1.5px solid #e5e7eb;background:#f8f9fa;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:inherit;color:#334155">+</button>'
+'</div></div>'
+'<div id="spWinResult" style="display:none"></div>'
+'</div>';
}
// Variant chips
let variantChipsHtml = '';
if(variants.length > 1) {
variantChipsHtml = '<div style="margin-bottom:16px">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Välj variant</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:6px" id="spVariantChips">'
+ variants.map((v, i) => {
const label = v[variantLabelKey] + variantLabelSuffix;
return '<button onclick="spSelectVariant('+i+')" class="sp-variant-chip" data-idx="'+i+'" style="padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s;'
+(i===0 ? 'background:#024550;color:#fff;border:2px solid #024550' : 'background:#f8f9fa;color:#334155;border:2px solid #e5e7eb')
+'">'+label+'</button>';
}).join('')
+'</div></div>';
}
// Images
const mainImgSrc = _spImages.length ? _spImages[0] : '';
let imgHtml = '<div style="width:100%;aspect-ratio:4/3;background:#f8f9fa;border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;margin-bottom:12px;cursor:'+(mainImgSrc?'pointer':'default')+'" id="spMainImageWrap" onclick="spOpenFullscreen()">';
if(mainImgSrc) {
imgHtml += '<img id="spMainImage" src="'+mainImgSrc+'" style="width:100%;height:100%;object-fit:contain">';
} else {
imgHtml += '<div style="text-align:center;color:#cbd5e1"><svg viewBox="0 0 24 24" style="width:48px;height:48px;stroke:currentColor;fill:none;stroke-width:1"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></div>';
}
imgHtml += '</div>';
// Thumbnails
if(_spImages.length > 1) {
imgHtml += '<div style="display:flex;gap:6px;flex-wrap:wrap" id="spThumbnails">'
+ _spImages.map((img, i) => '<img src="'+img+'" onclick="event.stopPropagation();spSetImage('+i+')" style="width:56px;height:56px;object-fit:cover;border-radius:8px;cursor:pointer;border:2px solid '+(i===0?'#024550':'#e5e7eb')+';transition:border-color .15s">').join('')
+'</div>';
}
// Dots
if(_spImages.length > 1) {
imgHtml += '<div style="display:flex;justify-content:center;gap:6px;margin-top:8px" id="spDots">'
+ _spImages.map((_, i) => '<span onclick="event.stopPropagation();spSetImage('+i+')" style="width:8px;height:8px;border-radius:50%;cursor:pointer;transition:background .15s;background:'+(i===0?'#024550':'#cbd5e1')+'"></span>').join('')
+'</div>';
}
// Badges
const greenBadge = p.greenTechEligible ? '<span style="display:inline-flex;align-items:center;gap:4px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;font-size:11px;font-weight:700;padding:5px 12px;border-radius:8px"><svg viewBox="0 0 24 24" style="width:14px;height:14px;fill:currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>Grönt teknik-avdrag</span>' : '';
const rotBadge = p.rotEligible ? '<span style="display:inline-flex;align-items:center;gap:4px;background:linear-gradient(135deg,#2563eb,#3b82f6);color:#fff;font-size:11px;font-weight:700;padding:5px 12px;border-radius:8px;margin-left:6px">ROT-avdrag</span>' : '';
// Specs table
let specsHtml = '';
if(attributes.length) {
specsHtml = '<div>'
+'<div style="font-size:13px;font-weight:700;color:#1a1a1a;margin-bottom:8px">Specifikationer</div>'
+'<table style="width:100%;border-collapse:collapse;font-size:13px">'
+ attributes.map(a => '<tr style="border-top:1px solid #f1f5f9"><td style="padding:8px 0;color:#64748b;width:40%">'+a.key+'</td><td style="padding:8px 0;font-weight:600">'+a.value+'</td></tr>').join('')
+'</table></div>';
}
// Documents
let docsHtml = '';
if(documents.length) {
const typeLabels = {manual:'Manual',datasheet:'Datablad',certificate:'Certifikat',warranty:'Garanti',other:'Dokument'};
docsHtml = '<div>'
+'<div style="font-size:13px;font-weight:700;color:#1a1a1a;margin-bottom:8px">Dokument</div>'
+ documents.map(d => {
const tl = typeLabels[d.type] || d.type || 'Dokument';
return '<a href="'+d.file+'" target="_blank" download style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:#f8f9fa;border-radius:8px;margin-bottom:6px;text-decoration:none;color:#334155;transition:background .15s" onmouseover="this.style.background=\'#f1f5f9\'" onmouseout="this.style.background=\'#f8f9fa\'">'
+'<svg viewBox="0 0 24 24" style="width:20px;height:20px;stroke:#0284c7;fill:none;stroke-width:2;flex-shrink:0"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
+'<div style="flex:1;min-width:0"><div style="font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+(d.name||'Dokument')+'</div>'
+'<span style="font-size:10px;color:#94a3b8">'+tl+'</span></div>'
+'<svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:#94a3b8;fill:none;stroke-width:2;flex-shrink:0"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'
+'</a>';
}).join('')
+'</div>';
}
// Bottom section
let bottomHtml = '';
if(specsHtml || docsHtml) {
bottomHtml = '<div style="border-top:1px solid #f1f5f9;padding:20px 24px 24px;display:grid;grid-template-columns:'+(specsHtml && docsHtml ? '1fr 1fr' : '1fr')+';gap:24px">'
+ specsHtml + docsHtml +'</div>';
}
// Remove existing
document.getElementById('productSalesModal')?.remove();
const modal = document.createElement('div');
modal.id = 'productSalesModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:1100px;width:100%;max-height:95vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
// Header
+'<div style="padding:14px 24px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center">'
+'<button onclick="document.getElementById(\'productSalesModal\').remove()" style="display:flex;align-items:center;gap:6px;background:none;border:none;cursor:pointer;color:#64748b;font-size:13px;font-weight:600;font-family:inherit;padding:4px 0"><svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2"><polyline points="15 18 9 12 15 6"/></svg>Tillbaka</button>'
+(isAdmin ? '<button onclick="document.getElementById(\'productSalesModal\').remove();editProduct(\''+p.id+'\')" style="display:flex;align-items:center;gap:6px;background:#f8f9fa;border:1px solid #e5e7eb;border-radius:8px;padding:7px 14px;cursor:pointer;font-size:12px;font-weight:600;color:#334155;font-family:inherit"><svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Redigera</button>' : '')
+'</div>'
// Main: bild + info + tillval vertikalt
+'<div style="padding:24px">'
// TOP ROW: bild + produktinfo
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px">'
+'<div>'+imgHtml+'</div>'
+'<div>'
+'<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">'+p.catLabel+'</div>'
+'<h2 style="font-size:24px;font-weight:800;color:#1a1a1a;margin:0 0 8px 0;line-height:1.2">'+p.name+'</h2>'
+(greenBadge || rotBadge ? '<div style="margin-bottom:12px">'+greenBadge+rotBadge+'</div>' : '')
+(p.desc ? '<p style="font-size:14px;color:#64748b;line-height:1.6;margin:0 0 20px 0">'+p.desc+'</p>' : '')
+ (widthOnlyHtml || styleWidthHtml || windowSizesHtml || variantChipsHtml)
+'<div id="spQtyArea"></div>'
+'<div id="spPriceArea" style="background:#f8f9fa;border-radius:12px;padding:16px"></div>'
+'</div>'
+'</div>'
// TILLVAL: 2 kolumner
+'<div id="spTillvalArea" style="margin-bottom:16px"></div>'
+'<div id="spColorSection" style="margin-top:16px">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">F\u00e4rg</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">'
+'<div style="padding:12px;background:#f8f9fa;border:1.5px solid #e5e7eb;border-radius:10px;cursor:pointer;transition:all .15s" onclick="spOpenColorPicker(\'insida\')" onmouseover="this.style.borderColor=\'#94a3b8\'" onmouseout="this.style.borderColor=\'#e5e7eb\'">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;margin-bottom:6px">F\u00e4rg insida</div>'
+'<div style="display:flex;align-items:center;gap:8px"><div id="spColorInsidaSwatch" style="width:36px;height:36px;border-radius:6px;background:#f0ece4;border:1px solid #ddd"></div><div><div id="spColorInsidaName" style="font-size:12px;font-weight:600;color:#1a1a1a">Vit</div><div id="spColorInsidaCode" style="font-size:10px;color:#94a3b8">RAL 9010</div></div></div>'
+'</div>'
+'<div style="padding:12px;background:#f8f9fa;border:1.5px solid #e5e7eb;border-radius:10px;cursor:pointer;transition:all .15s" onclick="spOpenColorPicker(\'utsida\')" onmouseover="this.style.borderColor=\'#94a3b8\'" onmouseout="this.style.borderColor=\'#e5e7eb\'">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;margin-bottom:6px">F\u00e4rg utsida</div>'
+'<div style="display:flex;align-items:center;gap:8px"><div id="spColorUtsidaSwatch" style="width:36px;height:36px;border-radius:6px;background:#f0ece4;border:1px solid #ddd"></div><div><div id="spColorUtsidaName" style="font-size:12px;font-weight:600;color:#1a1a1a">Vit</div><div id="spColorUtsidaCode" style="font-size:10px;color:#94a3b8">RAL 9010</div></div></div>'
+'</div>'
+'</div>'
+'</div>'
+'<div id="spSprojsSection" style="margin-top:16px">'
+'<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Spr\u00f6js</div>'
+'<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px" id="spSprojsGrid">'
+'<div onclick="spSelectSprojs(0)" class="sp-sprojs-opt" data-idx="0" style="border:2px solid #024550;border-radius:10px;padding:10px;text-align:center;cursor:pointer;background:#f0fdf4">'
+'<svg viewBox="0 0 60 80" style="width:48px;height:64px;margin:0 auto 6px"><rect x="2" y="2" width="56" height="76" rx="2" fill="none" stroke="#64748b" stroke-width="2"/><rect x="8" y="8" width="44" height="64" fill="#e0f2fe" stroke="#94a3b8" stroke-width="1"/></svg>'
+'<div style="font-size:11px;font-weight:600;color:#1a1a1a">Inga</div></div>'
+'<div onclick="spSelectSprojs(1)" class="sp-sprojs-opt" data-idx="1" style="border:2px solid #e5e7eb;border-radius:10px;padding:10px;text-align:center;cursor:pointer">'
+'<svg viewBox="0 0 60 80" style="width:48px;height:64px;margin:0 auto 6px"><rect x="2" y="2" width="56" height="76" rx="2" fill="none" stroke="#64748b" stroke-width="2"/><rect x="8" y="8" width="44" height="64" fill="#e0f2fe" stroke="#94a3b8" stroke-width="1"/><line x1="30" y1="8" x2="30" y2="72" stroke="#94a3b8" stroke-width="1.5"/><line x1="8" y1="40" x2="52" y2="40" stroke="#94a3b8" stroke-width="1.5"/></svg>'
+'<div style="font-size:10px;font-weight:500;color:#334155">1V, 1H</div></div>'
+'<div onclick="spSelectSprojs(2)" class="sp-sprojs-opt" data-idx="2" style="border:2px solid #e5e7eb;border-radius:10px;padding:10px;text-align:center;cursor:pointer">'
+'<svg viewBox="0 0 60 80" style="width:48px;height:64px;margin:0 auto 6px"><rect x="2" y="2" width="56" height="76" rx="2" fill="none" stroke="#64748b" stroke-width="2"/><rect x="8" y="8" width="44" height="64" fill="#e0f2fe" stroke="#94a3b8" stroke-width="1"/><line x1="30" y1="8" x2="30" y2="72" stroke="#94a3b8" stroke-width="1.5"/><line x1="8" y1="30" x2="52" y2="30" stroke="#94a3b8" stroke-width="1.5"/><line x1="8" y1="50" x2="52" y2="50" stroke="#94a3b8" stroke-width="1.5"/></svg>'
+'<div style="font-size:10px;font-weight:500;color:#334155">1V, 2H</div></div>'
+'<div onclick="spSelectSprojs(3)" class="sp-sprojs-opt" data-idx="3" style="border:2px solid #e5e7eb;border-radius:10px;padding:10px;text-align:center;cursor:pointer">'
+'<svg viewBox="0 0 60 80" style="width:48px;height:64px;margin:0 auto 6px"><rect x="2" y="2" width="56" height="76" rx="2" fill="none" stroke="#64748b" stroke-width="2"/><rect x="8" y="8" width="44" height="64" fill="#e0f2fe" stroke="#94a3b8" stroke-width="1"/><line x1="22" y1="8" x2="22" y2="72" stroke="#94a3b8" stroke-width="1.5"/><line x1="38" y1="8" x2="38" y2="72" stroke="#94a3b8" stroke-width="1.5"/><line x1="8" y1="30" x2="52" y2="30" stroke="#94a3b8" stroke-width="1.5"/><line x1="8" y1="50" x2="52" y2="50" stroke="#94a3b8" stroke-width="1.5"/></svg>'
+'<div style="font-size:10px;font-weight:500;color:#334155">2V, 2H</div></div>'
+'</div>'
+'</div>'
+'<button onclick="cartAddFromModal()" style="width:100%;padding:12px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;justify-content:center;gap:8px;transition:opacity .15s;box-shadow:0 4px 12px rgba(16,185,129,.3)" onmouseover="this.style.opacity=\'0.9\'" onmouseout="this.style.opacity=\'1\'"><svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>Lägg till i kalkyl</button>'
+'</div>'
+'</div>'
+ bottomHtml
+'</div>';
document.body.appendChild(modal);
// Load tillval
// Init width-only
_spSelectedWidthOnly = null;
// Init style+width chips
if(p.specs && p.specs.type === 'style_width_variants') {
_spSelectedStyle = 0;
_spSelectedWidth = null;
spRenderWidthChips();
}
if(!["fonster","dorrar"].includes(p.cat)){var _cs=document.getElementById("spColorSection");if(_cs)_cs.style.display="none";var _ss=document.getElementById("spSprojsSection");if(_ss)_ss.style.display="none";}
// KVM input for per-unit products
var qtyArea=document.getElementById('spQtyArea');
if(qtyArea && p.unit && p.unit!=='st'){
var unitLabel=p.unit==='kvm'?'m²':p.unit;
qtyArea.innerHTML='<div style="margin-bottom:12px"><label style="font-size:11px;font-weight:600;color:#64748b;display:block;margin-bottom:4px">Antal '+unitLabel+'</label><input type="number" id="spQtyInput" value="1" min="1" placeholder="150" oninput="spUpdatePrice()" style="width:160px;padding:10px 14px;border:1.5px solid #e5e7eb;border-radius:8px;font-size:14px;font-family:inherit"></div>';
}
await spLoadTillval(p.id);
spUpdatePrice();
}
async function spLoadTillval(productId) {
try {
const [svcRes, accRes, linkedRes] = await Promise.all([
fetch('/api/products.php?cat=tjanster&active=1'),
fetch('/api/products.php?cat=tillbehor&active=1'),
fetch('/api/products.php?services_for='+productId)
]);
const svcData = await svcRes.json();
const accData = await accRes.json();
const linkedData = await linkedRes.json();
const linkedMap = {}; if(linkedData.success) (linkedData.services||[]).forEach(s=>{linkedMap[s.service_id]={default_on:parseInt(s.default_on),hidden:parseInt(s.hidden||0),mandatory:parseInt(s.mandatory||0),default_variant:s.default_variant||null};});
const linked = Object.keys(linkedMap);
const allProducts = [...(svcData.success ? svcData.products : []), ...(accData.success ? accData.products : [])];
_spTillval = allProducts
.filter(s => linked.includes(s.id))
.map(s => {
let specs = null;
try { if(s.specs) specs = typeof s.specs === 'string' ? JSON.parse(s.specs) : s.specs; } catch(e){}
const lnk=linkedMap[s.id]||{}; const isOblig=parseInt(s.tillval_obligatorisk)||0; const isHid=parseInt(s.tillval_hidden)||0; return { id: s.id, name: s.name, price: parseFloat(s.price), unit: s.unit || 'st', checked: !!(lnk.default_on || isOblig || lnk.mandatory), included: !!(isOblig || lnk.mandatory), hidden: !!(lnk.hidden || isHid), qty: 1, userQty: 0, userL: 0, userW: 0, computedPrice: null, excluded: false, included: false, specs: specs, selectedStyle: 0, selectedVariant: null };
});
// Resolve default_variant to selectedStyle + selectedVariant
_spTillval.forEach(function(tv) {
var lnk = linkedMap[tv.id] || {};
if (lnk.default_variant && tv.specs && tv.specs.type === 'style_width_variants' && tv.specs.styles) {
var parts = lnk.default_variant.split('|');
for (var si = 0; si < tv.specs.styles.length; si++) {
if (tv.specs.styles[si].style === parts[0]) {
tv.selectedStyle = si;
if (parts[1]) {
for (var wi = 0; wi < tv.specs.styles[si].variants.length; wi++) {
if (String(tv.specs.styles[si].variants[wi].width_mm) === parts[1]) { tv.selectedVariant = wi; break; }
}
}
break;
}
}
}
});
spApplyRulesAndRender();
} catch(e) { console.error('Load tillval error:', e); }
}
function spApplyRulesAndRender() {
const p = _spProduct;
const variants = (p.specs && p.specs.variants) || [];
const rules = (p.specs && p.specs.rules) || [];
const selectedVariant = _spSelectedWindowSize || variants[_spSelectedVariant] || {};
// 1) Kör föräldraproduktens regler
const ruleResult = peApplyRules(rules, selectedVariant, _spTillval);
// Bygg berikad data med alla tillvals valda bredder
const enrichedVariant = Object.assign({}, selectedVariant);
_spTillval.forEach(tv => {
if(tv.checked && tv.specs && tv.selectedVariant !== null) {
const key = tv.id.replace(/-/g, '_');
if(tv.specs.type === 'style_width_variants') {
const style = tv.specs.styles[tv.selectedStyle] || tv.specs.styles[0];
const v = style && style.variants[tv.selectedVariant];
if(v) { enrichedVariant[key + '_width'] = v.width_mm || 0; enrichedVariant[key + '_style'] = tv.selectedStyle; }
} else if(tv.specs.type === 'width_variants') {
const v = tv.specs.variants[tv.selectedVariant];
if(v) enrichedVariant[key + '_width'] = v.width_mm || 0;
}
}
});
_spTillval.forEach(tv => {
// 2) Kör tillvalets EGNA regler med berikad variant-data (inkl andra tillvals val)
const tvRules = (tv.specs && tv.specs.rules) || [];
const tvResult = tvRules.length ? peApplyRules(tvRules, enrichedVariant, []) : {};
const selfResult = tvResult[''] || tvResult['__product'] || null;
// Merge: förälderns regel först, sedan tillvalets egen
const r = ruleResult[tv.id] || {};
if(selfResult) { Object.keys(selfResult).forEach(k => { if(selfResult[k] !== null && selfResult[k] !== undefined) r[k] = selfResult[k]; }); }
if(Object.keys(r).length) {
tv.excluded = !!r.exkluderad;
tv.included = !!r.inkluderad;
tv.computedPrice = r.pris !== null && r.pris !== undefined ? r.pris : null;
if(r.antal !== null && r.antal !== undefined) tv.qty = r.antal; else tv.qty = 1;
if(r.max !== undefined) tv.max = r.max;
if(r.min !== undefined) tv.min = r.min;
if(r.langd !== undefined) { tv.computedLangd = r.langd; if(tv.unit === "m" || tv.unit === "löpmeter") tv.userQty = Math.round(r.langd) / 1000; }
if(r.bredd !== undefined) tv.computedBredd = r.bredd;
if(tv.included) tv.checked = true;
if(tv.excluded) tv.checked = false;
} else {
tv.excluded = false; tv.included = false; tv.computedPrice = null; tv.qty = 1;
delete tv.max; delete tv.min; delete tv.computedLangd; delete tv.computedBredd;
}
});
spRenderTillval();
spUpdatePrice();
}
function spRenderTillval() {
const el = document.getElementById('spTillvalArea');
if(!el) return;
const visible = _spTillval.filter(tv => !tv.excluded && !tv.hidden);
if(!visible.length) { el.innerHTML = ''; return; }
el.innerHTML = '<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Tillval</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">'
+ visible.map(tv => {
const realIdx = _spTillval.indexOf(tv);
let price = tv.computedPrice !== null ? tv.computedPrice : tv.price;
// Override price from selected variant
if(tv.specs && tv.selectedVariant !== null && tv.selectedVariant !== undefined) {
if(tv.specs.type === 'style_width_variants') {
const sv = tv.specs.styles[tv.selectedStyle]?.variants[tv.selectedVariant];
if(sv && sv.price != null) price = sv.price;
} else if(tv.specs.type === 'width_variants') {
const wv = tv.specs.variants[tv.selectedVariant];
if(wv && wv.price != null) price = wv.price;
}
}
const effectiveQty = tv.unit === 'm²' ? (tv.userL * tv.userW) : (tv.unit !== 'st' && tv.userQty > 0 ? tv.userQty : tv.qty);
const totalPrice = price * (effectiveQty || (tv.unit === 'st' ? 1 : 0));
const unitLabel = tv.unit && tv.unit !== 'st' ? ' <span style="color:#94a3b8;font-weight:400;font-size:11px">('+price.toLocaleString('sv-SE')+' kr/'+tv.unit+')</span>' : '';
// Build variant selector for tillval with specs
let variantSelector = '';
if(tv.specs && tv.specs.type === 'style_width_variants' && tv.checked) {
const styles = tv.specs.styles;
const selStyle = styles[tv.selectedStyle] || styles[0];
variantSelector = '<div style="margin-top:8px;display:flex;flex-direction:column;gap:6px" onclick="event.preventDefault();event.stopPropagation()">'
+'<div style="display:flex;gap:4px;flex-wrap:wrap">'
+ styles.map((s, si) => '<button onclick="event.preventDefault();spSetTillvalStyle('+realIdx+','+si+')" style="padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;border:1.5px solid '+(si===tv.selectedStyle?'#024550':'#e5e7eb')+';background:'+(si===tv.selectedStyle?'#024550':'#fff')+';color:'+(si===tv.selectedStyle?'#fff':'#334155')+'">'+s.style+'</button>').join('')
+'</div>'
+'<select onchange="spSetTillvalVariant('+realIdx+',this.value)" style="padding:6px 10px;border:1.5px solid '+(tv.selectedVariant===null?'#f59e0b':'#e5e7eb')+';border-radius:6px;font-size:12px;font-family:inherit;background:'+(tv.selectedVariant===null?'#fffbeb':'#fff')+'">'
+'<option value="">⚠ Välj bredd (obligatorisk)...</option>'
+ selStyle.variants.map((v, vi) => {
const lbl = (v.label || (v.width_mm ? v.width_mm + ' mm' : (v.length_mm ? v.length_mm + ' mm' : '')));
const pr = v.price != null ? ' — ' + v.price.toFixed(2) + ' kr/' + tv.unit : ' — pris saknas';
var isDefault = tv.selectedVariant === null && tv.specs.default_width && v.width_mm == tv.specs.default_width; if(isDefault) tv.selectedVariant = vi;
return '<option value="'+vi+'"'+((vi===tv.selectedVariant)?' selected':'')+'>'+lbl+pr+'</option>';
}).join('')
+'</select></div>';
} else if(tv.specs && tv.specs.type === 'width_variants' && tv.checked) {
variantSelector = '<div style="margin-top:8px" onclick="event.preventDefault();event.stopPropagation()">'
+'<select onchange="spSetTillvalVariant('+realIdx+',this.value)" style="padding:6px 10px;border:1.5px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;background:#fff;width:100%">'
+'<option value="">Välj variant...</option>'
+ tv.specs.variants.map((v, vi) => {
const lbl = v.label || (v.width_mm ? v.width_mm + ' mm' : (v.length_mm ? v.length_mm + ' mm' : ''));
const pr = v.price != null ? ' — ' + v.price.toFixed(2) + ' kr' : ' — pris saknas';
return '<option value="'+vi+'"'+(vi===tv.selectedVariant?' selected':'')+'>'+lbl+pr+'</option>';
}).join('')
+'</select></div>';
}
// Build quantity input based on unit
let qtyInput = '';
if(tv.unit === 'm²') {
qtyInput = '<div style="display:flex;align-items:center;gap:4px;margin-top:6px" onclick="event.preventDefault();event.stopPropagation()">'
+'<input type="number" min="0" step="0.1" value="'+(tv.userL||'')+'" placeholder="L" oninput="spSetTillvalDim('+realIdx+',\'L\',this.value)" style="width:56px;padding:5px 6px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;text-align:center">'
+'<span style="font-size:11px;color:#94a3b8">×</span>'
+'<input type="number" min="0" step="0.1" value="'+(tv.userW||'')+'" placeholder="B" oninput="spSetTillvalDim('+realIdx+',\'W\',this.value)" style="width:56px;padding:5px 6px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit;text-align:center">'
+'<span style="font-size:11px;color:#64748b">= '+(tv.userL&&tv.userW?(tv.userL*tv.userW).toFixed(1):'0')+' m²</span>'
+'</div>';
} else if(tv.unit !== 'st') {
qtyInput = '<div style="display:flex;align-items:center;gap:6px;margin-top:6px" onclick="event.preventDefault();event.stopPropagation()">'
+'<input type="number" min="0" step="0.1" value="'+(tv.userQty||'')+'" placeholder="Antal" oninput="spSetTillvalQty('+realIdx+',this.value)" style="width:70px;padding:5px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;font-family:inherit">'
+'<span style="font-size:11px;color:#64748b">'+tv.unit+'</span>'
+'</div>';
} else if(tv.qty > 1) {
qtyInput = '';
// show qty from rules inline
}
const qtyLabel = (tv.unit === 'st' && tv.qty > 1) ? ' <span style="color:#94a3b8;font-weight:400">×'+tv.qty+'</span>' : '';
return '<div style="padding:10px 14px;background:'+(tv.checked?'#f0fdf4':'#f8f9fa')+';border:1px solid '+(tv.checked?'#bbf7d0':'#e5e7eb')+';border-radius:10px;margin-bottom:6px;transition:all .15s">'
+'<label style="display:flex;align-items:'+(qtyInput?'flex-start':'center')+';gap:10px;cursor:'+(tv.included?'default':'pointer')+'">'
+'<input type="checkbox" '+(tv.checked?'checked':'')+' '+(tv.included?'disabled':'')+' onchange="spToggleTillval('+realIdx+')" style="width:18px;height:18px;accent-color:#059669;flex-shrink:0;margin-top:'+(qtyInput?'2px':'0')+'">'
+'<div style="flex:1"><div style="display:flex;align-items:center;justify-content:space-between">'
+'<span style="font-size:13px;font-weight:500;color:#334155">'+tv.name+qtyLabel+unitLabel+(tv.computedBredd?' <span style="color:#0284c7;font-size:11px;font-weight:400">B:'+Math.round(tv.computedBredd)+' mm</span>':'')+(tv.computedLangd?' <span style="color:#0284c7;font-size:11px;font-weight:400">L:'+Math.round(tv.computedLangd)+' mm</span>':'')+'</span>'
+'<span style="font-size:13px;font-weight:700;color:'+(tv.checked?'#059669':'#94a3b8')+'">'+totalPrice.toLocaleString('sv-SE')+' kr</span>'
+'</div>'+(variantSelector||'')+(qtyInput||'')+'</div>'
+'</label>'
+'</div>';
}).join('') + '</div>';
}
function spSetTillvalStyle(idx, styleIdx) {
_spTillval[idx].selectedStyle = styleIdx;
_spTillval[idx].selectedVariant = null;
spApplyRulesAndRender();
}
function spSetTillvalVariant(idx, val) {
_spTillval[idx].selectedVariant = val !== '' ? parseInt(val) : null;
spApplyRulesAndRender();
}
function spSetTillvalQty(idx, val) {
_spTillval[idx].userQty = parseFloat(val) || 0;
spRenderTillval();
spUpdatePrice();
}
function spSetTillvalDim(idx, dim, val) {
if(dim === 'L') _spTillval[idx].userL = parseFloat(val) || 0;
else _spTillval[idx].userW = parseFloat(val) || 0;
spRenderTillval();
spUpdatePrice();
}
function spToggleTillval(idx) {
if(_spTillval[idx].included || _spTillval[idx].excluded) return;
_spTillval[idx].checked = !_spTillval[idx].checked;
spApplyRulesAndRender();
}
function spSelectVariant(idx) {
_spSelectedVariant = idx;
document.querySelectorAll('.sp-variant-chip').forEach(btn => {
const i = parseInt(btn.dataset.idx);
btn.style.background = i===idx ? '#024550' : '#f8f9fa';
btn.style.color = i===idx ? '#fff' : '#334155';
btn.style.borderColor = i===idx ? '#024550' : '#e5e7eb';
});
spApplyRulesAndRender();
}
let _spSelectedWidthOnly = null;
function spSelectWidthOnly(idx) {
_spSelectedWidthOnly = idx;
document.querySelectorAll('.sp-wo-chip').forEach((btn, i) => {
btn.style.background = i===idx ? '#024550' : '#f8f9fa';
btn.style.color = i===idx ? '#fff' : '#334155';
btn.style.borderColor = i===idx ? '#024550' : '#e5e7eb';
});
spUpdatePrice();
}
let _spSelectedWindowSize = null;
let _spSelectedStyle = 0;
let _spSelectedWidth = null;
function spSelectStyle(idx) {
_spSelectedStyle = idx;
_spSelectedWidth = null;
document.querySelectorAll('.sp-style-chip').forEach((btn, i) => {
btn.style.background = i===idx ? '#024550' : '#f8f9fa';
btn.style.color = i===idx ? '#fff' : '#334155';
btn.style.borderColor = i===idx ? '#024550' : '#e5e7eb';
});
spRenderWidthChips();
document.getElementById('spStyleResult').style.display = 'none';
spUpdatePrice();
}
function spRenderWidthChips() {
const el = document.getElementById('spWidthChips');
if(!el || !_spProduct || !_spProduct.specs || _spProduct.specs.type !== 'style_width_variants') return;
const style = _spProduct.specs.styles[_spSelectedStyle];
if(!style) return;
el.innerHTML = style.variants.map((v, i) => {
const label = v.label || (v.width_mm ? v.width_mm + ' mm' : (v.length_mm ? v.length_mm + ' mm' : ''));
const priceStr = v.price != null ? v.price.toFixed(2) + ' kr' : '–';
return '<button onclick="spSelectWidth('+i+')" class="sp-width-chip" data-idx="'+i+'" style="padding:6px 14px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s;background:#f8f9fa;color:#334155;border:2px solid #e5e7eb;display:flex;flex-direction:column;align-items:center;gap:2px">'
+'<span>'+label+'</span>'
+'<span style="font-size:10px;font-weight:400;color:#64748b">'+priceStr+'</span>'
+'</button>';
}).join('');
}
function spSelectWidth(idx) {
_spSelectedWidth = idx;
document.querySelectorAll('.sp-width-chip').forEach((btn, i) => {
btn.style.background = i===idx ? '#024550' : '#f8f9fa';
btn.style.color = i===idx ? '#fff' : '#334155';
btn.style.borderColor = i===idx ? '#024550' : '#e5e7eb';
});
const style = _spProduct.specs.styles[_spSelectedStyle];
const v = style.variants[idx];
const resEl = document.getElementById('spStyleResult');
if(resEl && v) {
const label = v.label || (v.width_mm ? v.width_mm + ' mm' : (v.length_mm ? v.length_mm + ' mm' : ''));
if(v.price != null) {
resEl.innerHTML = '<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:12px;display:flex;justify-content:space-between;align-items:center">'
+'<div><span style="font-weight:600;color:#334155">'+style.style+' '+label+'</span></div>'
+'<div style="font-size:20px;font-weight:800;color:#024550">'+v.price.toFixed(2)+' kr/'+(_spProduct.unit||'st')+'</div></div>';
} else {
resEl.innerHTML = '<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:10px;padding:12px;text-align:center;color:#d97706;font-weight:600;font-size:13px">Pris ej angivet – kontakta Hedlunds</div>';
}
resEl.style.display = 'block';
}
spUpdatePrice();
}
function spWinQty(delta) {
const el = document.getElementById('spWinAntal');
if(!el) return;
let v = parseInt(el.value) || 1;
v = Math.max(1, v + delta);
el.value = v;
spUpdateWindowSize();
}
function spUpdateWindowSize() {
const p = _spProduct;
if(!p || !p.specs || p.specs.type !== 'window_sizes') return;
const wSel = document.getElementById('spWinWidth');
const hSel = document.getElementById('spWinHeight');
const resEl = document.getElementById('spWinResult');
if(!wSel || !hSel || !resEl) return;
const w = parseInt(wSel.value);
const h = parseInt(hSel.value);
if(!w || !h) {
resEl.style.display = 'none';
_spSelectedWindowSize = null;
spUpdatePrice();
return;
}
// Filter heights based on selected width
const availH = p.specs.sizes.filter(s => s.w === w).map(s => s.h);
Array.from(hSel.options).forEach(opt => {
if(!opt.value) return;
const hv = parseInt(opt.value);
opt.disabled = !availH.includes(hv);
opt.style.color = availH.includes(hv) ? '' : '#ccc';
});
// Filter widths based on selected height
const availW = p.specs.sizes.filter(s => s.h === h).map(s => s.w);
Array.from(wSel.options).forEach(opt => {
if(!opt.value) return;
const wv = parseInt(opt.value);
opt.disabled = !availW.includes(wv);
opt.style.color = availW.includes(wv) ? '' : '#ccc';
});
const match = p.specs.sizes.find(s => s.w === w && s.h === h);
if(match) {
_spSelectedWindowSize = match;
let noteHtml = '';
if(match.note && match.note !== 'None' && match.note !== '') {
const noteColor = match.note.toLowerCase().includes('laminated') ? '#d97706' :
match.note.toLowerCase().includes("can't") || match.note.toLowerCase().includes('no guarantee') ? '#dc2626' : '#64748b';
noteHtml = '<div style="margin-top:6px;padding:6px 10px;border-radius:6px;font-size:12px;font-weight:600;color:'+noteColor+';background:'+(noteColor==='#dc2626'?'#fef2f2':'#fffbeb')+'">'+match.note+'</div>';
}
const antal = parseInt(document.getElementById('spWinAntal')?.value) || 1;
const totalEur = match.price * antal;
const _cr = _currencyRates[p.costCurrency||'EUR'] || _currencyRates['EUR'] || 1;
const _mk = p.markupType==='percent'&&p.markupValue ? (1+p.markupValue/100) : 1;
const _mkA = p.markupType==='amount'&&p.markupValue ? p.markupValue*antal : 0;
const totalSek = Math.round(totalEur * _cr * _mk + _mkA);
resEl.innerHTML = '<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:14px;margin-top:8px">'
+'<div style="display:flex;justify-content:space-between;align-items:center">'
+'<div><div style="font-size:13px;font-weight:600;color:#334155">'+w+' × '+h+' dm <span style="color:#94a3b8;font-weight:400">('+(w*10)+' × '+(h*10)+' cm)</span></div>'
+'<div style="font-size:11px;color:#64748b;margin-top:2px">Area: '+((w*h)/100).toFixed(2)+' m² | Art.nr: '+match.article_id+'</div></div>'
+'<div style="text-align:right"><div style="font-size:22px;font-weight:800;color:#024550">'+totalSek.toLocaleString('sv-SE')+' kr</div>'
+(antal > 1 ? '<div style="font-size:11px;color:#64748b">'+antal+' st × '+Math.round(match.price*_cr*_mk).toLocaleString('sv-SE')+' kr</div>' : '')
+'</div></div>'
+noteHtml
+'</div>';
resEl.style.display = 'block';
} else {
_spSelectedWindowSize = null;
resEl.innerHTML = '<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:10px;padding:14px;margin-top:8px;text-align:center;color:#dc2626;font-size:13px;font-weight:600">Denna storlekskombination finns ej tillgänglig</div>';
resEl.style.display = 'block';
}
spApplyRulesAndRender();
}
// === SPRÖJS ===
let _spSelectedSprojs = 0;
function spSelectSprojs(idx) {
_spSelectedSprojs = idx;
document.querySelectorAll('.sp-sprojs-opt').forEach(el => {
const i = parseInt(el.dataset.idx);
el.style.borderColor = i === idx ? '#024550' : '#e5e7eb';
el.style.background = i === idx ? '#f0fdf4' : '';
});
}
// === FÄRGVÄLJARE ===
const _spColors = [
{ name:'Vit', code:'RAL 9010', hex:'#f0ece4', group:'standard' },
{ name:'Röd', code:'RAL 3011', hex:'#781f1c', group:'kulör' },
{ name:'Antracitgrå', code:'RAL 7016', hex:'#383e42', group:'kulör' },
{ name:'Matt svart', code:'RAL 9005M', hex:'#1a1a1a', group:'kulör' },
{ name:'Grafitsvart', code:'RAL 9011', hex:'#262626', group:'kulör' },
{ name:'Ljusgrå', code:'RAL 7035', hex:'#b0b0a8', group:'kulör' },
{ name:'Kvartsgrå', code:'RAL 7039', hex:'#6b6560', group:'kulör' },
{ name:'Basaltgrå', code:'RAL 7012', hex:'#585c5e', group:'kulör' },
{ name:'Mörkbrun', code:'RAL 8019', hex:'#3b3332', group:'kulör' },
{ name:'Svart', code:'RAL 9005', hex:'#0e0e10', group:'kulör' },
{ name:'Grön', code:'RAL 6009', hex:'#1f3a28', group:'kulör' },
{ name:'Blå', code:'RAL 5011', hex:'#1a2b3c', group:'kulör' },
];
let _spSelectedColors = { insida: _spColors[0], utsida: _spColors[0] };
function spOpenColorPicker(side) {
document.getElementById('spColorPickerModal')?.remove();
const isInsida = side === 'insida';
const current = _spSelectedColors[side];
const title = isInsida ? 'Kulör insida' : 'Kulör utsida';
const note = isInsida ? 'Insidan är normalt alltid vit.' : 'Välj vilken färg du ska ha på utsidan.';
const modal = document.createElement('div');
modal.id = 'spColorPickerModal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.onclick = e => { if(e.target === modal) modal.remove(); };
modal.innerHTML = '<div style="background:#fff;border-radius:16px;max-width:600px;width:100%;max-height:80vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.3)">'
+'<div style="padding:20px 24px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center">'
+'<div><h3 style="font-size:18px;font-weight:700;margin:0;color:#1a1a1a">'+title+'</h3>'
+'<div style="font-size:12px;color:#94a3b8;margin-top:2px">'+current.name+'</div></div>'
+'<button onclick="document.getElementById(\'spColorPickerModal\').remove()" style="display:flex;align-items:center;gap:4px;background:none;border:none;cursor:pointer;font-size:14px;font-weight:700;color:#334155;font-family:inherit">Stäng <svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2"><polyline points="18 15 12 9 6 15"/></svg></button>'
+'</div>'
+'<div style="padding:20px 24px">'
+'<p style="font-size:13px;color:#64748b;margin:0 0 16px">'+note+'</p>'
+'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:10px">'
+ _spColors.map((c, i) => {
const selected = current.code === c.code;
return '<div onclick="spSelectColor(\''+side+'\','+i+')" style="cursor:pointer;border:2px solid '+(selected?'#024550':'#e5e7eb')+';border-radius:10px;padding:8px;text-align:center;transition:all .15s;background:'+(selected?'#f0fdf4':'#fff')+'" onmouseover="this.style.borderColor=\'#94a3b8\'" onmouseout="this.style.borderColor=\''+(selected?'#024550':'#e5e7eb')+'\'">'
+'<div style="width:100%;aspect-ratio:1;background:'+c.hex+';border-radius:6px;border:1px solid rgba(0,0,0,.1);margin-bottom:6px"></div>'
+'<div style="font-size:11px;font-weight:600;color:#1a1a1a">'+c.name+'</div>'
+'<div style="font-size:9px;color:#94a3b8">'+c.code+'</div>'
+'</div>';
}).join('')
+'</div></div></div>';
document.body.appendChild(modal);
}
function spSelectColor(side, idx) {
_spSelectedColors[side] = _spColors[idx];
const c = _spColors[idx];
document.getElementById('spColor'+(side==='insida'?'Insida':'Utsida')+'Swatch').style.background = c.hex;
document.getElementById('spColor'+(side==='insida'?'Insida':'Utsida')+'Name').textContent = c.name;
document.getElementById('spColor'+(side==='insida'?'Insida':'Utsida')+'Code').textContent = c.code;
document.getElementById('spColorPickerModal')?.remove();
}
function spUpdatePrice() {
const el = document.getElementById('spPriceArea');
if(!el) return;
const p = _spProduct;
const variants = (p.specs && p.specs.variants) || [];
const rules = (p.specs && p.specs.rules) || [];
const sv = variants[_spSelectedVariant] || {};
const ruleResult = peApplyRules(rules, sv, _spTillval);
let displayPrice = p.price, greenDeduction = 0, isPerUnit = false, unitLabel = '';
// Width-only override
if(p.specs && p.specs.type === 'width_variants') {
if(_spSelectedWidthOnly !== null) {
const v = p.specs.variants[_spSelectedWidthOnly];
if(v && v.price != null) {
displayPrice = v.price;
unitLabel = 'kr/' + (p.unit || 'st');
isPerUnit = true;
} else {
el.innerHTML = '<div style="text-align:center;color:#d97706;font-size:13px;padding:8px">Pris ej angivet</div>';
return;
}
} else {
el.innerHTML = '<div style="text-align:center;color:#94a3b8;font-size:13px;padding:8px">Välj variant för att se pris</div>';
return;
}
}
// Style+width override
if(p.specs && p.specs.type === 'style_width_variants') {
if(_spSelectedWidth !== null) {
const style = p.specs.styles[_spSelectedStyle];
const v = style && style.variants[_spSelectedWidth];
if(v && v.price != null) {
displayPrice = v.price;
unitLabel = 'kr/' + (p.unit || 'st');
isPerUnit = true;
} else {
el.innerHTML = '<div style="text-align:center;color:#d97706;font-size:13px;padding:8px">Pris ej angivet</div>';
return;
}
} else {
el.innerHTML = '<div style="text-align:center;color:#94a3b8;font-size:13px;padding:8px">Välj utförande och bredd för att se pris</div>';
return;
}
}
// Window sizes override
if(p.specs && p.specs.type === 'window_sizes') {
if(_spSelectedWindowSize) {
const winAntal = parseInt(document.getElementById('spWinAntal')?.value) || 1;
let eurPrice = _spSelectedWindowSize.price * winAntal;
// Konvertera till SEK
const currency = p.costCurrency || 'EUR';
const rate = _currencyRates[currency] || _currencyRates['EUR'] || 1;
let sekPrice = eurPrice * rate;
// Applicera påslag
if(p.markupType === 'percent' && p.markupValue) {
sekPrice = sekPrice * (1 + p.markupValue / 100);
} else if(p.markupType === 'amount' && p.markupValue) {
sekPrice = sekPrice + (p.markupValue * winAntal);
}
displayPrice = Math.round(sekPrice);
unitLabel = 'kr';
isPerUnit = true;
} else {
el.innerHTML = '<div style="text-align:center;color:#94a3b8;font-size:13px;padding:8px">Välj bredd och höjd för att se pris</div>';
return;
}
}
var spQtyEl=document.getElementById('spQtyInput');var spQty=spQtyEl?parseInt(spQtyEl.value)||1:1;
if(variants.length) {
if(sv.totalt !== undefined) {
displayPrice = parseFloat(sv.totalt) || 0;
if(sv.gron_teknik !== undefined) greenDeduction = parseFloat(sv.gron_teknik) || 0;
} else if(sv.pris_per_m2 !== undefined) {
displayPrice = parseFloat(sv.pris_per_m2) || 0;
isPerUnit = true; unitLabel = 'kr/m²';
} else if(sv.price !== undefined) {
displayPrice = parseFloat(sv.price) || 0;
} else {
displayPrice = p.price;
}
}
if(p.unit && p.unit!=='st' && spQty>1 && !isPerUnit) displayPrice=displayPrice*spQty;
if(ruleResult['__product'] && ruleResult['__product'].pris !== null) {
displayPrice = ruleResult['__product'].pris;
}
let tillvalTotal = 0;
_spTillval.filter(tv => tv.checked && !tv.excluded).forEach(tv => {
let pr = tv.computedPrice !== null ? tv.computedPrice : tv.price;
if(tv.specs && tv.selectedVariant !== null && tv.selectedVariant !== undefined) {
if(tv.specs.type === 'style_width_variants') {
const sv = tv.specs.styles[tv.selectedStyle]?.variants[tv.selectedVariant];
if(sv && sv.price != null) pr = sv.price;
} else if(tv.specs.type === 'width_variants') {
const wv = tv.specs.variants[tv.selectedVariant];
if(wv && wv.price != null) pr = wv.price;
}
}
const eq = tv.unit === 'm²' ? (tv.userL * tv.userW) : (tv.unit && tv.unit !== 'st' && tv.userQty > 0 ? tv.userQty : tv.qty);
tillvalTotal += pr * (eq || (tv.unit === 'st' || !tv.unit ? 1 : 0));
});
let html = '';
if(isPerUnit) {
html = '<div style="display:flex;justify-content:space-between;align-items:baseline">'
+'<span style="font-size:15px;font-weight:700;color:#1a1a1a">Pris</span>'
+'<span style="font-size:24px;font-weight:800;color:#024550">'+displayPrice.toLocaleString('sv-SE')+' '+unitLabel+'</span>'
+'</div>';
if(tillvalTotal > 0) {
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;padding-top:8px;border-top:1px solid #e5e7eb">'
+'<span style="font-size:13px;color:#64748b">Tillval</span>'
+'<span style="font-size:14px;font-weight:600;color:#334155">+'+tillvalTotal.toLocaleString('sv-SE')+' kr</span>'
+'</div>';
}
} else {
// Hidden tillval (installation etc)
var hiddenTotal=0, hiddenItems=[];
_spTillval.filter(function(tv){return tv.checked && !tv.excluded && tv.hidden;}).forEach(function(tv){
var hiPrice=tv.price*(tv.unit===p.unit&&spQty>1?spQty:1); hiddenTotal+=hiPrice; hiddenItems.push({name:tv.name,price:hiPrice});
if(tv.childTillval) tv.childTillval.filter(function(ch){return ch.obligatorisk;}).forEach(function(ch){
hiddenTotal+=ch.price; hiddenItems.push({name:ch.name,price:ch.price,isChild:true});
});
});
// Product name + hidden items + totalt
if(hiddenTotal > 0) {
var arbete = displayPrice - hiddenTotal;
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px"><span style="font-size:13px;color:#64748b">'+p.name+'</span><span style="font-size:15px;font-weight:600;color:#334155">'+arbete.toLocaleString('sv-SE')+' kr</span></div>';
hiddenItems.forEach(function(hi){
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;padding-left:'+(hi.isChild?'12':'0')+'px"><span style="font-size:13px;color:#64748b">'+hi.name+'</span><span style="font-size:15px;font-weight:600;color:#334155">'+hi.price.toLocaleString('sv-SE')+' kr</span></div>';
});
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;padding-top:6px;border-top:1px solid #e5e7eb"><span style="font-size:13px;font-weight:600;color:#334155">Totalt</span><span style="font-size:15px;font-weight:700;color:#334155">'+displayPrice.toLocaleString('sv-SE')+' kr</span></div>';
} else {
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px"><span style="font-size:13px;color:#64748b">'+p.name+'</span><span style="font-size:15px;font-weight:600;color:#334155">'+displayPrice.toLocaleString('sv-SE')+' kr</span></div>';
}
if(greenDeduction > 0) {
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px"><span style="font-size:13px;color:#059669">Grönt teknik-avdrag</span><span style="font-size:15px;font-weight:600;color:#059669">-'+greenDeduction.toLocaleString('sv-SE')+' kr</span></div>';
}
// Itemized tillval
if(tillvalTotal > 0) {
html += '<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;margin:4px 0">Tillval</div>';
_spTillval.filter(function(tv){return tv.checked && !tv.excluded && !tv.hidden;}).forEach(function(tv){
var tvPr = tv.price;
if(tv.specs && tv.selectedVariant !== null && tv.selectedVariant !== undefined && tv.specs.variants && tv.specs.variants[tv.selectedVariant]) {
var sv2 = tv.specs.variants[tv.selectedVariant];
tvPr = sv2.totalt || sv2.price || sv2.att_betala || tvPr;
}
var tvEq = (tv.unit && tv.unit !== 'st' && tv.userQty > 0) ? tv.userQty : (tv.qty||1);
var tvLine = tvPr * tvEq;
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;padding-left:8px"><span style="font-size:12px;color:#64748b">'+tv.name+'</span><span style="font-size:13px;font-weight:600;color:#334155">+'+tvLine.toLocaleString('sv-SE')+' kr</span></div>';
if(tv.childTillval) tv.childTillval.filter(function(ch){return ch.obligatorisk;}).forEach(function(ch){
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;padding-left:16px"><span style="font-size:11px;color:#94a3b8">'+ch.name+'</span><span style="font-size:12px;font-weight:600;color:#94a3b8">+'+ch.price.toLocaleString('sv-SE')+' kr</span></div>';
});
});
}
const grandTotal = displayPrice - greenDeduction + tillvalTotal;
html += '<div style="border-top:2px solid #e5e7eb;padding-top:10px;margin-top:4px;display:flex;justify-content:space-between;align-items:center">'
+'<span style="font-size:15px;font-weight:700;color:#1a1a1a">Att betala</span>'
+'<span style="font-size:24px;font-weight:800;color:#024550">'+grandTotal.toLocaleString('sv-SE')+' kr</span>'
+'</div>';
}
el.innerHTML = html;
}
function spSetImage(idx) {
_spCurrentImage = idx;
const mainImg = document.getElementById('spMainImage');
if(mainImg) mainImg.src = _spImages[idx];
document.querySelectorAll('#spThumbnails img').forEach((t, i) => { t.style.borderColor = i===idx ? '#024550' : '#e5e7eb'; });
document.querySelectorAll('#spDots span').forEach((d, i) => { d.style.background = i===idx ? '#024550' : '#cbd5e1'; });
}
// === KALKYL VARUKORG (CART) ===
let _cartItems = [];
function _cartLoad() {
try { _cartItems = JSON.parse(sessionStorage.getItem('solarCart') || '[]'); } catch(e) { _cartItems = []; }
}
function _cartSave() {
sessionStorage.setItem('solarCart', JSON.stringify(_cartItems));
cartUpdateBadge();
}
function cartAddFromModal() {
if(!_spProduct) return;
const p = _spProduct;
const variants = (p.specs && p.specs.variants) || [];
const sv = variants[_spSelectedVariant] || {};
const rules = (p.specs && p.specs.rules) || [];
const ruleResult = peApplyRules(rules, sv, _spTillval);
// Determine variant label
let variantLabel = '';
if(variants.length > 1) {
const first = variants[0];
if(first.kwh !== undefined) variantLabel = sv.kwh + ' kWh';
else if(first.paneler !== undefined) variantLabel = sv.paneler + ' paneler';
else if(first.cm !== undefined) variantLabel = sv.cm + ' cm';
else if(first.material !== undefined) variantLabel = sv.material;
else variantLabel = Object.values(sv)[0];
}
// Calculate prices
let basePrice = p.price, greenDeduction = 0, isPerUnit = false;
if(variants.length) {
if(sv.totalt !== undefined) { basePrice = parseFloat(sv.totalt) || 0; if(sv.gron_teknik !== undefined) greenDeduction = parseFloat(sv.gron_teknik) || 0; }
else if(sv.pris_per_m2 !== undefined) { basePrice = parseFloat(sv.pris_per_m2) || 0; isPerUnit = true; }
else if(sv.price !== undefined) { basePrice = parseFloat(sv.price) || 0; }
}
if(ruleResult['__product'] && ruleResult['__product'].pris !== null) basePrice = ruleResult['__product'].pris;
let tillvalTotal = 0;
// Window size info
let windowSize = null;
if(p.specs && p.specs.type === 'window_sizes' && _spSelectedWindowSize) {
const winAntal = parseInt(document.getElementById('spWinAntal')?.value) || 1;
const _cr = _currencyRates[p.costCurrency||'EUR'] || _currencyRates['EUR'] || 1;
const _mk = p.markupType==='percent'&&p.markupValue ? (1+p.markupValue/100) : 1;
const _mkA = p.markupType==='amount'&&p.markupValue ? p.markupValue*winAntal : 0;
const eurPrice = _spSelectedWindowSize.price * winAntal;
basePrice = Math.round(eurPrice * _cr * _mk + _mkA);
windowSize = {
w: _spSelectedWindowSize.w,
h: _spSelectedWindowSize.h,
antal: winAntal,
article_id: _spSelectedWindowSize.article_id,
eurPrice: eurPrice,
sekPrice: basePrice
};
variantLabel = (_spSelectedWindowSize.w*10)+'x'+(_spSelectedWindowSize.h*10)+' cm' + (winAntal > 1 ? ' x'+winAntal : '');
}
// Style/width selection for style_width_variants
let styleWidthInfo = null;
if(p.specs && p.specs.type === 'style_width_variants' && _spSelectedWidth !== null) {
const style = p.specs.styles[_spSelectedStyle];
const variant = style && style.variants[_spSelectedWidth];
styleWidthInfo = { style: style?.style, width_mm: variant?.width_mm, price: variant?.price };
variantLabel = (style?.style || '') + ' ' + (variant?.width_mm || '') + ' mm';
}
// Tillval with their selected variants
const tillvalDetailed = _spTillval.filter(tv => tv.checked && !tv.excluded).map(tv => {
let pr = tv.computedPrice !== null ? tv.computedPrice : tv.price;
let selectedStyle = null, selectedWidth = null;
if(tv.specs && tv.selectedVariant !== null && tv.selectedVariant !== undefined) {
if(tv.specs.type === 'style_width_variants') {
const s = tv.specs.styles[tv.selectedStyle];
const v = s && s.variants[tv.selectedVariant];
if(v && v.price != null) pr = v.price;
selectedStyle = s?.style;
selectedWidth = v?.width_mm;
} else if(tv.specs.type === 'width_variants') {
const v = tv.specs.variants[tv.selectedVariant];
if(v && v.price != null) pr = v.price;
selectedWidth = v?.width_mm || v?.length_mm;
}
}
const eq = tv.unit === 'm\u00b2' ? (tv.userL * tv.userW) : (tv.unit && tv.unit !== 'st' && tv.userQty > 0 ? tv.userQty : tv.qty);
const effectiveQty = eq || (tv.unit === 'st' || !tv.unit ? 1 : 0);
const t = pr * effectiveQty;
tillvalTotal += t;
return {
id: tv.id, name: tv.name, price: pr, qty: effectiveQty, total: t,
unit: tv.unit, computedLangd: tv.computedLangd || null, computedBredd: tv.computedBredd || null,
selectedStyle: selectedStyle, selectedWidth: selectedWidth
};
});
// Spröjs
const sprojsLabels = ['Inga','1V, 1H','1V, 2H','2V, 2H'];
const sprojs = { index: _spSelectedSprojs || 0, label: sprojsLabels[_spSelectedSprojs || 0] };
// Färg
const farg = {
insida: _spSelectedColors ? { ..._spSelectedColors.insida } : { name:'Vit', code:'RAL 9010' },
utsida: _spSelectedColors ? { ..._spSelectedColors.utsida } : { name:'Vit', code:'RAL 9010' }
};
const total = basePrice - greenDeduction + tillvalTotal;
_cartItems.push({
productId: p.id,
productName: p.name,
catLabel: p.catLabel,
img: p.img || '',
variantIdx: _spSelectedVariant,
variantLabel: variantLabel,
windowSize: windowSize,
styleWidthInfo: styleWidthInfo,
tillval: tillvalDetailed,
basePrice: basePrice,
greenDeduction: greenDeduction,
tillvalTotal: tillvalTotal,
total: total,
isPerUnit: isPerUnit,
sprojs: sprojs,
farg: farg
});
_cartSave();
cartToast('Tillagd i kalkyl!');
}
function cartRemove(idx) {
_cartItems.splice(idx, 1);
_cartSave();
cartRender();
}
function cartClear() {
_cartItems = [];
_cartSave();
cartRender();
}
function cartUpdateBadge() {
let btn = document.getElementById('cartFloatBtn');
if(!btn) {
btn = document.createElement('div');
btn.id = 'cartFloatBtn';
btn.onclick = () => cartTogglePanel();
btn.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:9990;background:#024550;color:#fff;border-radius:16px;padding:12px 20px;cursor:pointer;box-shadow:0 8px 24px rgba(2,69,80,.4);display:flex;align-items:center;gap:8px;font-family:Inter,sans-serif;font-size:14px;font-weight:700;transition:transform .2s,opacity .2s;user-select:none';
btn.innerHTML = '<svg viewBox="0 0 24 24" style="width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg><span id="cartBadgeText">Kalkyl (0)</span>';
document.body.appendChild(btn);
}
const count = _cartItems.length;
document.getElementById('cartBadgeText').textContent = 'Kalkyl (' + count + ')';
btn.style.transform = count > 0 ? 'scale(1)' : 'scale(0)';
btn.style.opacity = count > 0 ? '1' : '0';
btn.style.pointerEvents = count > 0 ? 'auto' : 'none';
}
function cartTogglePanel() {
let panel = document.getElementById('cartPanel');
if(panel) { panel.remove(); return; }
panel = document.createElement('div');
panel.id = 'cartPanel';
panel.style.cssText = 'position:fixed;top:0;right:0;width:400px;max-width:90vw;height:100vh;background:#fff;z-index:9998;box-shadow:-8px 0 32px rgba(0,0,0,.15);display:flex;flex-direction:column;font-family:Inter,sans-serif;animation:cartSlideIn .25s ease-out';
// Add animation keyframe if not exists
if(!document.getElementById('cartAnimStyle')) {
const style = document.createElement('style');
style.id = 'cartAnimStyle';
style.textContent = '@keyframes cartSlideIn{from{transform:translateX(100%)}to{transform:translateX(0)}}';
document.head.appendChild(style);
}
panel.innerHTML = '<div style="padding:16px 20px;border-bottom:1px solid #f1f5f9;display:flex;justify-content:space-between;align-items:center">'
+'<h3 style="margin:0;font-size:16px;font-weight:800;color:#1a1a1a">Kalkyl</h3>'
+'<button onclick="document.getElementById(\'cartPanel\').remove()" style="background:none;border:none;cursor:pointer;color:#94a3b8;font-size:22px;line-height:1;padding:4px">×</button>'
+'</div>'
+'<div id="cartPanelItems" style="flex:1;overflow-y:auto;padding:12px 20px"></div>'
+'<div id="cartPanelFooter" style="padding:16px 20px;border-top:1px solid #f1f5f9"></div>';
document.body.appendChild(panel);
// Backdrop
const backdrop = document.createElement('div');
backdrop.id = 'cartBackdrop';
backdrop.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:9997';
backdrop.onclick = () => { panel.remove(); backdrop.remove(); };
document.body.appendChild(backdrop);
panel._backdrop = backdrop;
const origRemove = panel.remove.bind(panel);
panel.remove = function() { backdrop.remove(); origRemove(); };
cartRender();
}
function cartRender() {
const itemsEl = document.getElementById('cartPanelItems');
const footerEl = document.getElementById('cartPanelFooter');
if(!itemsEl || !footerEl) return;
if(!_cartItems.length) {
itemsEl.innerHTML = '<div style="text-align:center;padding:40px 0;color:#94a3b8"><svg viewBox="0 0 24 24" style="width:40px;height:40px;stroke:currentColor;fill:none;stroke-width:1.5;margin-bottom:8px"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg><div style="font-size:13px">Varukorgen är tom</div><div style="font-size:12px;margin-top:4px">Lägg till produkter från katalogen</div></div>';
footerEl.innerHTML = '';
return;
}
let grandTotal = 0;
itemsEl.innerHTML = _cartItems.map((item, idx) => {
grandTotal += item.total;
const imgTag = item.img ? '<img src="'+item.img+'" style="width:48px;height:48px;object-fit:cover;border-radius:8px;flex-shrink:0">' : '<div style="width:48px;height:48px;background:#f1f5f9;border-radius:8px;flex-shrink:0"></div>';
let details = '';
if(item.variantLabel) details += '<div style="font-size:11px;color:#64748b">'+item.variantLabel+'</div>';
if(item.greenDeduction > 0) details += '<div style="font-size:11px;color:#059669">Grönt teknik: -'+item.greenDeduction.toLocaleString('sv-SE')+' kr</div>';
if(item.tillval && item.tillval.length) {
details += item.tillval.map(tv => '<div style="font-size:11px;color:#64748b">+ '+tv.name+': '+tv.total.toLocaleString('sv-SE')+' kr</div>').join('');
}
return '<div style="display:flex;gap:12px;padding:12px 0;border-bottom:1px solid #f8f9fa">'
+ imgTag
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:700;color:#1a1a1a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+item.productName+'</div>'
+'<div style="font-size:10px;color:#94a3b8;margin-bottom:2px">'+item.catLabel+'</div>'
+ details
+'<div style="font-size:14px;font-weight:800;color:#024550;margin-top:4px">'+item.total.toLocaleString('sv-SE')+' kr'+(item.isPerUnit?' /m²':'')+'</div>'
+'</div>'
+'<button onclick="cartRemove('+idx+')" style="background:none;border:none;cursor:pointer;color:#dc2626;padding:4px;flex-shrink:0;align-self:flex-start" title="Ta bort"><svg viewBox="0 0 24 24" style="width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button>'
+'</div>';
}).join('');
footerEl.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">'
+'<span style="font-size:15px;font-weight:700;color:#1a1a1a">Totalt</span>'
+'<span style="font-size:20px;font-weight:800;color:#024550">'+grandTotal.toLocaleString('sv-SE')+' kr</span>'
+'</div>'
+'<button onclick="cartSaveAsQuote()" style="width:100%;padding:12px;background:#024550;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;margin-bottom:8px">Spara som kalkyl</button>'
+'<button onclick="cartClear();cartRender()" style="width:100%;padding:10px;background:#fee2e2;color:#dc2626;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Rensa varukorg</button>';
}
async function cartSaveAsQuote() {
if(!_cartItems.length) return;
const cats = [...new Set(_cartItems.map(i=>i.catLabel))];
const summary = _cartItems.length + ' produkt' + (_cartItems.length>1?'er':'') + ' — ' + cats.join(', ');
const total = _cartItems.reduce((s,i)=>s+i.total, 0);
try {
const res = await fetch('/api/quotes.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({
category: 'produktkatalog',
config_data: JSON.stringify({items: _cartItems}),
total_price: total,
panel_name: summary,
status: 'utkast',
created_by: gStaffId || null
})
});
const data = await res.json();
if(data.error) { alert('Fel: ' + data.error); return; }
cartToast('Kalkyl #' + data.id + ' sparad!');
_cartItems = [];
_cartSave();
cartRender();
} catch(e) { alert('Fel: ' + e.message); }
}
function cartToast(msg) {
const t = document.createElement('div');
t.style.cssText = 'position:fixed;bottom:80px;right:24px;z-index:100000;background:#024550;color:#fff;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:600;font-family:Inter,sans-serif;box-shadow:0 8px 24px rgba(0,0,0,.2);animation:cartSlideIn .2s ease-out;display:flex;align-items:center;gap:8px';
t.innerHTML = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:#10b981;fill:none;stroke-width:2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' + msg;
document.body.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity .3s'; setTimeout(() => t.remove(), 300); }, 2500);
}
// Init cart on page load
_cartLoad();
setTimeout(cartUpdateBadge, 500);
function spOpenFullscreen() {
if(!_spImages.length) return;
const ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.92);display:flex;align-items:center;justify-content:center';
let fsIdx = _spCurrentImage;
const render = () => { ov.querySelector('img').src = _spImages[fsIdx]; };
ov.innerHTML = '<button onclick="this.parentElement.remove()" style="position:absolute;top:20px;right:20px;background:rgba(255,255,255,.15);border:none;color:#fff;width:40px;height:40px;border-radius:50%;font-size:22px;cursor:pointer;z-index:1">×</button>'
+(_spImages.length > 1 ? '<button id="spFsPrev" style="position:absolute;left:20px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,.15);border:none;color:#fff;width:44px;height:44px;border-radius:50%;font-size:22px;cursor:pointer;z-index:1"><svg viewBox="0 0 24 24" style="width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2"><polyline points="15 18 9 12 15 6"/></svg></button>' : '')
+'<img src="'+_spImages[fsIdx]+'" style="max-width:90vw;max-height:90vh;object-fit:contain;border-radius:8px">'
+(_spImages.length > 1 ? '<button id="spFsNext" style="position:absolute;right:20px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,.15);border:none;color:#fff;width:44px;height:44px;border-radius:50%;font-size:22px;cursor:pointer;z-index:1"><svg viewBox="0 0 24 24" style="width:24px;height:24px;stroke:currentColor;fill:none;stroke-width:2"><polyline points="9 18 15 12 9 6"/></svg></button>' : '');
document.body.appendChild(ov);
if(_spImages.length > 1) {
ov.querySelector('#spFsPrev').onclick = e => { e.stopPropagation(); fsIdx = (fsIdx - 1 + _spImages.length) % _spImages.length; render(); };
ov.querySelector('#spFsNext').onclick = e => { e.stopPropagation(); fsIdx = (fsIdx + 1) % _spImages.length; render(); };
}
ov.addEventListener('keydown', e => { if(e.key==='Escape') ov.remove(); if(e.key==='ArrowLeft'&&_spImages.length>1){fsIdx=(fsIdx-1+_spImages.length)%_spImages.length;render();} if(e.key==='ArrowRight'&&_spImages.length>1){fsIdx=(fsIdx+1)%_spImages.length;render();} });
ov.tabIndex = 0; ov.focus();
}
</script>
<!-- Mobil bottom navigation -->
<nav class="mobile-nav" id="mobileNav">
<a data-page="dashboard" class="active"><svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>Hem</a>
<a data-page="faltsalj"><svg viewBox="0 0 24 24"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>Fält</a>
<a data-page="bildgenerering"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>Bild</a>
<a data-page="kunder"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>Kunder</a>
<a data-page="installningar"><svg viewBox="0 0 24 24"><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-2.83 2.83l-.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-4 0v-.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-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.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 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>Inst.</a>
</nav>
<!-- Lightbox för bildförhandsgranskning -->
<div id="imgLightbox" onclick="if(event.target===this)closeLightbox()" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;cursor:zoom-out">
<img id="imgLightboxImg" style="max-width:90vw;max-height:90vh;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.5)">
<button onclick="closeLightbox()" style="position:absolute;top:20px;right:20px;background:rgba(255,255,255,.2);color:#fff;border:none;border-radius:50%;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button>
</div>
<script src="js/solargroup-updates.js"></script>
</body>
</html>