js/productlist.js

Code: DEV-E7734603 Size: 24.2 KB Lines: 317 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/js/productlist.js

Task / Comment

Open report form
// productlist.js — produktlista (tidigare produkter.js, döpt om 2026-04-22)
// produkter.js - Product catalog, filter, render, drag/drop

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', taxType: p.tax_type || 'NONE', fixedPrice: parseInt(p.fixed_price) || 0,
                    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); }
    // Ladda kategoriordning från DB
    try { var catRes = await fetch("/api/categories.php?all=1"); var catData = await catRes.json(); if(catData.success && catData.categories) { _catOrder = catData.categories.map(function(c){ return c.id; }); } } catch(e){}
    filterCatalog();
}

let _catOrder = []; // Fylls dynamiskt från categories API
// 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 — men OM det finns söktext, visa alla
    const tillvalCats = ['tjanster','tillbehor'];
    if(!val && !searchTerm) 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');
    // Ladda kategoriordning från DB
    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;
            if(!s.sizes || !s.sizes.length) {
                varHtml = '<div style="margin-top:8px;font-size:11px;color:#94a3b8">'+(s.models?s.models.length+' modeller':'Inga storlekar')+'</div>';
            } else {
                // Beräkna widths/heights från sizes om de saknas (fallback för manuellt skapade produkter)
                var widths = (s.widths && s.widths.length) ? s.widths : [...new Set(s.sizes.map(function(x){return x.w;}))];
                var heights = (s.heights && s.heights.length) ? s.heights : [...new Set(s.sizes.map(function(x){return x.h;}))];
                var allPr = s.models && s.models.length ? s.sizes.flatMap(function(x){return s.models.map(function(m){return x[m.key];}).filter(function(v){return v!=null;});}) : s.sizes.map(function(x){return x.price;}).filter(function(v){return v!=null;});
                var minP = allPr.length ? Math.min.apply(null,allPr) : 0;
                var maxP = allPr.length ? Math.max.apply(null,allPr) : 0;
                var sizeCount = s.sizes.length;
                var hasLam = s.sizes.some(function(x){return 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>'
                    +(s.model?'<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.apply(null,widths)+'\u2013'+Math.max.apply(null,widths)+' dm | H\u00f6jd: '+Math.min.apply(null,heights)+'\u2013'+Math.max.apply(null,heights)+' dm</div>'
                    +(allPr.length?(function(){var cur=p.costCurrency||"SEK";var cr=cur==="SEK"?1:(typeof _currencyRates!=="undefined"&&_currencyRates[cur]?_currencyRates[cur]:1);var mk=p.markupType==="percent"&&p.markupValue?(1+p.markupValue/100):1;return "<div style=\"font-weight:600;color:#024550;margin-top:4px\">"+Math.round(minP*cr*mk).toLocaleString("sv-SE")+" \u2013 "+Math.round(maxP*cr*mk).toLocaleString("sv-SE")+" kr</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
    // Ladda kategoriordning från DB
    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?p.specs.sizes.length:0) : (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 = [];