backups/2026-03-28-pre-sidebar-refactor/konfigurator-fonster.js

Code: DEV-E99A8E6A Size: 20.1 KB Lines: 399 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/backups/2026-03-28-pre-sidebar-refactor/konfigurator-fonster.js

Task / Comment

Open report form
// konfigurator-fonster.js - Fönster configurator

// === 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() { var _lbl=document.getElementById("cfgFileLabel");if(_lbl)_lbl.textContent="konfigurator-fonster.js";
    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">&#10003; 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">&times;</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); }
}

// =============================================