js/dashboard.js

Code: DEV-A4D0F6AD Size: 9.8 KB Lines: 177 Path: /home/prodconfig.wenesthosting.com/dev.solargroup.wenest.se/js/dashboard.js

Task / Comment

Open report form
// dashboard.js - Dashboard + charts

function _dashScopeParam(widgetKey){
    if(typeof widgetScope !== 'function') return '';
    const s = widgetScope(widgetKey);
    return (s === 'own' && typeof gStaffId !== 'undefined' && gStaffId) ? '&saljare_id='+gStaffId : '';
}

async function loadDashboard() {
    try {
        const monthNames = ['Januari','Februari','Mars','April','Maj','Juni','Juli','Augusti','September','Oktober','November','December'];
        const currentMonth = monthNames[new Date().getMonth()];

        // Set dashboard title with current month
        const dashTitle = document.getElementById('dashTitle');
        if (dashTitle) dashTitle.textContent = 'Dashboard - ' + currentMonth;

        // Apply per-widget visibility
        const _vis = (k) => typeof isWidgetVisible==='function' ? isWidgetVisible(k) : true;
        [['dashTotalValue','stat_total'],['dashOffertar','stat_offertar'],['dashKunder','stat_kunder'],['dashAccepted','stat_accepted']].forEach(([id,key]) => {
            const card = document.getElementById(id)?.closest('.stat-card');
            if(card) card.style.display = _vis(key) ? '' : 'none';
        });
        [['dashTopSellers','top_sellers','.dash-panel'],['dashRecentDeals','recent_deals','.dash-panel'],['dashMeetings','meetings','.dash-panel'],['chartMonthly','chart_monthly','.dash-chart-panel'],['chartYearly','chart_yearly','.dash-chart-panel']].forEach(([id,key,sel]) => {
            const p = document.getElementById(id)?.closest(sel);
            if(p) p.style.display = _vis(key) ? '' : 'none';
        });

        // Load deal stats for current month
        const now = new Date();
        const dashMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
        const statsScope = _dashScopeParam('stat_total');
        const dealsScope = _dashScopeParam('recent_deals');
        const custScope = _dashScopeParam('stat_kunder');
        const [statsR, dealsR, custR] = await Promise.all([
            fetch('api/deals.php?action=stats&month='+dashMonth+statsScope+'&t='+Date.now()),
            fetch('api/deals.php?limit=5&status_exclude=anger,ej_godkand,avbrott&sort=datum_salj'+dealsScope+'&t='+Date.now()),
            fetch('api/customers.php?limit=1'+custScope+'&t='+Date.now())
        ]);
        const stats = await statsR.json();
        const dealsData = await dealsR.json();
        const custData = await custR.json();

        const el = (id,v) => { const e=document.getElementById(id); if(e) e.textContent=v; };

        // Total value (exclude anger/ej_godkand/avbrott)
        const excludeStatuses = ['anger','ej_godkand','avbrott'];
        let totalVal = 0, offertCount = 0, acceptedCount = 0;
        Object.entries(stats).forEach(([k,v]) => {
            if (!excludeStatuses.includes(k)) totalVal += v.value || 0;
            if (k === 'offert') offertCount = v.count || 0;
            if (['order','projektering','bestallning','leverans','montering','besiktning','fardigstall'].includes(k)) acceptedCount += v.count || 0;
        });

        el('dashTotalValue', fmtKr(totalVal));
        el('dashOffertar', offertCount);
        el('dashKunder', custData.total || 0);
        el('dashAccepted', acceptedCount);

        // Recent deals
        const container = document.getElementById('dashRecentDeals');
        if (container && dealsData.deals) {
            if (!dealsData.deals.length) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Inga affärer ännu</div>';
            } else {
                container.innerHTML = dealsData.deals.map(d => {
                    const products = (d.product_types||'').split(',').filter(Boolean).map(p => PRODUCT_LABELS[p]||p).join(', ');
                    const color = DEAL_STATUS_COLORS[d.status]||'#94a3b8';
                    const val = d.ordervarde_ink_moms ? Math.round(parseFloat(d.ordervarde_ink_moms)).toLocaleString('sv-SE')+' kr' : '-';
                    return '<div class="offert-item" style="cursor:pointer" onclick="showPage(\'projekt\');setTimeout(()=>showDealDetail('+d.id+'),200)">'
                        +'<div class="offert-item-left"><h4>'+d.customer_name+'</h4><p>'+(products||d.deal_number)+' &bull; '+(d.datum_salj||'')+'</p></div>'
                        +'<div class="offert-item-right"><div class="offert-price">'+val+'</div><div class="offert-status" style="color:'+color+'">'+DEAL_STATUS_LABELS[d.status]+'</div></div>'
                        +'</div>';
                }).join('');
            }
        }
        // Top sellers
        const tsContainer = document.getElementById('dashTopSellers');
        if (tsContainer) {
            try {
                const now = new Date();
                const curMonth = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0');
                const tsR = await fetch('api/deals.php?action=top_sellers&month='+curMonth+'&t='+Date.now());
                const sellers = await tsR.json();
                if (sellers.length) {
                    const medals = ['medal-gold','medal-silver','medal-bronze'];
                    tsContainer.innerHTML = sellers.slice(0,5).map((s,i) =>
                        '<div class="top-seller-item">'
                        +(i<3?'<div class="top-seller-medal '+medals[i]+'">'+(i+1)+'</div>':'<div style="width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:#94a3b8">'+(i+1)+'</div>')
                        +'<div class="top-seller-info"><div class="top-seller-name">'+s.name+'</div><div class="top-seller-detail">'+s.deal_count+' order</div></div>'
                        +'<div class="top-seller-amount">'+fmtKr(s.total_value)+'</div>'
                        +'</div>'
                    ).join('');
                } else {
                    tsContainer.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">Ingen data</div>';
                }
            } catch(e) { tsContainer.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8;font-size:13px">-</div>'; }
        }
    } catch(e) { console.error('loadDashboard error:', e); }
}

