// === SOLARGROUP UPDATES 2026-03-25 ===
// This file ADDS new functionality on top of the existing app
// --- FMT with 2 decimals ---
(function(){
const origFmt = window.fmt;
window.fmt = function(p) {
return p.toLocaleString('sv-SE',{minimumFractionDigits:2,maximumFractionDigits:2})+' kr';
};
})();
// --- Hide Färg/Spröjs for non-window products ---
(function(){
const origShowProductModal = window.showProductModal;
if(origShowProductModal) {
// Patch: after modal renders, hide color/sprojs for non-window products
const origInterval = setInterval(function(){
const modal = document.getElementById('productSalesModal');
if(modal && window._spProduct) {
const p = window._spProduct;
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';
}
clearInterval(origInterval);
}
}, 200);
setTimeout(function(){ clearInterval(origInterval); }, 5000);
}
})();
// --- Category data (loaded by konfigurator.js) ---
window._cfgCategories = [];
// --- Category Editor ---
window.showCategoryEditor = async function() {
var oldModal = document.getElementById('catEditorModal'); if(oldModal) oldModal.remove();
let cats = [];
try {
const r = await fetch('/api/categories.php?all=1');
const d = await r.json();
if(d.success) cats = d.categories;
} catch(e){}
const catCounts = {};
if(typeof catalogProducts !== 'undefined') {
catalogProducts.forEach(p => { catCounts[p.cat] = (catCounts[p.cat]||0) + 1; });
}
function renderList() {
const listHtml = cats.map((c,i) => {
const count = catCounts[c.id] || 0;
const imgThumb = c.image ? '<img src="'+c.image+'" style="width:80px;height:60px;object-fit:cover;border-radius:8px">' : '<div style="width:80px;height:60px;background:#f1f5f9;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#94a3b8">☆</div>';
return '<div style="display:flex;align-items:center;gap:16px;padding:16px;border:1px solid #e5e7eb;border-radius:12px;margin-bottom:8px;background:#fff;transition:all .15s" onmouseover="this.style.borderColor=\'#024550\'" onmouseout="this.style.borderColor=\'#e5e7eb\'">'
+imgThumb
+'<div style="flex:1;min-width:0">'
+'<div style="font-weight:600;font-size:14px">'+c.label+'</div>'
+'<div style="font-size:11px;color:#64748b">'+(c.description||'')+'</div>'
+'<div style="display:flex;gap:4px;margin-top:3px">'
+(count?'<span style="font-size:10px;background:#e0f2fe;color:#0284c7;padding:2px 6px;border-radius:4px">'+count+' produkter</span>':'<span style="font-size:10px;background:#f1f5f9;color:#94a3b8;padding:2px 6px;border-radius:4px">0 produkter</span>')
+(parseInt(c.is_tillval)?'<span style="font-size:10px;background:#fef3c7;color:#92400e;padding:2px 6px;border-radius:4px">Tillval</span>':'')
+'</div></div>'
+'<div style="display:flex;align-items:center;gap:6px">'
+'<div style="display:flex;flex-direction:column;gap:2px">'
+(i>0?'<button onclick="moveCat('+i+',-1)" style="background:none;border:1px solid #e5e7eb;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:14px;color:#64748b;line-height:1">▲</button>':'<div style="width:28px;height:20px"></div>')
+(i<cats.length-1?'<button onclick="moveCat('+i+',1)" style="background:none;border:1px solid #e5e7eb;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:14px;color:#64748b;line-height:1">▼</button>':'<div style="width:28px;height:20px"></div>')
+'</div>'
+'<button onclick="editCategory('+i+')" style="padding:6px 12px;background:#f8f9fa;border:1px solid #e5e7eb;border-radius:6px;font-size:12px;cursor:pointer;font-family:inherit;font-weight:600;color:#334155">Redigera</button>'
+'</div></div>';
}).join('');
document.getElementById('catEditorList').innerHTML = listHtml;
}
window._catEditorCats = cats;
window.editCategory = function(idx) {
const c = idx >= 0 ? cats[idx] : {id:'',label:'',description:'',image:'',is_tillval:0,sort_order:cats.length+1};
const isNew = idx < 0;
document.getElementById('catEditorContent').innerHTML = '<div style="padding:20px">'
+'<h3 style="font-size:16px;font-weight:700;margin:0 0 16px">'+(isNew?'Ny kategori':'Redigera: '+c.label)+'</h3>'
+'<div style="display:grid;grid-template-columns:200px 1fr;gap:16px;align-items:start">'
+'<div><div id="catEditImgPreview" style="width:200px;height:150px;border-radius:10px;overflow:hidden;border:1px solid #e5e7eb;margin-bottom:6px;cursor:pointer" onclick="document.getElementById(\'catEditImgInput\').click()">'
+(c.image?'<img src="'+c.image+'" style="width:100%;height:100%;object-fit:cover">':'<div style="width:100%;height:100%;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:12px;color:#94a3b8">Klicka för bild</div>')
+'</div><input type="file" id="catEditImgInput" accept="image/*" style="display:none" onchange="uploadCatImage(this)"></div>'
+'<div style="display:grid;gap:10px">'
+(isNew?'<div><label style="font-size:11px;font-weight:600;color:#64748b">ID</label><input id="catEditId" value="" placeholder="t.ex. varmepump" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>':'')
+'<div><label style="font-size:11px;font-weight:600;color:#64748b">Namn</label><input id="catEditLabel" value="'+c.label.replace(/"/g,'"')+'" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b">Beskrivning</label><input id="catEditDesc" value="'+(c.description||'').replace(/"/g,'"')+'" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>'
+'<div><label style="font-size:11px;font-weight:600;color:#64748b">Sortering</label><input id="catEditSort" type="number" value="'+(c.sort_order||0)+'" style="width:80px;padding:8px 12px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px;font-family:inherit"></div>'
+'<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer"><input type="checkbox" id="catEditTillval" '+(parseInt(c.is_tillval)?'checked':'')+' style="width:16px;height:16px;accent-color:#f59e0b">Tillval-kategori</label>'
+'</div></div>'
+'<input type="hidden" id="catEditImage" value="'+(c.image||'')+'">'
+'<input type="hidden" id="catEditIdx" value="'+idx+'">'
+'<div style="display:flex;gap:8px;margin-top:16px">'
+'<button onclick="saveCatEdit(\''+c.id+'\')" style="padding:10px 20px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Spara</button>'
+'<button onclick="showCategoryEditor()" style="padding:10px 20px;background:#f1f5f9;color:#64748b;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Avbryt</button>'
+(isNew?'':'<div style="flex:1"></div><button onclick="deleteCatEdit(\''+c.id+'\')" style="padding:10px 20px;background:#fee2e2;color:#dc2626;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">Radera</button>')
+'</div></div>';
};
window.uploadCatImage = async function(input) {
if(!input.files[0]) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = async function(e) {
try {
const r = await fetch('/api/upload-photo.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image:e.target.result,name:'cat_'+file.name})});
const d = await r.json();
if(d.success && d.url) {
document.getElementById('catEditImage').value = d.url;
document.getElementById('catEditImgPreview').innerHTML = '<img src="'+d.url+'" style="width:100%;height:100%;object-fit:cover">';
}
} catch(err){ alert('Fel: '+err.message); }
};
reader.readAsDataURL(file);
};
window.moveCat = async function(idx, dir) {
const other = idx + dir;
if(other < 0 || other >= cats.length) return;
const a = cats[idx], b = cats[other];
const tmpSort = a.sort_order; a.sort_order = b.sort_order; b.sort_order = tmpSort;
await fetch('/api/categories.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:a.id,label:a.label,description:a.description,image:a.image,is_tillval:a.is_tillval,sort_order:a.sort_order})});
await fetch('/api/categories.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:b.id,label:b.label,description:b.description,image:b.image,is_tillval:b.is_tillval,sort_order:b.sort_order})});
try { const rr=await fetch('/api/categories.php?all=1');const dd=await rr.json();if(dd.success){cats=dd.categories;window._catEditorCats=cats;} }catch(e){}
renderList();
if(typeof loadCfgCategories==='function') loadCfgCategories();
};
window.deleteCatEdit = async function(id) {
if(!confirm('Radera kategori "'+id+'"?')) return;
await fetch('/api/categories.php?id='+id, {method:'DELETE'});
try { const rr=await fetch('/api/categories.php?all=1');const dd=await rr.json();if(dd.success){cats=dd.categories;window._catEditorCats=cats;} }catch(e){}
document.getElementById('catEditorContent').innerHTML = '<div id="catEditorList" style="padding:20px"></div>';
renderList();
if(typeof loadCfgCategories==='function') loadCfgCategories();
};
window.saveCatEdit = async function(origId) {
const idx = parseInt(document.getElementById('catEditIdx').value);
const id = idx < 0 ? (document.getElementById('catEditId')?.value||'').trim() : origId;
if(!id) { alert('ID krävs'); return; }
const body = {id:id,label:document.getElementById('catEditLabel').value,description:document.getElementById('catEditDesc').value,image:document.getElementById('catEditImage').value,is_tillval:document.getElementById('catEditTillval').checked?1:0,sort_order:parseInt(document.getElementById('catEditSort').value)||0};
const r = await fetch('/api/categories.php', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d = await r.json();
if(d.success) {
try { const rr=await fetch('/api/categories.php?all=1');const dd=await rr.json();if(dd.success){cats=dd.categories;window._catEditorCats=cats;} }catch(e){}
document.getElementById('catEditorContent').innerHTML = '<div id="catEditorList" style="padding:20px"></div>';
renderList();
if(typeof loadCfgCategories==='function') loadCfgCategories();
}
};
const modal = document.createElement('div');
modal.id = 'catEditorModal';
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;width:900px;height:900px;max-width:95vw;max-height:95vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.2)">'
+'<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;margin:0">Kategorier</h2>'
+'<div style="display:flex;align-items:center;gap:10px">'
+'<button onclick="editCategory(-1)" style="padding:8px 16px;background:#059669;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit">+ Lägg till kategori</button>'
+'<button onclick="document.getElementById(\'catEditorModal\').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">×</button>'
+'</div></div>'
+'<div id="catEditorContent" style="flex:1;overflow-y:auto"><div id="catEditorList" style="padding:20px"></div></div>'
+'</div>';
document.body.appendChild(modal);
renderList();
};
// --- Kundpris auto-calc in product editor ---
window.peCalcPrice = function(){
const cost=parseFloat(document.getElementById('peCostPrice')?.value)||0;
const mtype=document.getElementById('peMarkupType')?.value||'percent';
const mval=parseFloat(document.getElementById('peMarkupValue')?.value)||0;
let price=cost;
if(mtype==='amount') price=cost+mval;
else if(mtype==='percent') price=Math.round(cost*(1+mval/100)*100)/100;
const el=document.getElementById('pePrice');
if(el) el.value=price;
};
// --- showAddToKalkyl ---
window.showAddToKalkyl = async function(category) {
let quotes = [];
try {
const r = await fetch('/api/quotes.php');
const d = await r.json();
if(d.success && d.quotes) quotes = d.quotes;
} catch(e){}
const modal = document.createElement('div');
modal.id = 'addToKalkylModal';
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(); };
let listHtml = quotes.slice(0,15).map(q => {
const customerName = q.customer_name || '';
const quoteName = q.quote_number || '#' + q.id;
const cats = q.category ? q.category.split(',').map(c => c.trim()).join(', ') : '';
const price = q.total_price && parseFloat(q.total_price) > 0 ? Math.round(parseFloat(q.total_price)).toLocaleString('sv-SE')+' kr' : '';
const status = q.status || 'utkast';
const statusColor = status === 'offert' ? '#f59e0b' : status === 'order' ? '#10b981' : '#94a3b8';
return '<div onclick="document.getElementById(\'addToKalkylModal\').remove();currentQuoteId='+q.id+';saveCfgQuote(\''+category+'\')" style="padding:14px 16px;border:1.5px solid #e5e7eb;border-radius:10px;cursor:pointer;margin-bottom:6px;transition:all .15s" onmouseover="this.style.borderColor=\'#024550\';this.style.background=\'#f0fdfa\'" onmouseout="this.style.borderColor=\'#e5e7eb\';this.style.background=\'#fff\'">'
+'<div style="display:flex;justify-content:space-between;align-items:center">'
+'<div>'
+'<div style="font-weight:600;font-size:14px">'+(customerName || quoteName)+'</div>'
+(customerName ? '<div style="font-size:11px;color:#94a3b8">'+quoteName+'</div>' : '')
+(cats ? '<div style="font-size:11px;color:#64748b;margin-top:2px">'+cats+'</div>' : '')
+'</div>'
+'<div style="text-align:right">'
+(price ? '<div style="font-weight:700;font-size:14px;color:#024550">'+price+'</div>' : '')
+'<div style="font-size:10px;font-weight:600;color:'+statusColor+';text-transform:uppercase">'+status+'</div>'
+'</div></div></div>';
}).join('');
modal.innerHTML = '<div style="background:#fff;border-radius:16px;padding:24px;width:480px;max-width:95vw;max-height:80vh;overflow-y:auto">'
+'<div style="display:flex;justify-content:space-between;margin-bottom:16px"><h3 style="font-size:17px;font-weight:700;margin:0">Lägg till i kalkyl</h3><button onclick="document.getElementById(\'addToKalkylModal\').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#94a3b8">×</button></div>'
+'<button onclick="document.getElementById(\'addToKalkylModal\').remove();currentQuoteId=null;saveCfgQuote(\''+category+'\')" style="width:100%;padding:14px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;margin-bottom:16px">+ Ny kalkyl</button>'
+(quotes.length?'<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;margin-bottom:8px">Eller lägg till i befintlig</div>'+listHtml:'')
+'</div>';
document.body.appendChild(modal);
};
console.log('Solargroup updates loaded');
// Update catalog category order from DB
(function(){
const origLoadCatalog = window.loadCatalogFromDB;
if(!origLoadCatalog) return;
// After categories load, update _catOrder
fetch('/api/categories.php?all=1').then(r=>r.json()).then(d=>{
if(d.success && d.categories) {
window._catOrder = d.categories.map(c=>c.id);
}
}).catch(()=>{});
})();
// Update catalog category select from DB
(function(){
fetch('/api/categories.php?all=1').then(r=>r.json()).then(d=>{
if(!d.success) return;
const sel = document.getElementById('catalogCatSelect');
if(!sel) return;
const current = sel.value;
let opts = '<option value="">Alla produkter</option>';
const main = d.categories.filter(c => !parseInt(c.is_tillval));
const tillval = d.categories.filter(c => parseInt(c.is_tillval));
main.forEach(c => { opts += '<option value="'+c.id+'">'+c.label+'</option>'; });
if(tillval.length) {
opts += '<optgroup label="Tillval">';
tillval.forEach(c => { opts += '<option value="'+c.id+'">'+c.label+'</option>'; });
opts += '</optgroup>';
}
sel.innerHTML = opts;
if(current) sel.value = current;
// Update sort order
window._catOrder = d.categories.map(c=>c.id);
}).catch(()=>{});
})();
// --- Fix tillval: variant select for standard variants (kwh etc) ---
(function(){
const origRender = window.spRenderTillval;
if(!origRender) return;
window.spRenderTillval = function() {
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 variantSelector = '';
// Standard variants (kwh, paneler etc) - NEW
if(tv.specs && tv.specs.variants && tv.specs.variants.length && !tv.specs.type && tv.checked) {
const first = tv.specs.variants[0];
let vKey = 'kwh', vSuffix = ' kWh';
if(first.paneler !== undefined) { vKey='paneler'; vSuffix=' paneler'; }
else if(first.kvm !== undefined) { vKey='kvm'; vSuffix=' m²'; }
else if(first.kwh === undefined) { vKey=Object.keys(first)[0]; vSuffix=''; }
variantSelector = '<div style="margin-top:6px" 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;width:100%">'
+'<option value="">Välj...</option>'
+ tv.specs.variants.map((v, vi) => {
const lbl = v[vKey] + vSuffix;
const pr = v.totalt || v.price || v.att_betala;
return '<option value="'+vi+'"'+(vi===tv.selectedVariant?' selected':'')+'>'+lbl+(pr?' — '+pr.toLocaleString('sv-SE')+' kr':'')+'</option>';
}).join('')
+'</select></div>';
}
// Style+width variants
else if(tv.specs && tv.specs.type === 'style_width_variants' && tv.checked) {
// keep existing behavior - call original
}
// Width variants
else if(tv.specs && tv.specs.type === 'width_variants' && tv.checked) {
variantSelector = '<div style="margin-top:6px" 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;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' : '');
const pr = v.price != null ? ' — ' + v.price.toFixed(2) + ' kr' : '';
return '<option value="'+vi+'"'+(vi===tv.selectedVariant?' selected':'')+'>'+lbl+pr+'</option>';
}).join('')
+'</select></div>';
}
// Qty input for non-st units
let qtyInput = '';
if(tv.unit && tv.unit !== 'st' && tv.checked) {
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>';
}
const totalPrice = tv.computedPrice !== null ? tv.computedPrice : tv.price;
return '<div style="padding:10px 14px;background:'+(tv.checked?'#f0fdf4':'#f8f9fa')+';border:1px solid '+(tv.checked?'#bbf7d0':'#e5e7eb')+';border-radius:10px;transition:all .15s">'
+'<div style="display:flex;align-items:center;gap:8px;cursor:pointer" onclick="spToggleTillval('+realIdx+')">'
+'<input type="checkbox" '+(tv.checked?'checked':'')+' '+(tv.included?'disabled':'')+' style="width:18px;height:18px;accent-color:#059669;flex-shrink:0;pointer-events:none">'
+'<span style="flex:1;font-size:13px;font-weight:600;color:#334155">'+tv.name+(tv.unit&&tv.unit!=='st'?' <span style="font-size:10px;color:#94a3b8">('+tv.price+' kr/'+tv.unit+')</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>';
}).join('') + '</div>';
spUpdatePrice();
};
})();
// --- Fix: spSetTillvalQty should NOT re-render (focus loss fix) ---
(function(){
const orig = window.spSetTillvalQty;
window.spSetTillvalQty = function(idx, val) {
_spTillval[idx].userQty = parseFloat(val) || 0;
spUpdatePrice(); // only update price, don't re-render
};
})();
// --- Fix: handle standard variant price in spUpdatePrice ---
(function(){
const origUpdate = window.spUpdatePrice;
if(!origUpdate) return;
// Patch the tillval price calculation to handle standard variants
const origCalc = window.spUpdatePrice;
// We can't easily patch spUpdatePrice without rewriting it
// Instead, add a helper that the existing code can use
window._getTillvalPrice = function(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;
} else if(tv.specs.variants && tv.specs.variants[tv.selectedVariant]) {
const sv = tv.specs.variants[tv.selectedVariant];
pr = sv.totalt || sv.price || sv.att_betala || pr;
}
}
return pr;
};
})();
console.log('Tillval patches loaded');
// --- Itemize tillval in price summary ---
(function(){
const origUpdate = window.spUpdatePrice;
if(!origUpdate) return;
// After each price update, replace "Tillval +X kr" with itemized list
const observer = new MutationObserver(function() {
const el = document.getElementById('spPriceArea');
if(!el) return;
// Find the "Tillval" line and replace with itemized
const divs = el.querySelectorAll('div');
divs.forEach(div => {
const span = div.querySelector('.price-line-label, span');
if(span && span.textContent.trim() === 'Tillval') {
// Get checked non-hidden tillval
if(typeof _spTillval === 'undefined') return;
const checked = _spTillval.filter(tv => tv.checked && !tv.excluded && !tv.hidden);
if(!checked.length) return;
let html = '<div style="font-size:11px;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;margin-top:4px">Tillval</div>';
checked.forEach(tv => {
let pr = window._getTillvalPrice ? window._getTillvalPrice(tv) : tv.price;
const eq = tv.unit === 'm²' ? (tv.userL * tv.userW) : (tv.unit && tv.unit !== 'st' && tv.userQty > 0 ? tv.userQty : tv.qty || 1);
const total = pr * (eq || 1);
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">+'+total.toLocaleString('sv-SE')+' kr</span>'
+'</div>';
});
div.outerHTML = html;
}
});
});
// Observe price area changes
setTimeout(function() {
const el = document.getElementById('spPriceArea');
if(el) observer.observe(el, {childList: true, subtree: true, characterData: true});
}, 1000);
})();
// --- Show hidden tillval (installation etc) in price area + load child tillval ---
(function(){
// Patch spLoadTillval to also load child tillval
const origLoadTillval = window.spLoadTillval;
if(!origLoadTillval) return;
window.spLoadTillval = async function(productId) {
await origLoadTillval(productId);
// Load child tillval for each tillval
for(const tv of _spTillval) {
tv.childTillval = [];
try {
const r = await fetch('/api/products.php?services_for='+tv.id);
const d = await r.json();
if(d.success && d.services && d.services.length) {
const allP = await window._spGetAllProducts();
const childIds = d.services.map(s=>s.service_id);
tv.childTillval = allP.filter(p=>childIds.includes(p.id)).map(p=>({
id:p.id, name:p.name, price:parseFloat(p.price),
obligatorisk:parseInt(p.tillval_obligatorisk)||0,
hidden:parseInt(p.tillval_hidden)||0,
rotEligible:parseInt(p.rot_eligible)||0,
greenTechEligible:parseInt(p.green_tech_eligible)||0
}));
}
} catch(e){}
}
spApplyRulesAndRender();
};
// Cache for all products
window._spAllProducts = null;
window._spGetAllProducts = async function() {
if(window._spAllProducts) return window._spAllProducts;
try {
const [r1,r2,r3] = await Promise.all([
fetch('/api/products.php?cat=tjanster&active=1'),
fetch('/api/products.php?cat=tillbehor&active=1'),
fetch('/api/products.php?active=1')
]);
const d1 = await r1.json(), d2 = await r2.json(), d3 = await r3.json();
const all = [...(d1.success?d1.products:[]), ...(d2.success?d2.products:[])];
const ids = new Set(all.map(p=>p.id));
if(d3.success) d3.products.forEach(p => { if(!ids.has(p.id)) all.push(p); });
window._spAllProducts = all;
} catch(e){ window._spAllProducts = []; }
return window._spAllProducts;
};
// Override price update to show hidden tillval + child tillval
const origPriceUpdate = window.spUpdatePrice;
window.spUpdatePrice = function() {
origPriceUpdate();
// Now patch the rendered price area
const el = document.getElementById('spPriceArea');
if(!el || !window._spProduct) return;
const p = window._spProduct;
// Find hidden tillval
const hiddenChecked = _spTillval.filter(tv => tv.checked && !tv.excluded && tv.hidden);
if(!hiddenChecked.length) return;
// Calculate hidden total
let hiddenTotal = 0;
let hiddenItems = [];
hiddenChecked.forEach(tv => {
hiddenTotal += tv.price;
hiddenItems.push({name:tv.name, price:tv.price, rotEligible:tv.rotEligible});
if(tv.childTillval) tv.childTillval.filter(ch=>ch.obligatorisk).forEach(ch => {
hiddenTotal += ch.price;
hiddenItems.push({name:ch.name, price:ch.price, rotEligible:ch.rotEligible, isChild:true});
});
});
// Also check child tillval on checked visible tillval
const visibleChecked = _spTillval.filter(tv => tv.checked && !tv.excluded && !tv.hidden);
let childItems = [];
visibleChecked.forEach(tv => {
if(tv.childTillval) tv.childTillval.filter(ch=>ch.obligatorisk).forEach(ch => {
childItems.push({name:ch.name, price:ch.price, parentName:tv.name});
});
});
// Find "Pris" line and insert hidden items after it
const firstDiv = el.querySelector('div');
if(!firstDiv) return;
// Build new content: Product name, hidden items, Totalt
const allDivs = Array.from(el.children);
const priceLine = allDivs[0]; // First line is "Pris"
if(!priceLine || hiddenTotal === 0) return;
// Calculate arbete (product - hidden)
const variants = (p.specs && p.specs.variants) || [];
const sv = variants[window._spSelectedVariant] || {};
let displayPrice = sv.totalt || p.price;
const spQtyEl = document.getElementById('spQtyInput');
const spQty = spQtyEl ? parseInt(spQtyEl.value)||1 : 1;
if(p.unit && p.unit !== 'st' && spQty > 1) displayPrice = displayPrice * spQty;
let arbete = displayPrice - hiddenTotal;
// Build replacement HTML
let newHtml = '<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(hi => {
newHtml += '<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>';
});
newHtml += '<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>';
// Replace the first "Pris" line with our expanded version
priceLine.outerHTML = newHtml;
// Add child tillval items after their parent in TILLVAL section
if(childItems.length) {
const tillvalHeader = el.querySelector('div[style*="TILLVAL"]') || Array.from(el.querySelectorAll('div')).find(d => d.textContent.trim() === 'Tillval');
// Just append child items to existing tillval list
}
};
})();
console.log('Hidden tillval + cascade patches loaded');
// --- Show "Kopplad till produkt" on Tillval tab ---
(function(){
const origEditProduct = window.editProduct;
if(!origEditProduct) return;
// After editProduct renders, add "Kopplad till produkt" section
const origLoad = window.peLoadServices;
if(!origLoad) return;
const patchedLoad = async function(productId) {
await origLoad(productId);
// Fetch which products use THIS product as tillval
try {
const r = await fetch('/api/products.php?linked_to='+productId);
const d = await r.json();
if(d.success && d.products && d.products.length) {
const container = document.getElementById('peServicesArea');
if(!container) return;
let html = '<div style="margin-top:24px;border-top:2px solid #e5e7eb;padding-top:16px">'
+'<h4 style="font-size:14px;font-weight:700;color:#1a1a1a;margin:0 0 10px">Kopplad till produkt</h4>'
+'<div style="font-size:11px;color:#94a3b8;margin-bottom:8px">Denna produkt används som tillval på följande produkter:</div>'
+'<table style="width:100%;font-size:12px;border-collapse:collapse">'
+'<tr style="background:#f8f9fa"><th style="padding:6px 10px;text-align:left;color:#64748b;font-weight:600">Produkt</th><th style="padding:6px 10px;text-align:left;color:#64748b;font-weight:600">Kategori</th><th style="padding:6px 10px;text-align:center;color:#64748b;font-weight:600">Dold</th><th style="padding:6px 10px;text-align:center;color:#64748b;font-weight:600">Standard</th></tr>';
d.products.forEach(p => {
html += '<tr style="border-top:1px solid #f1f5f9;cursor:pointer" onclick="document.querySelector(\'.pe-modal\')?.remove();editProduct(\''+p.id+'\')">'
+'<td style="padding:6px 10px;font-weight:600;color:#024550">'+p.name+'</td>'
+'<td style="padding:6px 10px;color:#64748b">'+(p.cat_label||p.cat)+'</td>'
+'<td style="padding:6px 10px;text-align:center">'+(parseInt(p.hidden)?'<span style="color:#f59e0b">●</span>':'')+'</td>'
+'<td style="padding:6px 10px;text-align:center">'+(parseInt(p.default_on)?'<span style="color:#059669">●</span>':'')+'</td>'
+'</tr>';
});
html += '</table></div>';
container.insertAdjacentHTML('beforeend', html);
}
} catch(e) { console.error(e); }
};
window.peLoadServices = patchedLoad;
})();
console.log('Linked-to patch loaded');