// personal.js - Staff management
// === PERSONAL ===
var currentStaffData = null;
var staffDT = null;
var allStaffData = [];
async function loadStaff() {
var role = '';
var rf = document.getElementById('staffRoleFilter');
if (rf) role = rf.value || '';
var url = '/api/staff.php?active=0';
if (role) url += '&role=' + role;
try {
var res = await fetch(url);
var staff = await res.json();
if (!Array.isArray(staff)) staff = [];
allStaffData = staff;
var cnt = document.getElementById('staffCount');
if (cnt) cnt.textContent = staff.length + ' person' + (staff.length !== 1 ? 'er' : '');
buildStaffDT(staff);
} catch (e) {
console.error('loadStaff error:', e);
}
}
function buildStaffDT(staff) {
var rows = staff.map(function(s) {
var roleLbl = roleLabels[s.role] || s.role || '';
var roleClr = roleColors[s.role] || 'gray';
var sales = parseFloat(s.total_sales || 0);
return [
s.id,
s.name || '',
s.email || '',
roleLbl,
s.active == 1 ? 'Aktiv' : 'Inaktiv',
sales,
roleClr
];
});
if (staffDT) { staffDT.destroy(); staffDT = null; }
staffDT = $('#staffDT').DataTable({
data: rows,
columns: [
{ title:'Namn', render: function(d,t,r){ return '<strong>' + r[1] + '</strong>'; } },
{ title:'E-post', render: function(d,t,r){ return r[2]; } },
{ title:'Roll', render: function(d,t,r){ return '<span class="status-badge ' + r[6] + '">' + r[3] + '</span>'; } },
{ title:'Status', render: function(d,t,r){ return r[4] === 'Aktiv' ? '<span class="status-badge green">Aktiv</span>' : '<span class="status-badge gray">Inaktiv</span>'; }, width:'80px' },
{ title:'Försäljning', className:'dt-right', render: function(d,t,r){ return r[5] ? r[5].toLocaleString('sv-SE') + ' kr' : '-'; } }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ personal', infoEmpty:'Ingen personal',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Ingen personal hittades'
},
pageLength: 50,
lengthMenu: [25, 50, 100],
order: [[0,'asc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showStaffDetail(data[0]); });
}
});
}
// --- Staff Detail View ---
async function showStaffDetail(id) {
try {
const res = await fetch(STAFF_API + '?id=' + id);
const staff = await res.json();
if (staff.error) return;
currentStaffData = staff;
document.getElementById('staffListView').style.display = 'none';
document.getElementById('staffDetailView').style.display = 'block';
document.getElementById('staffDetailName').textContent = staff.name;
document.getElementById('staffDetailRole').textContent = roleLabels[staff.role] || staff.role;
document.getElementById('staffDetailRole').className = 'status-badge ' + (roleColors[staff.role] || 'gray');
switchStaffTab('info');
} catch(e) { console.error('showStaffDetail error:', e); }
}
function closeStaffDetail() {
document.getElementById('staffDetailView').style.display = 'none';
document.getElementById('staffListView').style.display = 'block';
currentStaffData = null;
}
function switchStaffTab(tab) {
document.querySelectorAll('.staff-tab').forEach(function(btn) {
var isActive = btn.dataset.tab === tab;
btn.style.borderBottomColor = isActive ? '#024550' : 'transparent';
btn.style.color = isActive ? '#024550' : '#64748b';
});
var panels = {info:'staffTabInfo',loner:'staffTabLoner',salj:'staffTabSalj',utveckling:'staffTabUtveckling',affarslista:'staffTabAffarslista'};
Object.keys(panels).forEach(function(k){ document.getElementById(panels[k]).style.display = k === tab ? 'block' : 'none'; });
if (!currentStaffData) return;
var sid = currentStaffData.id;
if (tab === 'info') renderStaffInfo(currentStaffData);
else if (tab === 'loner') loadStaffLoner(sid);
else if (tab === 'salj') loadStaffMeetings(sid, 'salj_samtal');
else if (tab === 'utveckling') loadStaffMeetings(sid, 'utvecklings_samtal');
else if (tab === 'affarslista') loadStaffDeals(sid);
}
function staffInfoField(label, value) {
return '<div><div style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.5px;margin-bottom:2px">' + label + '</div><div style="font-size:14px;color:#1a1a1a;font-weight:500">' + (value || '-') + '</div></div>';
}
function renderStaffInfo(s) {
document.getElementById('staffTabInfo').innerHTML =
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;max-width:800px">' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Personuppgifter</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Namn', s.name) +
staffInfoField('E-post', s.email) +
staffInfoField('Telefon', s.phone) +
staffInfoField('Titel', s.title) +
staffInfoField('Personnummer', s.personnummer) +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Anställning</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Roll', roleLabels[s.role] || s.role) +
staffInfoField('Startdatum', s.start_date) +
staffInfoField('Grundlön', s.grundlon ? parseFloat(s.grundlon).toLocaleString('sv-SE') + ' kr' : null) +
staffInfoField('Skattesats', (s.skatt_procent || '30') + '%') +
staffInfoField('Status', s.active == 1 ? 'Aktiv' : 'Inaktiv') +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">Adress</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Adress', s.address) +
staffInfoField('Postnummer', s.zip) +
staffInfoField('Stad', s.city) +
'</div></div>' +
'<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:20px">' +
'<h3 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">System</h3>' +
'<div style="display:grid;gap:12px">' +
staffInfoField('Google', s.google_id ? '✓ Kopplad' : 'Ej kopplad') +
staffInfoField('Senast inloggning', s.last_login ? new Date(s.last_login).toLocaleDateString('sv-SE') : null) +
staffInfoField('Skapad', s.created_at ? new Date(s.created_at).toLocaleDateString('sv-SE') : null) +
'</div>' +
'<button onclick="editStaff(' + s.id + ')" style="margin-top:16px;padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">✎ Redigera</button>' +
'</div></div>';
}
// --- Löner ---
async function loadStaffLoner(staffId) {
var panel = document.getElementById('staffTabLoner');
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar löner...</div>';
try {
var res = await fetch('api/salary.php?action=my&staff_id=' + staffId);
var records = await res.json();
if (!records || !records.length) {
panel.innerHTML = '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga lönebesked registrerade</div>';
return;
}
var html = '<div style="max-width:900px">';
records.forEach(function(r) {
var brutto = parseFloat(r.brutto || 0);
var netto = parseFloat(r.netto || 0);
var skatt = parseFloat(r.skatt || 0);
html += '<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:16px 20px;margin-bottom:12px">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
'<div style="font-size:16px;font-weight:700;color:#024550">' + r.period + '</div>' +
'<div style="display:flex;gap:20px;font-size:13px">' +
'<span>Brutto: <strong>' + brutto.toLocaleString('sv-SE') + ' kr</strong></span>' +
'<span>Skatt: <strong style="color:#ef4444">' + skatt.toLocaleString('sv-SE') + ' kr</strong></span>' +
'<span>Netto: <strong style="color:#10b981">' + netto.toLocaleString('sv-SE') + ' kr</strong></span>' +
'</div></div>' +
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px;font-size:12px;color:#64748b">' +
'<div>Grundlön: ' + parseFloat(r.grundlon||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Provision: ' + parseFloat(r.provision||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Milersättning: ' + parseFloat(r.milersattning||0).toLocaleString('sv-SE') + ' kr</div>' +
'<div>Traktamente: ' + parseFloat(r.traktamente||0).toLocaleString('sv-SE') + ' kr</div>' +
(r.ovriga_tillagg && parseFloat(r.ovriga_tillagg) ? '<div>Övriga: ' + parseFloat(r.ovriga_tillagg).toLocaleString('sv-SE') + ' kr</div>' : '') +
'</div>';
if (r.provisions && r.provisions.length) {
html += '<div style="margin-top:10px;padding-top:10px;border-top:1px solid #f1f5f9">' +
'<div style="font-size:11px;color:#94a3b8;margin-bottom:4px">Provisionsdetaljer:</div>';
r.provisions.forEach(function(p) {
html += '<div style="font-size:12px;display:flex;justify-content:space-between;padding:2px 0"><span>' + (p.description || p.deal_title || '#' + p.deal_id) + '</span><span style="font-weight:600">' + parseFloat(p.amount).toLocaleString('sv-SE') + ' kr</span></div>';
});
html += '</div>';
}
html += '</div>';
});
html += '</div>';
panel.innerHTML = html;
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
// --- Möten (Sälj samtal & Utvecklings samtal) ---
async function loadStaffMeetings(staffId, type) {
var panelId = type === 'salj_samtal' ? 'staffTabSalj' : 'staffTabUtveckling';
var panel = document.getElementById(panelId);
var label = type === 'salj_samtal' ? 'Sälj samtal' : 'Utvecklings samtal';
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar...</div>';
try {
var res = await fetch('api/staff-meetings.php?staff_id=' + staffId + '&type=' + type);
var meetings = await res.json();
var html = '<div style="max-width:800px">' +
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">' +
'<h3 style="font-size:16px;font-weight:700;color:#024550;margin:0">' + label + '</h3>' +
'<button onclick="showMeetingForm(' + staffId + ',\'' + type + '\')" style="padding:8px 16px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">+ Nytt samtal</button></div>';
if (!meetings || !meetings.length) {
html += '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga ' + label.toLowerCase() + ' registrerade</div>';
} else {
meetings.forEach(function(m) {
var actions = '';
if (m.action_points) {
try {
var pts = JSON.parse(m.action_points);
if (Array.isArray(pts)) actions = pts.map(function(a){ return '<li style="font-size:12px;margin-bottom:4px">' + a + '</li>'; }).join('');
} catch(e) { actions = '<li style="font-size:12px">' + m.action_points + '</li>'; }
}
html += '<div style="background:#fff;border-radius:12px;border:1px solid #e5e7eb;padding:16px 20px;margin-bottom:12px">' +
'<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px">' +
'<div><div style="font-size:14px;font-weight:600;color:#1a1a1a">' + (m.title || label) + '</div>' +
'<div style="font-size:12px;color:#94a3b8;margin-top:2px">' + (m.meeting_date || '') + (m.conducted_by_name ? ' — ' + m.conducted_by_name : '') + '</div></div>' +
'<div style="display:flex;gap:4px">' +
'<button onclick="editMeetingInline(' + m.id + ',' + staffId + ',\'' + type + '\')" style="background:none;border:none;cursor:pointer;color:#3b82f6;font-size:14px" title="Redigera">✎</button>' +
'<button onclick="deleteMeeting(' + m.id + ',' + staffId + ',\'' + type + '\')" style="background:none;border:none;cursor:pointer;color:#ef4444;font-size:14px" title="Ta bort">✕</button>' +
'</div></div>' +
(m.notes ? '<div style="font-size:13px;color:#374151;margin-bottom:8px;white-space:pre-wrap">' + m.notes + '</div>' : '') +
(actions ? '<div style="margin-top:8px"><div style="font-size:11px;color:#94a3b8;margin-bottom:4px">Åtgärdspunkter:</div><ul style="margin:0;padding-left:20px">' + actions + '</ul></div>' : '') +
(m.next_meeting ? '<div style="margin-top:8px;font-size:12px;color:#3b82f6">Nästa möte: ' + m.next_meeting + '</div>' : '') +
'</div>';
});
}
html += '</div>';
panel.innerHTML = html;
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
function showMeetingForm(staffId, type, existing) {
var label = type === 'salj_samtal' ? 'Sälj samtal' : 'Utvecklings samtal';
var panelId = type === 'salj_samtal' ? 'staffTabSalj' : 'staffTabUtveckling';
var panel = document.getElementById(panelId);
var today = new Date().toISOString().split('T')[0];
var title = existing ? (existing.title || '') : '';
var date = existing ? (existing.meeting_date || today) : today;
var notes = existing ? (existing.notes || '') : '';
var actPts = '';
if (existing && existing.action_points) {
try { var arr = JSON.parse(existing.action_points); actPts = Array.isArray(arr) ? arr.join('\n') : existing.action_points; } catch(e) { actPts = existing.action_points; }
}
var nextM = existing ? (existing.next_meeting || '') : '';
var mid = existing ? existing.id : 0;
var formHtml = '<div id="meetingFormBox" style="background:#f8fafc;border:2px solid #024550;border-radius:12px;padding:20px;margin-bottom:20px">' +
'<h4 style="font-size:14px;font-weight:700;color:#024550;margin-bottom:16px">' + (mid ? 'Redigera' : 'Nytt') + ' ' + label + '</h4>' +
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">' +
'<div><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Titel</label><input type="text" id="mtgTitle" value="' + title.replace(/"/g,'"') + '" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div>' +
'<div><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Datum</label><input type="date" id="mtgDate" value="' + date + '" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;box-sizing:border-box"></div></div>' +
'<div style="margin-bottom:12px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Anteckningar</label><textarea id="mtgNotes" rows="4" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;box-sizing:border-box">' + notes.replace(/</g,'<') + '</textarea></div>' +
'<div style="margin-bottom:12px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Åtgärdspunkter (en per rad)</label><textarea id="mtgActions" rows="3" style="width:100%;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit;resize:vertical;box-sizing:border-box">' + actPts.replace(/</g,'<') + '</textarea></div>' +
'<div style="margin-bottom:16px"><label style="font-size:12px;color:#64748b;display:block;margin-bottom:4px">Nästa möte</label><input type="date" id="mtgNext" value="' + nextM + '" style="padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;font-family:inherit"></div>' +
'<div style="display:flex;gap:10px">' +
'<button onclick="saveMeeting(' + staffId + ',\'' + type + '\',' + mid + ')" style="padding:8px 20px;background:#024550;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">Spara</button>' +
'<button onclick="loadStaffMeetings(' + staffId + ',\'' + type + '\')" style="padding:8px 20px;background:#e5e7eb;color:#374151;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-family:inherit">Avbryt</button>' +
'</div></div>';
var old = document.getElementById('meetingFormBox');
if (old) old.remove();
var header = panel.querySelector('h3');
if (header && header.parentElement) header.parentElement.insertAdjacentHTML('afterend', formHtml);
else panel.insertAdjacentHTML('afterbegin', formHtml);
}
async function saveMeeting(staffId, type, meetingId) {
var actLines = document.getElementById('mtgActions').value.trim().split('\n').filter(function(l){ return l.trim(); });
var body = {
title: document.getElementById('mtgTitle').value.trim(),
meeting_date: document.getElementById('mtgDate').value,
notes: document.getElementById('mtgNotes').value.trim(),
action_points: actLines.length ? JSON.stringify(actLines) : null,
next_meeting: document.getElementById('mtgNext').value || null
};
try {
if (meetingId) {
body.id = meetingId;
await fetch('api/staff-meetings.php?action=update', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
} else {
body.staff_id = staffId;
body.type = type;
body.conducted_by = gStaffId || null;
await fetch('api/staff-meetings.php', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
}
loadStaffMeetings(staffId, type);
} catch(e) { alert('Fel: ' + e.message); }
}
async function editMeetingInline(meetingId, staffId, type) {
try {
var res = await fetch('api/staff-meetings.php?id=' + meetingId);
var m = await res.json();
if (m) showMeetingForm(staffId, type, m);
} catch(e) { alert('Fel: ' + e.message); }
}
async function deleteMeeting(meetingId, staffId, type) {
if (!confirm('Ta bort detta samtal?')) return;
try {
await fetch('api/staff-meetings.php?id=' + meetingId, { method:'DELETE' });
loadStaffMeetings(staffId, type);
} catch(e) { alert('Fel: ' + e.message); }
}
// --- Staff Deals ---
var staffDealsDT = null;
var staffDealsRaw = [];
var staffDealsFiltered = [];
async function loadStaffDeals(staffId) {
var panel = document.getElementById('staffTabAffarslista');
panel.innerHTML = '<div style="padding:20px;text-align:center;color:#94a3b8">Laddar affärer...</div>';
try {
var res = await fetch('api/deals.php?saljare_id=' + staffId + '&limit=200');
var data = await res.json();
staffDealsRaw = Array.isArray(data) ? data : (data.deals || []);
if (!staffDealsRaw.length) {
panel.innerHTML = '<div style="padding:40px;text-align:center;color:#94a3b8;background:#fff;border-radius:12px;border:1px solid #e5e7eb">Inga affärer</div>';
return;
}
renderStaffDealsPanel(staffId);
} catch(e) { panel.innerHTML = '<div style="padding:20px;color:#ef4444">Fel: ' + e.message + '</div>'; }
}
function renderStaffDealsPanel(staffId) {
var panel = document.getElementById('staffTabAffarslista');
// Collect unique months and statuses
var months = {};
var statuses = {};
staffDealsRaw.forEach(function(d) {
var ds = d.datum_salj || '';
if (ds.length >= 7) months[ds.substring(0, 7)] = true;
if (d.status) statuses[d.status] = true;
});
var monthKeys = Object.keys(months).sort().reverse();
var statusKeys = Object.keys(statuses).sort();
// Build filter bar
var html = '<div style="margin-bottom:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">' +
'<select id="sdMonth" onchange="applyStaffDealsFilter(' + staffId + ')" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">' +
'<option value="">Alla månader</option>';
monthKeys.forEach(function(m) {
var parts = m.split('-');
var label = parts[0] + ' ' + ['','Jan','Feb','Mar','Apr','Maj','Jun','Jul','Aug','Sep','Okt','Nov','Dec'][parseInt(parts[1])];
html += '<option value="' + m + '">' + label + '</option>';
});
html += '</select>' +
'<select id="sdStatus" onchange="applyStaffDealsFilter(' + staffId + ')" style="padding:6px 10px;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;font-family:inherit">' +
'<option value="">Alla statusar</option>';
statusKeys.forEach(function(s) {
html += '<option value="' + s + '">' + (DEAL_STATUS_LABELS[s] || s) + '</option>';
});
html += '</select>' +
'<button onclick="document.getElementById(\'sdMonth\').value=\'\';document.getElementById(\'sdStatus\').value=\'\';applyStaffDealsFilter(' + staffId + ')" style="padding:6px 12px;background:#f1f5f9;border:1px solid #e5e7eb;border-radius:8px;font-size:12px;cursor:pointer;font-family:inherit">Rensa</button>' +
'<div id="sdSumBox" style="margin-left:auto;font-size:13px;font-weight:600;color:#024550"></div>' +
'</div>';
// Stats cards
html += '<div id="sdStats" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-bottom:16px"></div>';
// Table
html += '<table id="staffDealsDT" class="display" style="width:100%"><thead><tr>' +
'<th>Affärsnr</th><th>Kund</th><th>Status</th><th>Säljstatus</th><th style="text-align:right">Värde</th><th>Datum</th>' +
'</tr></thead><tbody></tbody></table>';
panel.innerHTML = html;
applyStaffDealsFilter(staffId);
}
function applyStaffDealsFilter(staffId) {
var selMonth = document.getElementById('sdMonth') ? document.getElementById('sdMonth').value : '';
var selStatus = document.getElementById('sdStatus') ? document.getElementById('sdStatus').value : '';
staffDealsFiltered = staffDealsRaw.filter(function(d) {
if (selMonth) {
var ds = d.datum_salj || '';
if (ds.substring(0, 7) !== selMonth) return false;
}
if (selStatus && d.status !== selStatus) return false;
return true;
});
// Compute sum
var totalSum = 0;
staffDealsFiltered.forEach(function(d) { totalSum += parseFloat(d.ordervarde_ink_moms || 0); });
var sumBox = document.getElementById('sdSumBox');
if (sumBox) sumBox.textContent = 'Summa: ' + totalSum.toLocaleString('sv-SE') + ' kr (' + staffDealsFiltered.length + ' affärer)';
// Compute stats from ALL data (unfiltered)
renderStaffDealStats();
// Build DataTable
buildStaffDealsDT(staffDealsFiltered);
}
function renderStaffDealStats() {
var el = document.getElementById('sdStats');
if (!el) return;
var all = staffDealsRaw;
if (!all.length) { el.innerHTML = ''; return; }
// Group by month
var byMonth = {};
var totalVal = 0;
all.forEach(function(d) {
var ds = d.datum_salj || '';
var m = ds.length >= 7 ? ds.substring(0, 7) : 'unknown';
if (!byMonth[m]) byMonth[m] = { count: 0, value: 0, orders: 0 };
byMonth[m].count++;
var v = parseFloat(d.ordervarde_ink_moms || 0);
byMonth[m].value += v;
totalVal += v;
if (d.status === 'order') byMonth[m].orders++;
});
var monthKeys = Object.keys(byMonth).filter(function(k){ return k !== 'unknown'; }).sort();
var numMonths = monthKeys.length || 1;
var avgPerMonth = all.length / numMonths;
var totalOrders = all.filter(function(d){ return d.status === 'order'; }).length;
var avgOrdersPerMonth = totalOrders / numMonths;
var avgValuePerMonth = totalVal / numMonths;
// Current month trend
var now = new Date();
var curKey = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
var curMonth = byMonth[curKey] || { count: 0, value: 0, orders: 0 };
var dayOfMonth = now.getDate();
var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
var projectedCount = dayOfMonth > 0 ? Math.round(curMonth.count / dayOfMonth * daysInMonth) : 0;
var projectedValue = dayOfMonth > 0 ? Math.round(curMonth.value / dayOfMonth * daysInMonth) : 0;
var trending = projectedCount > avgPerMonth;
var trendPct = avgPerMonth > 0 ? Math.round((projectedCount - avgPerMonth) / avgPerMonth * 100) : 0;
function statCard(title, value, sub, color) {
return '<div style="background:#fff;border-radius:10px;border:1px solid #e5e7eb;padding:12px 14px">' +
'<div style="font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:.3px">' + title + '</div>' +
'<div style="font-size:20px;font-weight:700;color:' + (color || '#024550') + ';margin:4px 0">' + value + '</div>' +
(sub ? '<div style="font-size:11px;color:#64748b">' + sub + '</div>' : '') +
'</div>';
}
el.innerHTML =
statCard('Totalt', all.length + ' affärer', totalVal.toLocaleString('sv-SE') + ' kr') +
statCard('Snitt/mån', avgPerMonth.toFixed(1) + ' affärer', avgValuePerMonth.toLocaleString('sv-SE', {maximumFractionDigits:0}) + ' kr/mån') +
statCard('Order snitt/mån', avgOrdersPerMonth.toFixed(1), totalOrders + ' totalt') +
statCard('Denna månad', curMonth.count + ' affärer',
curMonth.value.toLocaleString('sv-SE') + ' kr') +
statCard('Prognos månad', projectedCount + ' affärer',
projectedValue.toLocaleString('sv-SE') + ' kr') +
statCard('Trend',
(trending ? '▲ +' : '▼ ') + trendPct + '%',
trending ? 'Bättre än snitt' : 'Under snitt',
trending ? '#10b981' : '#ef4444');
}
function buildStaffDealsDT(deals) {
var rows = deals.map(function(d) {
return [
d.id,
d.deal_number || '',
d.customer_name || '',
d.status || '',
d.salj_status || '',
parseFloat(d.ordervarde_ink_moms || 0),
d.datum_salj || ''
];
});
if (staffDealsDT) { staffDealsDT.destroy(); staffDealsDT = null; }
staffDealsDT = $('#staffDealsDT').DataTable({
data: rows,
columns: [
{ title:'Affärsnr', render: function(d,t,r){ return '<strong style="color:#024550">' + r[1] + '</strong>'; } },
{ title:'Kund', render: function(d,t,r){ return r[2]; } },
{ title:'Status', render: function(d,t,r){
var color = DEAL_STATUS_COLORS[r[3]] || '#94a3b8';
var label = DEAL_STATUS_LABELS[r[3]] || r[3] || '-';
return '<span style="font-size:10px;padding:2px 8px;border-radius:10px;color:#fff;background:'+color+'">'+label+'</span>';
}},
{ title:'Säljstatus', render: function(d,t,r){ return r[4] || '-'; }, width:'90px' },
{ title:'Värde', className:'dt-right', render: function(d,t,r){
return r[5] ? r[5].toLocaleString('sv-SE') + ' kr' : '-';
}},
{ title:'Datum', render: function(d,t,r){ return '<span style="font-size:12px;color:#64748b">'+r[6]+'</span>'; }, width:'100px' }
],
language: {
search:'Sök:', lengthMenu:'Visa _MENU_ per sida',
info:'Visar _START_-_END_ av _TOTAL_ affärer', infoEmpty:'Inga affärer',
infoFiltered:'(filtrerat från _MAX_ totalt)',
paginate:{first:'Första',last:'Sista',next:'Nästa',previous:'Föreg.'},
zeroRecords:'Inga affärer hittades'
},
pageLength: 25,
order: [[5,'desc']],
createdRow: function(row, data) {
$(row).css('cursor','pointer').on('click', function(){ showDealDetail(data[0]); });
}
});
}
function showStaffModal(staffData) {
var modal = document.getElementById('staffModal');
document.getElementById('staffModalTitle').textContent = staffData ? 'Redigera personal' : 'Lägg till personal';
document.getElementById('staffEditId').value = staffData ? staffData.id : '';
document.getElementById('staffName').value = staffData ? staffData.name : '';
document.getElementById('staffEmail').value = staffData ? (staffData.email || '') : '';
document.getElementById('staffRole').value = staffData ? staffData.role : 'saljare';
document.getElementById('staffPhone').value = staffData ? (staffData.phone || '') : '';
document.getElementById('staffTitle').value = staffData ? (staffData.title || '') : '';
document.getElementById('staffPnr').value = staffData ? (staffData.personnummer || '') : '';
document.getElementById('staffAddress').value = staffData ? (staffData.address || '') : '';
document.getElementById('staffZip').value = staffData ? (staffData.zip || '') : '';
document.getElementById('staffCity').value = staffData ? (staffData.city || '') : '';
document.getElementById('staffStartDate').value = staffData ? (staffData.start_date || '') : '';
document.getElementById('staffGrundlon').value = staffData ? (staffData.grundlon || '') : '';
document.getElementById('staffSkatt').value = staffData ? (staffData.skatt_procent || '30') : '30';
document.getElementById('staffModalError').style.display = 'none';
modal.style.display = 'flex';
}
function closeStaffModal() {
document.getElementById('staffModal').style.display = 'none';
}
async function saveStaff() {
var id = document.getElementById('staffEditId').value;
var data = {
name: document.getElementById('staffName').value.trim(),
email: document.getElementById('staffEmail').value.trim(),
role: document.getElementById('staffRole').value,
phone: document.getElementById('staffPhone').value.trim() || null,
title: document.getElementById('staffTitle').value.trim() || null,
personnummer: document.getElementById('staffPnr').value.trim() || null,
address: document.getElementById('staffAddress').value.trim() || null,
zip: document.getElementById('staffZip').value.trim() || null,
city: document.getElementById('staffCity').value.trim() || null,
start_date: document.getElementById('staffStartDate').value || null,
grundlon: document.getElementById('staffGrundlon').value ? parseFloat(document.getElementById('staffGrundlon').value) : null,
skatt_procent: document.getElementById('staffSkatt').value ? parseFloat(document.getElementById('staffSkatt').value) : 30,
};
if (!data.name || !data.email) {
const err = document.getElementById('staffModalError');
err.textContent = 'Namn och e-post krävs';
err.style.display = 'block';
return;
}
try {
const url = id ? STAFF_API + '?id=' + id : STAFF_API;
const res = await fetch(url, {
method: id ? 'PUT' : 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
const result = await res.json();
if (!res.ok) {
const err = document.getElementById('staffModalError');
err.textContent = result.error || 'Något gick fel';
err.style.display = 'block';
return;
}
closeStaffModal();
loadStaff();
// Refresh detail view if open
if (currentStaffData && currentStaffData.id == id) {
showStaffDetail(parseInt(id));
}
} catch (e) {
var err = document.getElementById('staffModalError');
err.textContent = e.message;
err.style.display = 'block';
}
}
async function editStaff(id) {
try {
var res = await fetch(STAFF_API + '?id=' + id);
var staff = await res.json();
showStaffModal(staff);
} catch (e) {
alert('Kunde inte ladda: ' + e.message);
}
}
async function toggleStaffActive(id, currentActive) {
try {
await fetch(STAFF_API + '?id=' + id, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({active: currentActive == 1 ? 0 : 1}),
});
loadStaff();
} catch (e) {
alert('Fel: ' + e.message);
}
}
// Monday.com settings