function fmtKr(n){return n?Math.round(n).toLocaleString('sv-SE')+' kr':'-'}

/* === KUNDER (DataTable) === */
let customerDT = null;
let customersLoaded = false;
let allCustData = [];


/* === DASHBOARD CHARTS === */
let dashMonthlyChart = null;
let dashYearlyChart = null;

async function initDashCharts(){
    const ctxM=document.getElementById('chartMonthly');
    const ctxY=document.getElementById('chartYearly');
    if(!ctxM||!ctxY)return;

    try {
        const monthlyScope = _dashScopeParam('chart_monthly');
        const yearlyScope = _dashScopeParam('chart_yearly');
        const [monthlyR, yearlyR] = await Promise.all([
            fetch('api/deals.php?action=chart_monthly'+monthlyScope+'&t='+Date.now()),
            fetch('api/deals.php?action=chart_yearly'+yearlyScope+'&t='+Date.now())
        ]);
        const monthlyData = await monthlyR.json();
        const yearlyData = await yearlyR.json();

        // Update month title
        const monthTitle = document.getElementById('dashMonthlyTitle');
        if (monthTitle) monthTitle.textContent = 'Försäljning ' + monthlyData.month_name;

        // Monthly chart — per week
        const weekLabels = monthlyData.weeks.map(w => w.week_label);
        const orderData = monthlyData.weeks.map(w => parseFloat(w.order_value));
        const offertData = monthlyData.weeks.map(w => parseFloat(w.offert_value));
        const lostData = monthlyData.weeks.map(w => parseFloat(w.lost_value));

        if (dashMonthlyChart) dashMonthlyChart.destroy();
        dashMonthlyChart = new Chart(ctxM,{
            type:'bar',
            data:{
                labels: weekLabels,
                datasets:[
                    {label:'Order',data:orderData,backgroundColor:'#10b981',borderRadius:4},
                    {label:'Offerter',data:offertData,backgroundColor:'#eab308',borderRadius:4},
                    {label:'Förlorade',data:lostData,backgroundColor:'#ef4444',borderRadius:4}
                ]
            },
            options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:12,font:{family:'Inter',size:11}}}},scales:{y:{beginAtZero:true,ticks:{callback:v=>v>=1000?(v/1000)+'k':''+v,font:{family:'Inter',size:11}},grid:{color:'#f1f5f9'}},x:{grid:{display:false},ticks:{font:{family:'Inter',size:11}}}}}
        });

        // Yearly chart
        const monthLabels = ['Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'];
        const salesData = yearlyData.months;

        // Update year title
        const yearTitle = document.getElementById('dashYearlyTitle');
        if (yearTitle) yearTitle.textContent = 'Helårsöversikt ' + yearlyData.year;

        if (dashYearlyChart) dashYearlyChart.destroy();
        dashYearlyChart = new Chart(ctxY,{
            type:'line',
            data:{
                labels: monthLabels,
                datasets:[
                    {label:'Försäljning',data:salesData,borderColor:'#024550',backgroundColor:'rgba(2,69,80,.08)',fill:true,tension:.3,pointRadius:5,pointBackgroundColor:'#024550'},
                    {label:'Mål',data:[500000,500000,550000,550000,600000,650000,400000,450000,600000,650000,700000,750000],borderColor:'#e5e7eb',borderDash:[5,5],fill:false,tension:.3,pointRadius:0}
                ]
            },
            options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{usePointStyle:true,padding:12,font:{family:'Inter',size:11}}}},scales:{y:{beginAtZero:true,ticks:{callback:v=>v>=1000?(v/1000)+'k':''+v,font:{family:'Inter',size:11}},grid:{color:'#f1f5f9'}},x:{grid:{display:false},ticks:{font:{family:'Inter',size:11}}}}}
        });
    } catch(e) {
        console.error('initDashCharts error:', e);
    }
}