js/project-flow.js

Code: DEV-81AE728B Size: 19.7 KB Lines: 289 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/js/project-flow.js

Task / Comment

Open report form
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 += '&region='+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); }
}