const API = 'api.php'; let customers = []; let sortField = 'yritys'; let sortAsc = true; let currentDetailId = null; let currentUser = { username: '', nimi: '', role: '' }; let currentCompany = null; // {id, nimi} let availableCompanies = []; // [{id, nimi}, ...] let currentTicketCompanyId = ''; // Avatun tiketin yritys (cross-company tuki) let currentUserSignatures = {}; // {mailbox_id: "allekirjoitus teksti"} // Elements const loginScreen = document.getElementById('login-screen'); const dashboard = document.getElementById('dashboard'); const loginForm = document.getElementById('login-form'); const loginError = document.getElementById('login-error'); const searchInput = document.getElementById('search-input'); const tbody = document.getElementById('customer-tbody'); const noCustomers = document.getElementById('no-customers'); const customerCount = document.getElementById('customer-count'); const totalBilling = document.getElementById('total-billing'); const customerModal = document.getElementById('customer-modal'); const detailModal = document.getElementById('detail-modal'); const customerForm = document.getElementById('customer-form'); const userModal = document.getElementById('user-modal'); // API helpers async function apiCall(action, method = 'GET', body = null) { const opts = { method, credentials: 'include' }; if (body) { opts.headers = { 'Content-Type': 'application/json' }; opts.body = JSON.stringify(body); } const res = await fetch(`${API}?action=${action}`, opts); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Virhe'); return data; } // ==================== AUTH ==================== const forgotBox = document.getElementById('forgot-box'); const resetBox = document.getElementById('reset-box'); const loginBox = document.querySelector('.login-box'); function showLoginView() { loginBox.style.display = ''; forgotBox.style.display = 'none'; resetBox.style.display = 'none'; } function showForgotView() { loginBox.style.display = 'none'; forgotBox.style.display = ''; resetBox.style.display = 'none'; } function showResetView() { loginBox.style.display = 'none'; forgotBox.style.display = 'none'; resetBox.style.display = ''; } document.getElementById('forgot-link').addEventListener('click', (e) => { e.preventDefault(); showForgotView(); }); document.getElementById('forgot-back').addEventListener('click', (e) => { e.preventDefault(); showLoginView(); }); async function loadCaptcha() { try { const data = await apiCall('captcha'); document.getElementById('captcha-question').textContent = data.question; } catch (e) { document.getElementById('captcha-question').textContent = 'Virhe'; } } // Salasanan palautuspyyntö document.getElementById('forgot-form').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('forgot-username').value; const forgotMsg = document.getElementById('forgot-msg'); const forgotError = document.getElementById('forgot-error'); forgotMsg.style.display = 'none'; forgotError.style.display = 'none'; try { await apiCall('password_reset_request', 'POST', { username }); forgotMsg.textContent = 'Jos käyttäjätunnukselle on sähköposti, palautuslinkki on lähetetty.'; forgotMsg.style.display = 'block'; } catch (err) { forgotError.textContent = err.message; forgotError.style.display = 'block'; } }); // Salasanan vaihto (reset token) document.getElementById('reset-form').addEventListener('submit', async (e) => { e.preventDefault(); const pw1 = document.getElementById('reset-password').value; const pw2 = document.getElementById('reset-password2').value; const resetMsg = document.getElementById('reset-msg'); const resetError = document.getElementById('reset-error'); resetMsg.style.display = 'none'; resetError.style.display = 'none'; if (pw1 !== pw2) { resetError.textContent = 'Salasanat eivät täsmää'; resetError.style.display = 'block'; return; } const params = new URLSearchParams(window.location.search); const token = params.get('reset'); try { await apiCall('password_reset', 'POST', { token, password: pw1 }); resetMsg.textContent = 'Salasana vaihdettu! Voit nyt kirjautua.'; resetMsg.style.display = 'block'; document.getElementById('reset-form').style.display = 'none'; setTimeout(() => { window.location.href = window.location.pathname; }, 3000); } catch (err) { resetError.textContent = err.message; resetError.style.display = 'block'; } }); async function checkAuth() { // Tarkista onko URL:ssa reset-token const params = new URLSearchParams(window.location.search); if (params.get('reset')) { loginScreen.style.display = 'flex'; try { const data = await apiCall('validate_reset_token&token=' + encodeURIComponent(params.get('reset'))); if (data.valid) { showResetView(); return; } } catch (e) {} showResetView(); document.getElementById('reset-error').textContent = 'Palautuslinkki on vanhentunut tai virheellinen'; document.getElementById('reset-error').style.display = 'block'; document.getElementById('reset-form').style.display = 'none'; return; } try { const data = await apiCall('check_auth'); if (data.authenticated) { currentUser = { username: data.username, nimi: data.nimi, role: data.role, id: data.user_id }; availableCompanies = data.companies || []; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentUserSignatures = data.signatures || {}; if (data.branding) applyBranding(data.branding); applyModules(data.enabled_modules || []); showDashboard(); return; } } catch (e) { /* not logged in */ } // Ei kirjautuneena → näytä login loginScreen.style.display = 'flex'; } loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; const captcha = document.getElementById('login-captcha').value; try { const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) }); loginError.style.display = 'none'; currentUser = { username: data.username, nimi: data.nimi, role: data.role, id: data.user_id }; availableCompanies = data.companies || []; currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null; currentUserSignatures = data.signatures || {}; showDashboard(); } catch (err) { loginError.textContent = err.message; loginError.style.display = 'block'; document.getElementById('login-captcha').value = ''; loadCaptcha(); } }); document.getElementById('btn-logout').addEventListener('click', async () => { await apiCall('logout'); dashboard.style.display = 'none'; loginScreen.style.display = 'flex'; document.getElementById('login-username').value = ''; document.getElementById('login-password').value = ''; document.getElementById('login-captcha').value = ''; showLoginView(); loadCaptcha(); loadBranding(); // Domain-pohjainen brändäys uudelleen }); async function showDashboard() { loginScreen.style.display = 'none'; dashboard.style.display = 'block'; document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username; const isSuperAdmin = currentUser.role === 'superadmin'; const isAdmin = currentUser.role === 'admin' || isSuperAdmin; // Näytä admin-toiminnot roolin mukaan document.getElementById('btn-users').style.display = isSuperAdmin ? '' : 'none'; document.getElementById('tab-settings').style.display = isAdmin ? '' : 'none'; document.getElementById('btn-companies').style.display = isAdmin ? '' : 'none'; // Yritysvalitsin populateCompanySelector(); // Avaa oikea tabi URL-hashin perusteella (tai customers oletuks) const hash = window.location.hash.replace('#', ''); const validTabs = ['customers', 'leads', 'devices', 'archive', 'changelog', 'support', 'users', 'settings', 'companies']; const startTab = validTabs.includes(hash) ? hash : 'customers'; switchToTab(startTab); } function populateCompanySelector() { const sel = document.getElementById('company-selector'); if (availableCompanies.length <= 1) { sel.style.display = 'none'; return; } sel.style.display = ''; sel.innerHTML = availableCompanies.map(c => `` ).join(''); } async function switchCompany(companyId) { try { await apiCall('company_switch', 'POST', { company_id: companyId }); currentCompany = availableCompanies.find(c => c.id === companyId) || null; // Päivitä brändäys vaihdetun yrityksen mukaan try { const auth = await apiCall('check_auth'); if (auth.branding) applyBranding(auth.branding); applyModules(auth.enabled_modules || []); } catch (e2) {} // Lataa uudelleen aktiivinen tab const hash = window.location.hash.replace('#', '') || 'customers'; switchToTab(hash); } catch (e) { alert(e.message); } } // ==================== TABS ==================== function switchToTab(target) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); const tabBtn = document.querySelector(`.tab[data-tab="${target}"]`); if (tabBtn) tabBtn.classList.add('active'); const content = document.getElementById('tab-content-' + target); if (content) content.classList.add('active'); // Tallenna aktiivinen tabi URL-hashiin window.location.hash = target; // Lataa sisältö tarvittaessa if (target === 'customers') loadCustomers(); if (target === 'leads') loadLeads(); if (target === 'devices') loadDevices(); if (target === 'archive') loadArchive(); if (target === 'changelog') loadChangelog(); if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); } if (target === 'users') loadUsers(); if (target === 'settings') loadSettings(); if (target === 'companies') loadCompaniesTab(); } document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { switchToTab(tab.dataset.tab); }); }); // Logo -> Asiakkaat (alkunäkymä) document.getElementById('brand-home').addEventListener('click', () => { switchToTab('customers'); }); // Käyttäjät-nappi headerissa document.getElementById('btn-users').addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.getElementById('tab-content-users').classList.add('active'); loadUsers(); }); document.getElementById('btn-companies').addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.getElementById('tab-content-companies').classList.add('active'); window.location.hash = 'companies'; loadCompaniesTab(); }); // ==================== CUSTOMERS ==================== async function loadCustomers() { customers = await apiCall('customers'); renderTable(); } function flattenRows(customerList) { const rows = []; customerList.forEach(c => { const liittymat = c.liittymat || []; if (liittymat.length === 0) { rows.push({ customer: c, liittyma: { asennusosoite: '', postinumero: '', kaupunki: '', liittymanopeus: '', hinta: 0, sopimuskausi: '', alkupvm: '' }, index: 0 }); } else { liittymat.forEach((l, i) => rows.push({ customer: c, liittyma: l, index: i })); } }); return rows; } function renderTable() { const query = searchInput.value.toLowerCase().trim(); let filtered = customers; if (query) { filtered = customers.filter(c => { const liittymat = c.liittymat || []; const inL = liittymat.some(l => (l.asennusosoite || '').toLowerCase().includes(query) || (l.postinumero || '').toLowerCase().includes(query) || (l.kaupunki || '').toLowerCase().includes(query) || (l.liittymanopeus || '').toLowerCase().includes(query) ); return c.yritys.toLowerCase().includes(query) || (c.yhteyshenkilö || '').toLowerCase().includes(query) || inL; }); } const rows = flattenRows(filtered); rows.sort((a, b) => { let va, vb; if (['asennusosoite', 'postinumero', 'kaupunki', 'liittymanopeus', 'hinta', 'sopimuskausi'].includes(sortField)) { va = a.liittyma[sortField] ?? ''; vb = b.liittyma[sortField] ?? ''; } else { va = a.customer[sortField] ?? ''; vb = b.customer[sortField] ?? ''; } if (sortField === 'hinta') { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; } else { va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); } if (va < vb) return sortAsc ? -1 : 1; if (va > vb) return sortAsc ? 1 : -1; return 0; }); if (rows.length === 0) { tbody.innerHTML = ''; noCustomers.style.display = 'block'; document.getElementById('customer-table').style.display = 'none'; } else { noCustomers.style.display = 'none'; document.getElementById('customer-table').style.display = 'table'; let prevId = null; tbody.innerHTML = rows.map(r => { const c = r.customer, l = r.liittyma; const isFirst = c.id !== prevId; prevId = c.id; const sopimus = l.sopimuskausi ? l.sopimuskausi + ' kk' : ''; const alkupvm = l.alkupvm ? ' (' + esc(l.alkupvm) + ')' : ''; return ` ${isFirst ? '' + esc(c.yritys) + '' : ''} ${esc(l.asennusosoite)}${l.postinumero ? ', ' + esc(l.postinumero) : ''} ${esc(l.kaupunki)} ${esc(l.liittymanopeus)} ${formatPrice(l.hinta)} ${sopimus}${alkupvm} ${isFirst ? `` : ''} `; }).join(''); } updateSummary(); } function getAllLiittymat() { const all = []; customers.forEach(c => (c.liittymat || []).forEach(l => all.push(l))); return all; } function updateSummary() { const liittymat = getAllLiittymat(); const count = customers.length; const connCount = liittymat.length; const total = liittymat.reduce((sum, l) => sum + (parseFloat(l.hinta) || 0), 0); customerCount.textContent = `${count} asiakasta, ${connCount} liittymää`; totalBilling.textContent = `Laskutus yhteensä: ${formatPrice(total)}/kk`; setText('stat-count', count); setText('stat-connections', connCount); setText('stat-billing', formatPrice(total)); setText('stat-yearly', formatPrice(total * 12)); updateTrivia(liittymat, connCount); } function updateTrivia(liittymat, connCount) { if (connCount === 0) { setTrivia('stat-top-zip', '-', ''); setText('stat-avg-price', '-'); const st = document.getElementById('stat-speed-table'); if (st) st.innerHTML = '-'; return; } // Postinumero const zipCounts = {}; liittymat.forEach(l => { const z = (l.postinumero || '').trim(); if (z) zipCounts[z] = (zipCounts[z] || 0) + 1; }); const topZip = Object.entries(zipCounts).sort((a, b) => b[1] - a[1])[0]; if (topZip) { const city = liittymat.find(l => (l.postinumero || '').trim() === topZip[0]); setTrivia('stat-top-zip', topZip[0], `${topZip[1]} liittymää` + (city && city.kaupunki ? ` (${city.kaupunki})` : '')); } else { setTrivia('stat-top-zip', '-', ''); } // Nopeudet const speedCounts = {}; liittymat.forEach(l => { const s = (l.liittymanopeus || '').trim(); if (s) speedCounts[s] = (speedCounts[s] || 0) + 1; }); const speedTable = document.getElementById('stat-speed-table'); if (speedTable) { const sorted = Object.entries(speedCounts).sort((a, b) => b[1] - a[1]); const maxC = sorted.length > 0 ? sorted[0][1] : 0; speedTable.innerHTML = sorted.length === 0 ? '-' : sorted.map(([sp, cnt]) => { const isTop = cnt === maxC; const w = Math.max(15, (cnt / maxC) * 50); return `${esc(sp)} (${cnt})`; }).join(''); } // Keskihinta const total = liittymat.reduce((sum, l) => sum + (parseFloat(l.hinta) || 0), 0); setText('stat-avg-price', formatPrice(total / connCount)); } function setTrivia(id, value, sub) { const el = document.getElementById(id); const subEl = document.getElementById(id + '-detail'); if (el) el.textContent = value; if (subEl) subEl.textContent = sub; } function setText(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; } function formatPrice(val) { return parseFloat(val || 0).toFixed(2).replace('.', ',') + ' €'; } function esc(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } function timeAgo(dateStr) { if (!dateStr) return ''; const date = new Date(dateStr.replace(' ', 'T')); const now = new Date(); const diffMs = now - date; if (diffMs < 0) return 'juuri nyt'; const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return 'juuri nyt'; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return diffMin + ' min sitten'; const diffHours = Math.floor(diffMin / 60); if (diffHours < 24) return diffHours + 'h sitten'; const diffDays = Math.floor(diffHours / 24); if (diffDays < 7) return diffDays + 'pv sitten'; if (diffDays < 30) return Math.floor(diffDays / 7) + 'vk sitten'; // Yli kuukausi → näytä päivämäärä return dateStr.substring(0, 10); } // Search & Sort searchInput.addEventListener('input', () => renderTable()); document.querySelectorAll('th[data-sort]').forEach(th => { th.addEventListener('click', () => { const f = th.dataset.sort; if (sortField === f) sortAsc = !sortAsc; else { sortField = f; sortAsc = true; } renderTable(); }); }); // Row click tbody.addEventListener('click', (e) => { const row = e.target.closest('tr'); if (row) showDetail(row.dataset.id); }); function detailVal(val) { return val ? esc(val) : '-'; } function detailLink(val, type) { if (!val) return '-'; if (type === 'tel') return `${esc(val)}`; if (type === 'email') return `${esc(val)}`; return esc(val); } function showDetail(id) { const c = customers.find(x => x.id === id); if (!c) return; currentDetailId = id; const liittymat = c.liittymat || []; const fullBilling = [c.laskutusosoite, c.laskutuspostinumero, c.laskutuskaupunki].filter(Boolean).join(', '); const liittymatHtml = liittymat.map((l, i) => { const addr = [l.asennusosoite, l.postinumero, l.kaupunki].filter(Boolean).join(', '); return `
${liittymat.length > 1 ? `
Liittymä ${i + 1}
` : ''}
Osoite
${detailVal(addr)}
Nopeus
${detailVal(l.liittymanopeus)}
Hinta / kk
${formatPrice(l.hinta)}
Sopimuskausi
${l.sopimuskausi ? l.sopimuskausi + ' kk' : '-'}
Alkaen
${detailVal(l.alkupvm)}
`; }).join(''); const totalH = liittymat.reduce((s, l) => s + (parseFloat(l.hinta) || 0), 0); document.getElementById('detail-title').textContent = c.yritys; document.getElementById('detail-body').innerHTML = `

Perustiedot

Yritys
${detailVal(c.yritys)}
Y-tunnus
${detailVal(c.ytunnus)}

Liittymät (${liittymat.length})

${liittymatHtml} ${liittymat.length > 1 ? `
Yhteensä: ${formatPrice(totalH)}/kk
` : ''}

Yhteystiedot

Yhteyshenkilö
${detailVal(c.yhteyshenkilö)}
Puhelin
${detailLink(c.puhelin, 'tel')}
Sähköposti
${detailLink(c.sahkoposti, 'email')}

Laskutustiedot

Laskutusosoite
${detailVal(fullBilling)}
Laskutussähköposti
${detailLink(c.laskutussahkoposti, 'email')}
E-laskuosoite
${detailVal(c.elaskuosoite)}
E-laskuvälittäjä
${detailVal(c.elaskuvalittaja)}
${c.lisatiedot ? `

Lisätiedot

${esc(c.lisatiedot)}

` : ''}

Tiedostot

Max 20 MB / tiedosto
`; detailModal.style.display = 'flex'; loadFiles(id); document.getElementById('file-upload-input').addEventListener('change', async function () { for (const file of this.files) { const fd = new FormData(); fd.append('customer_id', id); fd.append('file', file); try { const res = await fetch(`${API}?action=file_upload`, { method: 'POST', credentials: 'include', body: fd }); const data = await res.json(); if (!res.ok) alert(data.error || 'Virhe'); } catch (e) { alert('Tiedoston lähetys epäonnistui'); } } this.value = ''; loadFiles(id); }); } async function loadFiles(customerId) { const fileList = document.getElementById('file-list'); if (!fileList) return; try { const files = await apiCall(`file_list&customer_id=${customerId}`); if (files.length === 0) { fileList.innerHTML = '

Ei tiedostoja.

'; return; } fileList.innerHTML = files.map(f => `
${esc(f.filename)} ${formatFileSize(f.size)} · ${f.modified}
`).join(''); } catch (e) { fileList.innerHTML = '

Virhe ladattaessa tiedostoja.

'; } } function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } async function deleteFile(customerId, filename) { if (!confirm(`Poistetaanko tiedosto "${filename}"?`)) return; await apiCall('file_delete', 'POST', { customer_id: customerId, filename }); loadFiles(customerId); } // Detail modal actions document.getElementById('detail-close').addEventListener('click', () => detailModal.style.display = 'none'); document.getElementById('detail-cancel').addEventListener('click', () => detailModal.style.display = 'none'); document.getElementById('detail-edit').addEventListener('click', () => { detailModal.style.display = 'none'; editCustomer(currentDetailId); }); document.getElementById('detail-delete').addEventListener('click', () => { const c = customers.find(x => x.id === currentDetailId); if (c) { detailModal.style.display = 'none'; deleteCustomer(currentDetailId, c.yritys); } }); // ==================== FORM: Liittymät ==================== function createLiittymaRow(data = {}, index = 0) { const div = document.createElement('div'); div.className = 'liittyma-row'; div.dataset.index = index; div.innerHTML = `
Liittymä ${index + 1}
`; div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); }); return div; } function renumberLiittymaRows() { document.getElementById('liittymat-container').querySelectorAll('.liittyma-row').forEach((row, i) => { row.dataset.index = i; row.querySelector('.liittyma-row-title').textContent = `Liittymä ${i + 1}`; }); } function collectLiittymatFromForm() { return Array.from(document.getElementById('liittymat-container').querySelectorAll('.liittyma-row')).map(row => ({ asennusosoite: row.querySelector('.l-asennusosoite').value, postinumero: row.querySelector('.l-postinumero').value, kaupunki: row.querySelector('.l-kaupunki').value, liittymanopeus: row.querySelector('.l-liittymanopeus').value, hinta: row.querySelector('.l-hinta').value, sopimuskausi: row.querySelector('.l-sopimuskausi').value, alkupvm: row.querySelector('.l-alkupvm').value, })); } document.getElementById('btn-add-liittyma').addEventListener('click', () => { const container = document.getElementById('liittymat-container'); container.appendChild(createLiittymaRow({}, container.querySelectorAll('.liittyma-row').length)); }); document.getElementById('form-billing-same').addEventListener('change', function () { const bf = document.getElementById('billing-fields'); if (this.checked) { bf.style.display = 'none'; const first = document.querySelector('.liittyma-row'); if (first) { document.getElementById('form-laskutusosoite').value = first.querySelector('.l-asennusosoite').value; document.getElementById('form-laskutuspostinumero').value = first.querySelector('.l-postinumero').value; document.getElementById('form-laskutuskaupunki').value = first.querySelector('.l-kaupunki').value; } } else { bf.style.display = 'block'; } }); // Add/Edit modal document.getElementById('btn-add').addEventListener('click', () => openCustomerForm()); document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none'); document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none'); function openCustomerForm(customer = null) { const c = customer; document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas'; document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna'; document.getElementById('form-id').value = c ? c.id : ''; document.getElementById('form-yritys').value = c ? c.yritys : ''; document.getElementById('form-ytunnus').value = c ? (c.ytunnus || '') : ''; document.getElementById('form-yhteyshenkilo').value = c ? (c.yhteyshenkilö || '') : ''; document.getElementById('form-puhelin').value = c ? (c.puhelin || '') : ''; document.getElementById('form-sahkoposti').value = c ? (c.sahkoposti || '') : ''; document.getElementById('form-laskutusosoite').value = c ? (c.laskutusosoite || '') : ''; document.getElementById('form-laskutuspostinumero').value = c ? (c.laskutuspostinumero || '') : ''; document.getElementById('form-laskutuskaupunki').value = c ? (c.laskutuskaupunki || '') : ''; document.getElementById('form-laskutussahkoposti').value = c ? (c.laskutussahkoposti || '') : ''; document.getElementById('form-elaskuosoite').value = c ? (c.elaskuosoite || '') : ''; document.getElementById('form-elaskuvalittaja').value = c ? (c.elaskuvalittaja || '') : ''; document.getElementById('form-lisatiedot').value = c ? (c.lisatiedot || '') : ''; document.getElementById('form-priority-emails').value = c ? (c.priority_emails || '') : ''; document.getElementById('form-billing-same').checked = false; document.getElementById('billing-fields').style.display = 'block'; const container = document.getElementById('liittymat-container'); container.innerHTML = ''; (c ? (c.liittymat || []) : [{}]).forEach((l, i) => container.appendChild(createLiittymaRow(l, i))); customerModal.style.display = 'flex'; document.getElementById('form-yritys').focus(); } function editCustomer(id) { const c = customers.find(x => x.id === id); if (c) openCustomerForm(c); } async function deleteCustomer(id, name) { if (!confirm(`Arkistoidaanko asiakas "${name}"?\n\nAsiakas siirretään arkistoon, josta sen voi palauttaa.`)) return; await apiCall('customer_delete', 'POST', { id }); await loadCustomers(); } customerForm.addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('form-id').value; if (document.getElementById('form-billing-same').checked) { const first = document.querySelector('.liittyma-row'); if (first) { document.getElementById('form-laskutusosoite').value = first.querySelector('.l-asennusosoite').value; document.getElementById('form-laskutuspostinumero').value = first.querySelector('.l-postinumero').value; document.getElementById('form-laskutuskaupunki').value = first.querySelector('.l-kaupunki').value; } } const data = { yritys: document.getElementById('form-yritys').value, ytunnus: document.getElementById('form-ytunnus').value, yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value, puhelin: document.getElementById('form-puhelin').value, sahkoposti: document.getElementById('form-sahkoposti').value, laskutusosoite: document.getElementById('form-laskutusosoite').value, laskutuspostinumero: document.getElementById('form-laskutuspostinumero').value, laskutuskaupunki: document.getElementById('form-laskutuskaupunki').value, laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value, elaskuosoite: document.getElementById('form-elaskuosoite').value, elaskuvalittaja: document.getElementById('form-elaskuvalittaja').value, lisatiedot: document.getElementById('form-lisatiedot').value, priority_emails: document.getElementById('form-priority-emails').value, liittymat: collectLiittymatFromForm(), }; if (id) { data.id = id; await apiCall('customer_update', 'POST', data); } else { await apiCall('customer', 'POST', data); } customerModal.style.display = 'none'; await loadCustomers(); }); // ==================== LEADS ==================== let leads = []; let currentLeadId = null; const leadModal = document.getElementById('lead-modal'); const leadDetailModal = document.getElementById('lead-detail-modal'); const leadStatusLabels = { uusi: 'Uusi', kontaktoitu: 'Kontaktoitu', kiinnostunut: 'Kiinnostunut', odottaa: 'Odottaa toimitusta', ei_kiinnosta: 'Ei kiinnosta', }; async function loadLeads() { try { leads = await apiCall('leads'); renderLeads(); } catch (e) { console.error(e); } } function renderLeads() { const query = document.getElementById('lead-search-input').value.toLowerCase().trim(); let filtered = leads; if (query) { filtered = leads.filter(l => (l.yritys || '').toLowerCase().includes(query) || (l.yhteyshenkilo || '').toLowerCase().includes(query) || (l.kaupunki || '').toLowerCase().includes(query) ); } const ltbody = document.getElementById('leads-tbody'); const noLeads = document.getElementById('no-leads'); if (filtered.length === 0) { ltbody.innerHTML = ''; noLeads.style.display = 'block'; document.getElementById('leads-table').style.display = 'none'; } else { noLeads.style.display = 'none'; document.getElementById('leads-table').style.display = 'table'; ltbody.innerHTML = filtered.map(l => ` ${esc(l.yritys)} ${esc(l.yhteyshenkilo || '')} ${esc(l.kaupunki || '')} ${leadStatusLabels[l.tila] || l.tila || 'Uusi'} ${esc((l.luotu || '').substring(0, 10))} `).join(''); } document.getElementById('lead-count').textContent = `${leads.length} liidiä`; } document.getElementById('lead-search-input').addEventListener('input', () => renderLeads()); document.getElementById('leads-tbody').addEventListener('click', (e) => { const row = e.target.closest('tr'); if (row && row.dataset.leadId) showLeadDetail(row.dataset.leadId); }); function showLeadDetail(id) { const l = leads.find(x => x.id === id); if (!l) return; currentLeadId = id; document.getElementById('lead-detail-title').textContent = l.yritys; document.getElementById('lead-detail-body').innerHTML = `
Yritys
${detailVal(l.yritys)}
Tila
${leadStatusLabels[l.tila] || 'Uusi'}
Yhteyshenkilö
${detailVal(l.yhteyshenkilo)}
Puhelin
${detailLink(l.puhelin, 'tel')}
Sähköposti
${detailLink(l.sahkoposti, 'email')}
Osoite
${detailVal([l.osoite, l.kaupunki].filter(Boolean).join(', '))}
Lisätty
${detailVal(l.luotu)} (${esc(l.luoja || '')})
${l.muokattu ? `
Muokattu
${timeAgo(l.muokattu)} (${esc(l.muokkaaja || '')})
` : ''}
${l.muistiinpanot ? `
MUISTIINPANOT
${esc(l.muistiinpanot)}
` : ''}
`; leadDetailModal.style.display = 'flex'; } // Lead form document.getElementById('btn-add-lead').addEventListener('click', () => openLeadForm()); document.getElementById('lead-modal-close').addEventListener('click', () => leadModal.style.display = 'none'); document.getElementById('lead-form-cancel').addEventListener('click', () => leadModal.style.display = 'none'); function openLeadForm(lead = null) { document.getElementById('lead-modal-title').textContent = lead ? 'Muokkaa liidiä' : 'Lisää liidi'; document.getElementById('lead-form-submit').textContent = lead ? 'Päivitä' : 'Tallenna'; document.getElementById('lead-form-id').value = lead ? lead.id : ''; document.getElementById('lead-form-yritys').value = lead ? lead.yritys : ''; document.getElementById('lead-form-yhteyshenkilo').value = lead ? (lead.yhteyshenkilo || '') : ''; document.getElementById('lead-form-puhelin').value = lead ? (lead.puhelin || '') : ''; document.getElementById('lead-form-sahkoposti').value = lead ? (lead.sahkoposti || '') : ''; document.getElementById('lead-form-osoite').value = lead ? (lead.osoite || '') : ''; document.getElementById('lead-form-kaupunki').value = lead ? (lead.kaupunki || '') : ''; document.getElementById('lead-form-tila').value = lead ? (lead.tila || 'uusi') : 'uusi'; document.getElementById('lead-form-muistiinpanot').value = lead ? (lead.muistiinpanot || '') : ''; leadModal.style.display = 'flex'; document.getElementById('lead-form-yritys').focus(); } function editLead(id) { const l = leads.find(x => x.id === id); if (l) { leadDetailModal.style.display = 'none'; openLeadForm(l); } } async function deleteLead(id, name) { if (!confirm(`Poistetaanko liidi "${name}"?`)) return; await apiCall('lead_delete', 'POST', { id }); leadDetailModal.style.display = 'none'; await loadLeads(); } async function convertLeadToCustomer(id) { const l = leads.find(x => x.id === id); if (!l) return; if (!confirm(`Muutetaanko "${l.yritys}" asiakkaaksi?\n\nLiidi poistetaan ja asiakas luodaan sen tiedoilla.`)) return; await apiCall('lead_to_customer', 'POST', { id }); leadDetailModal.style.display = 'none'; await loadLeads(); await loadCustomers(); } document.getElementById('lead-form').addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('lead-form-id').value; const data = { yritys: document.getElementById('lead-form-yritys').value, yhteyshenkilo: document.getElementById('lead-form-yhteyshenkilo').value, puhelin: document.getElementById('lead-form-puhelin').value, sahkoposti: document.getElementById('lead-form-sahkoposti').value, osoite: document.getElementById('lead-form-osoite').value, kaupunki: document.getElementById('lead-form-kaupunki').value, tila: document.getElementById('lead-form-tila').value, muistiinpanot: document.getElementById('lead-form-muistiinpanot').value, }; if (id) { data.id = id; await apiCall('lead_update', 'POST', data); } else { await apiCall('lead_create', 'POST', data); } leadModal.style.display = 'none'; await loadLeads(); }); // Lead detail actions document.getElementById('lead-detail-close').addEventListener('click', () => leadDetailModal.style.display = 'none'); document.getElementById('lead-detail-cancel').addEventListener('click', () => leadDetailModal.style.display = 'none'); document.getElementById('lead-detail-edit').addEventListener('click', () => editLead(currentLeadId)); document.getElementById('lead-detail-delete').addEventListener('click', () => { const l = leads.find(x => x.id === currentLeadId); if (l) deleteLead(currentLeadId, l.yritys); }); document.getElementById('lead-detail-convert').addEventListener('click', () => convertLeadToCustomer(currentLeadId)); // ==================== ARCHIVE ==================== async function loadArchive() { try { const archive = await apiCall('archived_customers'); const atbody = document.getElementById('archive-tbody'); const noArc = document.getElementById('no-archive'); if (archive.length === 0) { atbody.innerHTML = ''; noArc.style.display = 'block'; document.getElementById('archive-table').style.display = 'none'; } else { noArc.style.display = 'none'; document.getElementById('archive-table').style.display = 'table'; atbody.innerHTML = archive.map(c => ` ${esc(c.yritys)} ${(c.liittymat || []).length} ${esc(c.arkistoitu || '')} ${esc(c.arkistoija || '')} ${currentUser.role === 'admin' ? `` : ''} `).join(''); } } catch (e) { console.error(e); } } async function restoreCustomer(id) { if (!confirm('Palautetaanko asiakas arkistosta?')) return; await apiCall('customer_restore', 'POST', { id }); loadArchive(); loadCustomers(); } async function permanentDelete(id, name) { if (!confirm(`Poistetaanko "${name}" PYSYVÄSTI?\n\nTätä ei voi perua!`)) return; await apiCall('customer_permanent_delete', 'POST', { id }); loadArchive(); } // ==================== CHANGELOG ==================== const actionLabels = { customer_create: 'Lisäsi asiakkaan', customer_update: 'Muokkasi asiakasta', customer_archive: 'Arkistoi asiakkaan', customer_restore: 'Palautti asiakkaan', customer_permanent_delete: 'Poisti pysyvästi', user_create: 'Lisäsi käyttäjän', user_update: 'Muokkasi käyttäjää', user_delete: 'Poisti käyttäjän', lead_create: 'Lisäsi liidin', lead_update: 'Muokkasi liidiä', lead_delete: 'Poisti liidin', lead_to_customer: 'Muutti liidin asiakkaaksi', config_update: 'Päivitti asetukset', ticket_fetch: 'Haki sähköpostit', ticket_reply: 'Vastasi tikettiin', ticket_status: 'Muutti tiketin tilaa', ticket_assign: 'Osoitti tiketin', ticket_note: 'Lisäsi muistiinpanon', ticket_delete: 'Poisti tiketin', ticket_customer: 'Linkitti tiketin asiakkaaseen', ticket_type: 'Muutti tiketin tyyppiä', }; async function loadChangelog() { try { const log = await apiCall('changelog&limit=200'); const ctbody = document.getElementById('changelog-tbody'); const noLog = document.getElementById('no-changelog'); if (log.length === 0) { ctbody.innerHTML = ''; noLog.style.display = 'block'; document.getElementById('changelog-table').style.display = 'none'; } else { noLog.style.display = 'none'; document.getElementById('changelog-table').style.display = 'table'; ctbody.innerHTML = log.map(e => ` ${esc(e.timestamp)} ${esc(e.user)} ${actionLabels[e.action] || esc(e.action)} ${esc(e.customer_name)} ${esc(e.details)} `).join(''); } } catch (e) { console.error(e); } } // ==================== USERS ==================== async function loadUsers() { try { const users = await apiCall('users'); const utbody = document.getElementById('users-tbody'); utbody.innerHTML = users.map(u => ` ${esc(u.username)} ${esc(u.nimi)} ${esc(u.email || '')} ${u.role === 'superadmin' ? 'Pääkäyttäjä' : (u.role === 'admin' ? 'Yritysadmin' : 'Käyttäjä')} ${esc(u.luotu)} ${u.id !== '${currentUser.id}' ? `` : ''} `).join(''); } catch (e) { console.error(e); } } let usersCache = []; document.getElementById('btn-add-user').addEventListener('click', () => openUserForm()); document.getElementById('user-modal-close').addEventListener('click', () => userModal.style.display = 'none'); document.getElementById('user-form-cancel').addEventListener('click', () => userModal.style.display = 'none'); function openUserForm(user = null) { document.getElementById('user-modal-title').textContent = user ? 'Muokkaa käyttäjää' : 'Lisää käyttäjä'; document.getElementById('user-form-id').value = user ? user.id : ''; document.getElementById('user-form-username').value = user ? user.username : ''; document.getElementById('user-form-username').disabled = !!user; document.getElementById('user-form-nimi').value = user ? user.nimi : ''; document.getElementById('user-form-email').value = user ? (user.email || '') : ''; document.getElementById('user-form-password').value = ''; document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*'; document.getElementById('user-form-role').value = user ? user.role : 'user'; // Yrityscheckboxit const allComps = availableCompanies.length > 0 ? availableCompanies : []; const userComps = user ? (user.companies || []) : []; const container = document.getElementById('user-company-checkboxes'); // Hae kaikki yritykset admin-näkymää varten apiCall('companies_all').then(companies => { container.innerHTML = companies.map(c => `` ).join(''); }).catch(() => { container.innerHTML = allComps.map(c => `` ).join(''); }); // Allekirjoitukset per postilaatikko const sigSection = document.getElementById('user-signatures-section'); const sigList = document.getElementById('user-signatures-list'); const userSigs = user ? (user.signatures || {}) : {}; apiCall('all_mailboxes').then(mailboxes => { if (mailboxes.length === 0) { sigSection.style.display = 'none'; return; } sigSection.style.display = ''; sigList.innerHTML = mailboxes.map(mb => `
` ).join(''); }).catch(() => { sigSection.style.display = 'none'; }); userModal.style.display = 'flex'; } async function editUser(id) { try { const users = await apiCall('users'); const u = users.find(x => x.id === id); if (u) openUserForm(u); } catch (e) { alert(e.message); } } async function deleteUser(id, username) { if (!confirm(`Poistetaanko käyttäjä "${username}"?`)) return; try { await apiCall('user_delete', 'POST', { id }); loadUsers(); } catch (e) { alert(e.message); } } document.getElementById('user-form').addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('user-form-id').value; const companies = [...document.querySelectorAll('.user-company-cb:checked')].map(cb => cb.value); // Kerää allekirjoitukset const signatures = {}; document.querySelectorAll('.sig-textarea').forEach(ta => { const mbId = ta.dataset.mailboxId; const val = ta.value.trim(); if (val) signatures[mbId] = val; }); const data = { username: document.getElementById('user-form-username').value, nimi: document.getElementById('user-form-nimi').value, email: document.getElementById('user-form-email').value, role: document.getElementById('user-form-role').value, companies, signatures, }; const pw = document.getElementById('user-form-password').value; if (pw) data.password = pw; else if (!id) { alert('Salasana vaaditaan uudelle käyttäjälle'); return; } try { if (id) { data.id = id; await apiCall('user_update', 'POST', data); } else { await apiCall('user_create', 'POST', data); } userModal.style.display = 'none'; loadUsers(); // Päivitä omat allekirjoitukset (check_auth palauttaa tuoreet) const auth = await apiCall('check_auth'); if (auth.authenticated) { currentUser = { username: auth.username, nimi: auth.nimi, role: auth.role, id: auth.user_id }; currentUserSignatures = auth.signatures || {}; } } catch (e) { alert(e.message); } }); // ==================== TICKETS (ASIAKASPALVELU) ==================== let tickets = []; let currentTicketId = null; let ticketReplyType = 'reply'; const ticketStatusLabels = { uusi: 'Uusi', kasittelyssa: 'Käsittelyssä', odottaa: 'Odottaa vastausta', ratkaistu: 'Ratkaistu', suljettu: 'Suljettu', }; const ticketTypeLabels = { laskutus: 'Laskutus', tekniikka: 'Tekniikka', vika: 'Vika', muu: 'Muu', }; async function loadTickets() { try { // Hae kaikkien yritysten tiketit jos useampi yritys const allParam = availableCompanies.length > 1 ? '&all=1' : ''; tickets = await apiCall('tickets' + allParam); renderTickets(); } catch (e) { console.error(e); } } function renderTickets() { const query = document.getElementById('ticket-search-input').value.toLowerCase().trim(); const statusFilter = document.getElementById('ticket-status-filter').value; const typeFilter = document.getElementById('ticket-type-filter').value; const showClosed = document.getElementById('ticket-show-closed').checked; let filtered = tickets; // Suljetut näkyvät vain kun täppä on päällä if (showClosed) { filtered = filtered.filter(t => t.status === 'suljettu'); } else { filtered = filtered.filter(t => t.status !== 'suljettu'); if (statusFilter) { filtered = filtered.filter(t => t.status === statusFilter); } } if (typeFilter) { filtered = filtered.filter(t => (t.type || 'muu') === typeFilter); } // Tag filter const tagFilter = (document.getElementById('ticket-tag-filter').value || '').trim().toLowerCase().replace(/^#/, ''); if (tagFilter) { filtered = filtered.filter(t => (t.tags || []).some(tag => tag.toLowerCase().includes(tagFilter))); } if (query) { filtered = filtered.filter(t => (t.subject || '').toLowerCase().includes(query) || (t.from_name || '').toLowerCase().includes(query) || (t.from_email || '').toLowerCase().includes(query) || (t.tags || []).some(tag => tag.toLowerCase().includes(query)) ); } // Sorttaus: prioriteetti → tila → päivämäärä const ticketSortField = document.getElementById('ticket-sort')?.value || 'status'; const statusPriority = { kasittelyssa: 0, uusi: 1, odottaa: 2, ratkaistu: 3, suljettu: 4 }; const priorityOrder = { urgent: 0, 'tärkeä': 1, normaali: 2 }; filtered.sort((a, b) => { // Urgent/tärkeä aina ensin const prioA = priorityOrder[a.priority || 'normaali'] ?? 2; const prioB = priorityOrder[b.priority || 'normaali'] ?? 2; if (prioA !== prioB) return prioA - prioB; if (ticketSortField === 'status') { const pa = statusPriority[a.status] ?? 9; const pb = statusPriority[b.status] ?? 9; if (pa !== pb) return pa - pb; return (b.updated || '').localeCompare(a.updated || ''); } else if (ticketSortField === 'updated') { return (b.updated || '').localeCompare(a.updated || ''); } else if (ticketSortField === 'created') { return (b.created || '').localeCompare(a.created || ''); } return 0; }); const ttbody = document.getElementById('tickets-tbody'); const noTickets = document.getElementById('no-tickets'); if (filtered.length === 0) { ttbody.innerHTML = ''; noTickets.style.display = 'block'; document.getElementById('tickets-table').style.display = 'none'; } else { noTickets.style.display = 'none'; document.getElementById('tickets-table').style.display = 'table'; const multiCompany = availableCompanies.length > 1; ttbody.innerHTML = filtered.map(t => { const lastType = t.last_message_type === 'reply_out' ? '→' : (t.last_message_type === 'note' ? '📝' : '←'); const typeLabel = ticketTypeLabels[t.type] || 'Muu'; const rowClass = t.priority === 'urgent' ? 'ticket-row-urgent' : (t.priority === 'tärkeä' ? 'ticket-row-important' : (t.status === 'kasittelyssa' ? 'ticket-row-active' : '')); const checked = bulkSelectedIds.has(t.id) ? 'checked' : ''; const companyBadge = multiCompany && t.company_name ? `${esc(t.company_name)} ` : ''; const prioBadge = t.priority === 'urgent' ? '🚨 ' : (t.priority === 'tärkeä' ? '⚠️ ' : ''); return ` ${ticketStatusLabels[t.status] || t.status} ${typeLabel} ${prioBadge}${companyBadge}${esc(t.subject)} ${esc(t.mailbox_name || t.from_name || t.from_email)} ${t.customer_name ? esc(t.customer_name) : '-'} ${lastType} ${t.message_count} ${timeAgo(t.updated)} `; }).join(''); // Re-attach checkbox listeners document.querySelectorAll('.ticket-checkbox').forEach(cb => { cb.addEventListener('change', function() { if (this.checked) bulkSelectedIds.add(this.dataset.ticketId); else bulkSelectedIds.delete(this.dataset.ticketId); updateBulkToolbar(); }); }); } const openCount = tickets.filter(t => t.status !== 'suljettu').length; document.getElementById('ticket-count').textContent = `${openCount} avointa tikettiä (${tickets.length} yht.)`; // Status summary const counts = {}; tickets.forEach(t => { counts[t.status] = (counts[t.status] || 0) + 1; }); const parts = []; if (counts.uusi) parts.push(`${counts.uusi} uutta`); if (counts.kasittelyssa) parts.push(`${counts.kasittelyssa} käsittelyssä`); if (counts.odottaa) parts.push(`${counts.odottaa} odottaa`); document.getElementById('ticket-status-summary').textContent = parts.join(' · '); } document.getElementById('ticket-search-input').addEventListener('input', () => renderTickets()); document.getElementById('ticket-status-filter').addEventListener('change', () => renderTickets()); document.getElementById('ticket-type-filter').addEventListener('change', () => renderTickets()); document.getElementById('ticket-tag-filter').addEventListener('input', () => renderTickets()); document.getElementById('ticket-sort').addEventListener('change', () => renderTickets()); document.getElementById('ticket-show-closed').addEventListener('change', () => renderTickets()); document.getElementById('bulk-select-all').addEventListener('change', function() { const checkboxes = document.querySelectorAll('.ticket-checkbox'); checkboxes.forEach(cb => { cb.checked = this.checked; if (this.checked) bulkSelectedIds.add(cb.dataset.ticketId); else bulkSelectedIds.delete(cb.dataset.ticketId); }); updateBulkToolbar(); }); document.getElementById('tickets-tbody').addEventListener('click', (e) => { const row = e.target.closest('tr'); if (row && row.dataset.ticketId) showTicketDetail(row.dataset.ticketId, row.dataset.companyId || ''); }); // Helper: lisää company_id query parametri tiketti-kutsuihin function ticketCompanyParam() { return currentTicketCompanyId ? '&company_id=' + encodeURIComponent(currentTicketCompanyId) : ''; } async function showTicketDetail(id, companyId = '') { try { currentTicketCompanyId = companyId; const ticket = await apiCall('ticket_detail&id=' + encodeURIComponent(id) + ticketCompanyParam()); currentTicketId = id; // Header document.getElementById('ticket-detail-header').innerHTML = `

${esc(ticket.subject)}

${esc(ticket.from_name)} <${esc(ticket.from_email)}> · Luotu ${esc(ticket.created)}
Tagit:
${(ticket.tags || []).map(tag => '#' + esc(tag) + ' ').join('')}
${ticket.auto_close_at ? '⏰ Auto-close: ' + esc(ticket.auto_close_at.substring(0, 10)) + '' : ''}
${ticket.cc ? '
CC: ' + esc(ticket.cc) + '
' : ''}`; // Load users for assignment dropdown try { const users = await apiCall('users'); const assignSelect = document.getElementById('ticket-assign-select'); users.forEach(u => { const opt = document.createElement('option'); opt.value = u.username; opt.textContent = u.nimi || u.username; if (u.username === ticket.assigned_to) opt.selected = true; assignSelect.appendChild(opt); }); } catch (e) { /* non-admin may not access users */ } // Type change handler document.getElementById('ticket-type-select').addEventListener('change', async function() { try { await apiCall('ticket_type' + ticketCompanyParam(), 'POST', { id: currentTicketId, type: this.value }); } catch (e) { alert(e.message); } }); // Status change handler document.getElementById('ticket-status-select').addEventListener('change', async function() { try { await apiCall('ticket_status' + ticketCompanyParam(), 'POST', { id: currentTicketId, status: this.value }); } catch (e) { alert(e.message); } }); // Assign handler document.getElementById('ticket-assign-select').addEventListener('change', async function() { try { await apiCall('ticket_assign' + ticketCompanyParam(), 'POST', { id: currentTicketId, assigned_to: this.value }); } catch (e) { alert(e.message); } }); // Customer link — load customers dropdown try { const custSelect = document.getElementById('ticket-customer-select'); customers.forEach(c => { const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.yritys; if (c.id === ticket.customer_id) opt.selected = true; custSelect.appendChild(opt); }); } catch (e) {} document.getElementById('ticket-customer-select').addEventListener('change', async function() { const selOpt = this.options[this.selectedIndex]; const custName = this.value ? selOpt.textContent : ''; try { await apiCall('ticket_customer' + ticketCompanyParam(), 'POST', { id: currentTicketId, customer_id: this.value, customer_name: custName }); } catch (e) { alert(e.message); } }); // Priority handler document.getElementById('ticket-priority-select').addEventListener('change', async function() { try { await apiCall('ticket_priority' + ticketCompanyParam(), 'POST', { id: currentTicketId, priority: this.value }); // Päivitä näkymä (visuaalinen muutos) await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e) { alert(e.message); } }); // Delete handler document.getElementById('btn-ticket-delete').addEventListener('click', async () => { if (!confirm('Poistetaanko tiketti "' + ticket.subject + '"?')) return; try { await apiCall('ticket_delete' + ticketCompanyParam(), 'POST', { id: currentTicketId }); showTicketListView(); loadTickets(); } catch (e) { alert(e.message); } }); // Tags: add new tag on Enter document.getElementById('ticket-tag-input').addEventListener('keydown', async (e) => { if (e.key !== 'Enter') return; e.preventDefault(); const input = e.target; const newTag = input.value.trim().toLowerCase().replace(/^#/, ''); if (!newTag) return; const currentTags = (ticket.tags || []).slice(); if (!currentTags.includes(newTag)) currentTags.push(newTag); input.value = ''; try { await apiCall('ticket_tags' + ticketCompanyParam(), 'POST', { id: currentTicketId, tags: currentTags }); await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e2) { alert(e2.message); } }); // Tags: remove tag document.querySelectorAll('.ticket-tag-remove').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const tagEl = btn.closest('.ticket-tag-editable'); const tagToRemove = tagEl.dataset.tag; const currentTags = (ticket.tags || []).filter(t => t !== tagToRemove); try { await apiCall('ticket_tags' + ticketCompanyParam(), 'POST', { id: currentTicketId, tags: currentTags }); await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e2) { alert(e2.message); } }); }); // Thread messages const thread = document.getElementById('ticket-thread'); thread.innerHTML = (ticket.messages || []).map(m => { const isOut = m.type === 'reply_out'; const isNote = m.type === 'note'; const typeClass = isOut ? 'ticket-msg-out' : (isNote ? 'ticket-msg-note' : 'ticket-msg-in'); const typeIcon = isOut ? '→ Vastaus' : (isNote ? '📝 Muistiinpano' : '← Saapunut'); return `
${typeIcon} ${esc(m.from_name || m.from)} ${esc(m.timestamp)}
${esc(m.body)}
`; }).join(''); // Show detail, hide list document.getElementById('ticket-list-view').style.display = 'none'; document.getElementById('ticket-detail-view').style.display = 'block'; // Reset reply form document.getElementById('ticket-reply-body').value = ''; document.getElementById('ticket-reply-body').placeholder = 'Kirjoita vastaus...'; ticketReplyType = 'reply'; document.querySelectorAll('.btn-reply-tab').forEach(b => b.classList.remove('active')); document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active'); document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus'; // CC-kenttä — täytetään tiketin CC:stä const ccField = document.getElementById('reply-cc'); if (ccField) ccField.value = ticket.cc || ''; // Mailbox-valinta — täytetään yrityksen postilaatikoista const mbSelect = document.getElementById('reply-mailbox-select'); if (mbSelect) { try { const mailboxes = await apiCall('all_mailboxes'); mbSelect.innerHTML = mailboxes.map(mb => `` ).join(''); // Vaihda allekirjoitusta kun mailbox vaihtuu mbSelect.addEventListener('change', function() { updateSignaturePreview(this.value); }); } catch (e) { mbSelect.innerHTML = ''; } } // Allekirjoituksen esikatselu function updateSignaturePreview(mbId) { const sigPreview = document.getElementById('signature-preview'); const sig = currentUserSignatures[mbId] || ''; if (sig) { sigPreview.textContent = '-- \n' + sig; sigPreview.style.display = 'block'; } else { sigPreview.style.display = 'none'; } } updateSignaturePreview(ticket.mailbox_id || ''); // Vastauspohjat — lataa dropdown try { const templates = await apiCall('reply_templates'); const tplSelect = document.getElementById('reply-template-select'); tplSelect.innerHTML = ''; templates.forEach(t => { tplSelect.innerHTML += ``; }); tplSelect.addEventListener('change', function() { const opt = this.options[this.selectedIndex]; const body = opt.dataset.body || ''; if (body) { const textarea = document.getElementById('ticket-reply-body'); textarea.value = textarea.value ? textarea.value + '\n\n' + body : body; textarea.focus(); } this.value = ''; // Reset select }); } catch (e) { /* templates not critical */ } } catch (e) { alert(e.message); } } function showTicketListView() { document.getElementById('ticket-detail-view').style.display = 'none'; document.getElementById('ticket-rules-view').style.display = 'none'; document.getElementById('ticket-list-view').style.display = 'block'; currentTicketId = null; // Reset bulk selection bulkSelectedIds.clear(); const selectAll = document.getElementById('bulk-select-all'); if (selectAll) selectAll.checked = false; updateBulkToolbar(); } document.getElementById('btn-ticket-back').addEventListener('click', () => { showTicketListView(); loadTickets(); }); // Reply type tabs document.querySelectorAll('.btn-reply-tab').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-reply-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); ticketReplyType = btn.dataset.replyType; const textarea = document.getElementById('ticket-reply-body'); const sendBtn = document.getElementById('btn-send-reply'); const sigPrev = document.getElementById('signature-preview'); const metaFields = document.getElementById('reply-meta-fields'); const tplWrap = document.getElementById('reply-template-select-wrap'); if (ticketReplyType === 'note') { textarea.placeholder = 'Kirjoita sisäinen muistiinpano...'; sendBtn.textContent = 'Tallenna muistiinpano'; sigPrev.style.display = 'none'; if (metaFields) metaFields.style.display = 'none'; if (tplWrap) tplWrap.style.display = 'none'; } else { textarea.placeholder = 'Kirjoita vastaus...'; sendBtn.textContent = 'Lähetä vastaus'; if (metaFields) metaFields.style.display = ''; if (tplWrap) tplWrap.style.display = ''; // Näytä allekirjoitus jos on asetettu if (sigPrev.textContent.trim()) sigPrev.style.display = 'block'; } }); }); // Send reply or note document.getElementById('btn-send-reply').addEventListener('click', async () => { const body = document.getElementById('ticket-reply-body').value.trim(); if (!body) { alert('Kirjoita viesti ensin'); return; } if (!currentTicketId) return; const btn = document.getElementById('btn-send-reply'); btn.disabled = true; btn.textContent = 'Lähetetään...'; try { const action = ticketReplyType === 'note' ? 'ticket_note' : 'ticket_reply'; const payload = { id: currentTicketId, body }; if (ticketReplyType !== 'note') { const mbSel = document.getElementById('reply-mailbox-select'); const ccFld = document.getElementById('reply-cc'); if (mbSel) payload.mailbox_id = mbSel.value; if (ccFld) payload.cc = ccFld.value.trim(); } await apiCall(action + ticketCompanyParam(), 'POST', payload); // Reload the detail view await showTicketDetail(currentTicketId, currentTicketCompanyId); } catch (e) { alert(e.message); } finally { btn.disabled = false; btn.textContent = ticketReplyType === 'note' ? 'Tallenna muistiinpano' : 'Lähetä vastaus'; } }); // Fetch emails document.getElementById('btn-fetch-emails').addEventListener('click', async () => { const btn = document.getElementById('btn-fetch-emails'); const status = document.getElementById('ticket-fetch-status'); btn.disabled = true; btn.textContent = '⏳ Haetaan...'; status.style.display = 'block'; status.className = ''; status.style.background = '#f0f7ff'; status.style.color = '#0f3460'; status.textContent = 'Yhdistetään sähköpostipalvelimeen...'; try { const result = await apiCall('ticket_fetch', 'POST'); status.style.background = '#eafaf1'; status.style.color = '#27ae60'; status.textContent = `Valmis! ${result.new_tickets} uutta tikettiä, ${result.threaded} ketjutettu viestiä. Yhteensä ${result.total} tikettiä.`; await loadTickets(); } catch (e) { status.style.background = '#fef2f2'; status.style.color = '#e74c3c'; status.textContent = 'Virhe: ' + e.message; } finally { btn.disabled = false; btn.textContent = '📧 Hae postit'; setTimeout(() => { status.style.display = 'none'; }, 8000); } }); // ==================== TICKET AUTO-REFRESH ==================== let ticketAutoRefreshTimer = null; function startTicketAutoRefresh() { stopTicketAutoRefresh(); const seconds = parseInt(document.getElementById('ticket-refresh-interval').value) || 60; ticketAutoRefreshTimer = setInterval(() => { // Vain jos support-tabi on aktiivinen ja listanäkymä näkyy const supportActive = document.getElementById('tab-content-support').classList.contains('active'); const listVisible = document.getElementById('ticket-list-view').style.display !== 'none'; if (supportActive && listVisible) { loadTickets(); } }, seconds * 1000); } function stopTicketAutoRefresh() { if (ticketAutoRefreshTimer) { clearInterval(ticketAutoRefreshTimer); ticketAutoRefreshTimer = null; } } document.getElementById('ticket-auto-refresh').addEventListener('change', function() { if (this.checked) { startTicketAutoRefresh(); } else { stopTicketAutoRefresh(); } }); document.getElementById('ticket-refresh-interval').addEventListener('change', function() { if (document.getElementById('ticket-auto-refresh').checked) { startTicketAutoRefresh(); // Käynnistä uudelleen uudella intervallilla } }); // ==================== TICKET RULES (AUTOMAATTISÄÄNNÖT) ==================== let ticketRules = []; let editingRuleId = null; async function loadRules() { try { ticketRules = await apiCall('ticket_rules'); renderRules(); } catch (e) { console.error(e); } } function renderRules() { const list = document.getElementById('rules-list'); if (ticketRules.length === 0) { list.innerHTML = '
Ei sääntöjä vielä. Lisää ensimmäinen sääntö.
'; return; } list.innerHTML = ticketRules.map(r => { const conditions = []; if (r.from_contains) conditions.push('Lähettäjä: ' + esc(r.from_contains) + ''); if (r.subject_contains) conditions.push('Otsikko: ' + esc(r.subject_contains) + ''); const actions = []; if (r.set_status) actions.push('Tila → ' + (ticketStatusLabels[r.set_status] || r.set_status)); if (r.set_type) actions.push('Tyyppi → ' + (ticketTypeLabels[r.set_type] || r.set_type)); if (r.set_tags) actions.push('Tagit: #' + r.set_tags.split(',').map(t => t.trim()).join(' #')); if (r.auto_close_days) actions.push('Auto-close: ' + r.auto_close_days + 'pv'); return `
${esc(r.name)}
${conditions.length ? 'Ehdot: ' + conditions.join(', ') : 'Ei ehtoja'} → ${actions.length ? actions.join(', ') : 'Ei toimenpiteitä'}
`; }).join(''); } function showRulesView() { document.getElementById('ticket-list-view').style.display = 'none'; document.getElementById('ticket-detail-view').style.display = 'none'; document.getElementById('ticket-rules-view').style.display = 'block'; loadRules(); } function hideRulesView() { document.getElementById('ticket-rules-view').style.display = 'none'; document.getElementById('ticket-list-view').style.display = 'block'; } function showRuleForm(rule) { document.getElementById('rule-form-container').style.display = ''; document.getElementById('rule-form-title').textContent = rule ? 'Muokkaa sääntöä' : 'Uusi sääntö'; document.getElementById('rule-form-id').value = rule ? rule.id : ''; document.getElementById('rule-form-name').value = rule ? rule.name : ''; document.getElementById('rule-form-from').value = rule ? rule.from_contains : ''; document.getElementById('rule-form-subject').value = rule ? rule.subject_contains : ''; document.getElementById('rule-form-status').value = rule ? (rule.set_status || '') : ''; document.getElementById('rule-form-type').value = rule ? (rule.set_type || '') : ''; document.getElementById('rule-form-tags').value = rule ? (rule.set_tags || '') : ''; document.getElementById('rule-form-autoclose').value = rule ? (rule.auto_close_days || '') : ''; editingRuleId = rule ? rule.id : null; } function hideRuleForm() { document.getElementById('rule-form-container').style.display = 'none'; editingRuleId = null; } document.getElementById('btn-ticket-rules').addEventListener('click', () => showRulesView()); document.getElementById('btn-rules-back').addEventListener('click', () => hideRulesView()); document.getElementById('btn-add-rule').addEventListener('click', () => showRuleForm(null)); document.getElementById('btn-cancel-rule').addEventListener('click', () => hideRuleForm()); document.getElementById('btn-save-rule').addEventListener('click', async () => { const name = document.getElementById('rule-form-name').value.trim(); if (!name) { alert('Nimi puuttuu'); return; } const data = { name, from_contains: document.getElementById('rule-form-from').value.trim(), subject_contains: document.getElementById('rule-form-subject').value.trim(), set_status: document.getElementById('rule-form-status').value, set_type: document.getElementById('rule-form-type').value, set_tags: document.getElementById('rule-form-tags').value.trim(), auto_close_days: parseInt(document.getElementById('rule-form-autoclose').value) || 0, enabled: true, }; const existingId = document.getElementById('rule-form-id').value; if (existingId) data.id = existingId; try { await apiCall('ticket_rule_save', 'POST', data); hideRuleForm(); await loadRules(); } catch (e) { alert(e.message); } }); async function editRule(id) { const rule = ticketRules.find(r => r.id === id); if (rule) showRuleForm(rule); } async function deleteRule(id) { if (!confirm('Poistetaanko sääntö?')) return; try { await apiCall('ticket_rule_delete', 'POST', { id }); await loadRules(); } catch (e) { alert(e.message); } } async function toggleRule(id, enabled) { const rule = ticketRules.find(r => r.id === id); if (!rule) return; try { await apiCall('ticket_rule_save', 'POST', { ...rule, enabled }); await loadRules(); } catch (e) { alert(e.message); } } // ==================== BULK ACTIONS ==================== let bulkSelectedIds = new Set(); function updateBulkToolbar() { const toolbar = document.getElementById('bulk-actions-toolbar'); if (bulkSelectedIds.size > 0) { toolbar.style.display = 'flex'; document.getElementById('bulk-count').textContent = bulkSelectedIds.size + ' valittu'; } else { toolbar.style.display = 'none'; } } async function bulkCloseSelected() { if (bulkSelectedIds.size === 0) return; if (!confirm(`Suljetaanko ${bulkSelectedIds.size} tikettiä?`)) return; try { await apiCall('ticket_bulk_status', 'POST', { ids: [...bulkSelectedIds], status: 'suljettu' }); bulkSelectedIds.clear(); updateBulkToolbar(); await loadTickets(); } catch (e) { alert(e.message); } } async function bulkDeleteSelected() { if (bulkSelectedIds.size === 0) return; if (!confirm(`Poistetaanko ${bulkSelectedIds.size} tikettiä pysyvästi?`)) return; try { await apiCall('ticket_bulk_delete', 'POST', { ids: [...bulkSelectedIds] }); bulkSelectedIds.clear(); updateBulkToolbar(); await loadTickets(); } catch (e) { alert(e.message); } } // ==================== SETTINGS ==================== async function loadSettings() { try { const config = await apiCall('config'); document.getElementById('settings-api-key').value = config.api_key || ''; document.getElementById('settings-cors').value = (config.cors_origins || []).join('\n'); // Näytä yrityksen nimi API-otsikossa const apiTitle = document.getElementById('api-company-name'); if (apiTitle && currentCompany) apiTitle.textContent = currentCompany.nimi + ' — '; const key = config.api_key || 'AVAIN'; document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`; // Telegram-asetukset document.getElementById('settings-telegram-token').value = config.telegram_bot_token || ''; document.getElementById('settings-telegram-chat').value = config.telegram_chat_id || ''; } catch (e) { console.error(e); } // Vastauspohjat loadTemplates(); } // ==================== VASTAUSPOHJAT ==================== let replyTemplates = []; async function loadTemplates() { try { replyTemplates = await apiCall('reply_templates'); renderTemplates(); } catch (e) { console.error(e); } } function renderTemplates() { const list = document.getElementById('templates-list'); if (!list) return; if (replyTemplates.length === 0) { list.innerHTML = '

Ei vastauspohjia vielä.

'; return; } list.innerHTML = replyTemplates.map(t => `
${esc(t.nimi)}
${esc(t.body.substring(0, 80))}
` ).join(''); } document.getElementById('btn-add-template').addEventListener('click', () => { document.getElementById('template-edit-id').value = ''; document.getElementById('template-edit-name').value = ''; document.getElementById('template-edit-body').value = ''; document.getElementById('template-form').style.display = 'block'; }); document.getElementById('btn-cancel-template').addEventListener('click', () => { document.getElementById('template-form').style.display = 'none'; }); document.getElementById('btn-save-template').addEventListener('click', async () => { const id = document.getElementById('template-edit-id').value || undefined; const nimi = document.getElementById('template-edit-name').value.trim(); const body = document.getElementById('template-edit-body').value.trim(); if (!nimi || !body) { alert('Täytä nimi ja sisältö'); return; } try { await apiCall('reply_template_save', 'POST', { id, nimi, body }); document.getElementById('template-form').style.display = 'none'; loadTemplates(); } catch (e) { alert(e.message); } }); window.editTemplate = function(id) { const t = replyTemplates.find(x => x.id === id); if (!t) return; document.getElementById('template-edit-id').value = t.id; document.getElementById('template-edit-name').value = t.nimi; document.getElementById('template-edit-body').value = t.body; document.getElementById('template-form').style.display = 'block'; }; window.deleteTemplate = async function(id) { if (!confirm('Poistetaanko vastauspohja?')) return; try { await apiCall('reply_template_delete', 'POST', { id }); loadTemplates(); } catch (e) { alert(e.message); } }; // ==================== TELEGRAM ==================== document.getElementById('btn-save-telegram').addEventListener('click', async () => { try { await apiCall('config_update', 'POST', { telegram_bot_token: document.getElementById('settings-telegram-token').value.trim(), telegram_chat_id: document.getElementById('settings-telegram-chat').value.trim(), }); alert('Telegram-asetukset tallennettu!'); } catch (e) { alert(e.message); } }); document.getElementById('btn-test-telegram').addEventListener('click', async () => { try { await apiCall('telegram_test', 'POST'); alert('Testiviesti lähetetty!'); } catch (e) { alert(e.message); } }); document.getElementById('btn-generate-key').addEventListener('click', async () => { try { const config = await apiCall('generate_api_key', 'POST'); document.getElementById('settings-api-key').value = config.api_key || ''; document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${config.api_key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`; } catch (e) { alert(e.message); } }); document.getElementById('btn-save-settings').addEventListener('click', async () => { try { const config = await apiCall('config_update', 'POST', { api_key: document.getElementById('settings-api-key').value, cors_origins: document.getElementById('settings-cors').value, }); alert('Asetukset tallennettu!'); } catch (e) { alert(e.message); } }); document.getElementById('btn-test-api').addEventListener('click', async () => { const osoite = document.getElementById('test-api-address').value.trim(); const postinumero = document.getElementById('test-api-zip').value.trim(); const kaupunki = document.getElementById('test-api-city').value.trim(); const apiKey = document.getElementById('settings-api-key').value; if (!osoite || !postinumero || !kaupunki) { alert('Täytä osoite, postinumero ja kaupunki'); return; } const result = document.getElementById('test-api-result'); result.style.display = 'block'; result.textContent = 'Haetaan...'; try { const params = `osoite=${encodeURIComponent(osoite)}&postinumero=${encodeURIComponent(postinumero)}&kaupunki=${encodeURIComponent(kaupunki)}`; const res = await fetch(`${API}?action=saatavuus&key=${encodeURIComponent(apiKey)}&${params}`); const data = await res.json(); result.textContent = JSON.stringify(data, null, 2); } catch (e) { result.textContent = 'Virhe: ' + e.message; } }); // ==================== MODALS ==================== customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; }); detailModal.addEventListener('click', (e) => { if (e.target === detailModal) detailModal.style.display = 'none'; }); userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.style.display = 'none'; }); leadModal.addEventListener('click', (e) => { if (e.target === leadModal) leadModal.style.display = 'none'; }); leadDetailModal.addEventListener('click', (e) => { if (e.target === leadDetailModal) leadDetailModal.style.display = 'none'; }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { customerModal.style.display = 'none'; detailModal.style.display = 'none'; userModal.style.display = 'none'; leadModal.style.display = 'none'; leadDetailModal.style.display = 'none'; } }); // ==================== COMPANY SELECTOR ==================== document.getElementById('company-selector').addEventListener('change', function () { switchCompany(this.value); }); // ==================== YRITYKSET-TAB (admin) ==================== let companiesTabData = []; let currentCompanyDetail = null; async function loadCompaniesTab() { try { companiesTabData = await apiCall('companies_all'); renderCompaniesTable(); } catch (e) { console.error(e); // Fallback: käytä availableCompanies companiesTabData = availableCompanies; renderCompaniesTable(); } } function renderCompaniesTable() { const tbody = document.getElementById('companies-tbody'); const superAdmin = currentUser?.role === 'superadmin'; tbody.innerHTML = companiesTabData.map(c => ` ${esc(c.id)} ${esc(c.nimi)} - ${esc((c.luotu || '').substring(0, 10))} ${c.aktiivinen !== false ? 'Aktiivinen' : 'Ei aktiivinen'} ${superAdmin ? `` : ''} `).join(''); // Piilota "Lisää yritys" nappi jos ei superadmin const addBtn = document.getElementById('btn-add-company'); if (addBtn) addBtn.style.display = superAdmin ? '' : 'none'; document.getElementById('companies-list-view').style.display = ''; document.getElementById('company-detail-view').style.display = 'none'; } document.getElementById('btn-add-company').addEventListener('click', () => { const nimi = prompt('Yrityksen nimi:'); if (!nimi) return; const id = prompt('Yrityksen ID (pienillä kirjaimilla, a-z, 0-9, viiva sallittu):'); if (!id) return; apiCall('company_create', 'POST', { id, nimi }).then(() => { loadCompaniesTab(); // Päivitä myös company-selector apiCall('check_auth').then(data => { if (data.authenticated) { availableCompanies = data.companies || []; currentCompany = availableCompanies.find(c => c.id === data.company_id) || currentCompany; populateCompanySelector(); } }); }).catch(e => alert(e.message)); }); async function deleteCompany(id, nimi) { if (!confirm(`Poistetaanko yritys "${nimi}"? Tämä poistaa pääsyn yrityksen dataan.`)) return; try { await apiCall('company_delete', 'POST', { id }); loadCompaniesTab(); // Päivitä selector availableCompanies = availableCompanies.filter(c => c.id !== id); if (currentCompany && currentCompany.id === id) { currentCompany = availableCompanies[0] || null; if (currentCompany) switchCompany(currentCompany.id); } populateCompanySelector(); } catch (e) { alert(e.message); } } async function showCompanyDetail(id) { currentCompanyDetail = id; document.getElementById('companies-list-view').style.display = 'none'; document.getElementById('company-detail-view').style.display = ''; const comp = companiesTabData.find(c => c.id === id); document.getElementById('company-detail-title').textContent = (comp ? comp.nimi : id) + ' — Asetukset'; document.getElementById('company-edit-nimi').value = comp ? comp.nimi : ''; // Brändäyskentät document.getElementById('company-edit-subtitle').value = comp?.subtitle || ''; const color = comp?.primary_color || '#0f3460'; document.getElementById('company-edit-color').value = color; document.getElementById('company-edit-color-text').value = color; document.getElementById('company-edit-domains').value = (comp?.domains || []).join('\n'); // Logo-esikatselu const logoPreview = document.getElementById('company-logo-preview'); if (comp?.logo_file) { logoPreview.src = 'api.php?action=company_logo&company_id=' + encodeURIComponent(id) + '&t=' + Date.now(); logoPreview.style.display = ''; } else { logoPreview.style.display = 'none'; } // Moduuli-checkboxit const enabledMods = comp?.enabled_modules || []; document.querySelectorAll('#modules-checkboxes input[data-module]').forEach(cb => { const mod = cb.dataset.module; // Jos enabled_modules on tyhjä → kaikki päällä (oletus) cb.checked = enabledMods.length === 0 ? DEFAULT_MODULES.includes(mod) : enabledMods.includes(mod); }); // Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein await apiCall('company_switch', 'POST', { company_id: id }); // Lataa postilaatikot loadMailboxes(); // Lataa sijainnit loadSites(); // Lataa käyttäjäoikeudet loadCompanyUsers(id); } document.getElementById('btn-company-back').addEventListener('click', () => { // Vaihda takaisin alkuperäiseen yritykseen if (currentCompany) apiCall('company_switch', 'POST', { company_id: currentCompany.id }); renderCompaniesTable(); }); // Synkronoi color picker <-> text input document.getElementById('company-edit-color').addEventListener('input', function() { document.getElementById('company-edit-color-text').value = this.value; }); document.getElementById('company-edit-color-text').addEventListener('input', function() { if (/^#[0-9a-fA-F]{6}$/.test(this.value)) { document.getElementById('company-edit-color').value = this.value; } }); // Poimi hallitseva väri kuvasta (canvas) function extractDominantColor(file) { return new Promise((resolve) => { const img = new Image(); const url = URL.createObjectURL(file); img.onload = () => { const canvas = document.createElement('canvas'); const size = 50; // Pieni koko nopeuttaa canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, size, size); const pixels = ctx.getImageData(0, 0, size, size).data; URL.revokeObjectURL(url); // Laske värien esiintymät (ryhmiteltynä 32-askeleen tarkkuudella) const colorCounts = {}; for (let i = 0; i < pixels.length; i += 4) { const r = pixels[i], g = pixels[i+1], b = pixels[i+2], a = pixels[i+3]; if (a < 128) continue; // Ohita läpinäkyvät // Ohita lähes valkoiset, mustat ja harmaat const max = Math.max(r, g, b), min = Math.min(r, g, b); const saturation = max === 0 ? 0 : (max - min) / max; if (max > 230 && min > 200) continue; // Valkoinen if (max < 30) continue; // Musta if (saturation < 0.15 && max > 60) continue; // Harmaa // Ryhmittele const qr = Math.round(r / 32) * 32; const qg = Math.round(g / 32) * 32; const qb = Math.round(b / 32) * 32; const key = `${qr},${qg},${qb}`; colorCounts[key] = (colorCounts[key] || 0) + 1; } // Etsi yleisin let bestKey = null, bestCount = 0; for (const [key, count] of Object.entries(colorCounts)) { if (count > bestCount) { bestCount = count; bestKey = key; } } if (bestKey) { const [r, g, b] = bestKey.split(',').map(Number); const hex = '#' + [r, g, b].map(v => Math.min(255, v).toString(16).padStart(2, '0')).join(''); resolve(hex); } else { resolve(null); // Ei löytynyt selkeää väriä } }; img.onerror = () => { URL.revokeObjectURL(url); resolve(null); }; img.src = url; }); } // Logo-upload — poimi väri automaattisesti document.getElementById('company-logo-upload').addEventListener('change', async function() { if (!this.files[0] || !currentCompanyDetail) return; const file = this.files[0]; // Poimi väri logosta ennen uploadia const dominantColor = await extractDominantColor(file); const formData = new FormData(); formData.append('logo', file); formData.append('company_id', currentCompanyDetail); try { const res = await fetch('api.php?action=company_logo_upload', { method: 'POST', body: formData, credentials: 'include' }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Virhe'); // Päivitä preview const preview = document.getElementById('company-logo-preview'); preview.src = data.logo_url + '&t=' + Date.now(); preview.style.display = ''; // Päivitä paikallinen data const comp = companiesTabData.find(c => c.id === currentCompanyDetail); if (comp) comp.logo_file = data.logo_file; // Aseta logosta poimittu väri teemaväriksi if (dominantColor) { const colorInput = document.getElementById('company-edit-color'); if (colorInput) { colorInput.value = dominantColor; // Näytä ilmoitus const msg = document.createElement('span'); msg.textContent = ` Väri ${dominantColor} poimittu logosta`; msg.style.cssText = 'color:#27ae60;font-size:0.85rem;margin-left:8px;'; colorInput.parentElement.appendChild(msg); setTimeout(() => msg.remove(), 4000); } } } catch (e) { alert(e.message); } this.value = ''; // Reset file input }); document.getElementById('btn-save-company-settings').addEventListener('click', async () => { const nimi = document.getElementById('company-edit-nimi').value.trim(); if (!nimi) return; const subtitle = document.getElementById('company-edit-subtitle').value.trim(); const primary_color = document.getElementById('company-edit-color').value; const domainsText = document.getElementById('company-edit-domains').value; const domains = domainsText.split('\n').map(d => d.trim()).filter(d => d); // Moduulit const enabled_modules = []; document.querySelectorAll('#modules-checkboxes input[data-module]:checked').forEach(cb => { enabled_modules.push(cb.dataset.module); }); try { await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi, subtitle, primary_color, domains, enabled_modules }); alert('Asetukset tallennettu!'); // Päivitä paikalliset tiedot const comp = companiesTabData.find(c => c.id === currentCompanyDetail); if (comp) { comp.nimi = nimi; comp.subtitle = subtitle; comp.primary_color = primary_color; comp.domains = domains; comp.enabled_modules = enabled_modules; } const avail = availableCompanies.find(c => c.id === currentCompanyDetail); if (avail) avail.nimi = nimi; populateCompanySelector(); // Jos tämä on aktiivinen yritys → päivitä brändäys ja moduulit heti if (currentCompany && currentCompany.id === currentCompanyDetail) { applyBranding({ nimi, subtitle, primary_color, logo_url: comp?.logo_file ? 'api.php?action=company_logo&company_id=' + encodeURIComponent(currentCompanyDetail) + '&t=' + Date.now() : '' }); applyModules(enabled_modules); } } catch (e) { alert(e.message); } }); // ==================== POSTILAATIKOT ==================== let mailboxesData = []; async function loadMailboxes() { try { mailboxesData = await apiCall('mailboxes'); renderMailboxes(); } catch (e) { console.error(e); } } function renderMailboxes() { const container = document.getElementById('mailboxes-list'); if (mailboxesData.length === 0) { container.innerHTML = '

Ei postilaatikoita. Lisää ensimmäinen postilaatikko.

'; return; } container.innerHTML = mailboxesData.map(mb => `
${esc(mb.nimi)} ${esc(mb.imap_user)} ${mb.aktiivinen !== false ? 'Aktiivinen' : 'Ei aktiivinen'}
`).join(''); } document.getElementById('btn-add-mailbox').addEventListener('click', () => { showMailboxForm(); }); function showMailboxForm(mb = null) { document.getElementById('mailbox-form-title').textContent = mb ? 'Muokkaa postilaatikkoa' : 'Uusi postilaatikko'; document.getElementById('mailbox-form-id').value = mb ? mb.id : ''; document.getElementById('mailbox-form-nimi').value = mb ? mb.nimi : ''; document.getElementById('mailbox-form-host').value = mb ? mb.imap_host : ''; document.getElementById('mailbox-form-port').value = mb ? mb.imap_port : 993; document.getElementById('mailbox-form-user').value = mb ? mb.imap_user : ''; document.getElementById('mailbox-form-password').value = mb ? mb.imap_password : ''; document.getElementById('mailbox-form-encryption').value = mb ? (mb.imap_encryption || 'ssl') : 'ssl'; document.getElementById('mailbox-form-smtp-email').value = mb ? (mb.smtp_from_email || '') : ''; document.getElementById('mailbox-form-smtp-name').value = mb ? (mb.smtp_from_name || '') : ''; document.getElementById('mailbox-form-container').style.display = ''; } function editMailbox(id) { const mb = mailboxesData.find(m => m.id === id); if (mb) showMailboxForm(mb); } async function deleteMailbox(id, nimi) { if (!confirm(`Poistetaanko postilaatikko "${nimi}"?`)) return; try { await apiCall('mailbox_delete', 'POST', { id }); loadMailboxes(); } catch (e) { alert(e.message); } } document.getElementById('btn-save-mailbox').addEventListener('click', async () => { const data = { id: document.getElementById('mailbox-form-id').value || undefined, nimi: document.getElementById('mailbox-form-nimi').value, imap_host: document.getElementById('mailbox-form-host').value, imap_port: parseInt(document.getElementById('mailbox-form-port').value) || 993, imap_user: document.getElementById('mailbox-form-user').value, imap_password: document.getElementById('mailbox-form-password').value, imap_encryption: document.getElementById('mailbox-form-encryption').value, smtp_from_email: document.getElementById('mailbox-form-smtp-email').value, smtp_from_name: document.getElementById('mailbox-form-smtp-name').value, aktiivinen: true, }; try { await apiCall('mailbox_save', 'POST', data); document.getElementById('mailbox-form-container').style.display = 'none'; loadMailboxes(); } catch (e) { alert(e.message); } }); document.getElementById('btn-cancel-mailbox').addEventListener('click', () => { document.getElementById('mailbox-form-container').style.display = 'none'; }); // ==================== YRITYKSEN KÄYTTÄJÄOIKEUDET ==================== async function loadCompanyUsers(companyId) { try { const users = await apiCall('users'); const container = document.getElementById('company-users-list'); container.innerHTML = users.map(u => { const hasAccess = (u.companies || []).includes(companyId); return ``; }).join(''); } catch (e) { console.error(e); } } async function toggleCompanyUser(userId, companyId, add) { try { const users = await apiCall('users'); const user = users.find(u => u.id === userId); if (!user) return; let companies = user.companies || []; if (add && !companies.includes(companyId)) { companies.push(companyId); } else if (!add) { companies = companies.filter(c => c !== companyId); } await apiCall('user_update', 'POST', { id: userId, companies }); } catch (e) { alert(e.message); } } // ==================== LAITTEET (DEVICES) ==================== let devicesData = []; let sitesData = []; async function loadDevices() { try { devicesData = await apiCall('devices'); renderDevices(); } catch (e) { console.error(e); } } function renderDevices() { const query = (document.getElementById('device-search-input')?.value || '').toLowerCase().trim(); let filtered = devicesData; if (query) { filtered = devicesData.filter(d => (d.nimi || '').toLowerCase().includes(query) || (d.hallintaosoite || '').toLowerCase().includes(query) || (d.serial || '').toLowerCase().includes(query) || (d.site_name || '').toLowerCase().includes(query) || (d.funktio || '').toLowerCase().includes(query) || (d.tyyppi || '').toLowerCase().includes(query) || (d.malli || '').toLowerCase().includes(query) ); } const tbody = document.getElementById('device-tbody'); const noDevices = document.getElementById('no-devices'); if (filtered.length === 0) { tbody.innerHTML = ''; noDevices.style.display = 'block'; } else { noDevices.style.display = 'none'; tbody.innerHTML = filtered.map(d => { const pingIcon = d.ping_check ? (d.ping_status === 'up' ? '🟢' : (d.ping_status === 'down' ? '🔴' : '⚪')) : ''; return ` ${esc(d.nimi)} ${esc(d.hallintaosoite || '-')} ${esc(d.serial || '-')} ${d.site_name ? esc(d.site_name) : '-'} ${esc(d.funktio || '-')} ${esc(d.tyyppi || '-')} ${esc(d.malli || '-')} ${pingIcon} `; }).join(''); } document.getElementById('device-count').textContent = filtered.length + ' laitetta' + (query ? ` (${devicesData.length} yhteensä)` : ''); } async function editDevice(id) { const d = devicesData.find(x => x.id === id); if (!d) return; document.getElementById('device-form-id').value = d.id; document.getElementById('device-form-nimi').value = d.nimi || ''; document.getElementById('device-form-hallintaosoite').value = d.hallintaosoite || ''; document.getElementById('device-form-serial').value = d.serial || ''; document.getElementById('device-form-funktio').value = d.funktio || ''; document.getElementById('device-form-tyyppi').value = d.tyyppi || ''; document.getElementById('device-form-malli').value = d.malli || ''; document.getElementById('device-form-ping-check').checked = d.ping_check || false; document.getElementById('device-form-lisatiedot').value = d.lisatiedot || ''; await loadSitesForDropdown(); document.getElementById('device-form-site').value = d.site_id || ''; document.getElementById('device-modal-title').textContent = 'Muokkaa laitetta'; document.getElementById('device-modal').style.display = 'flex'; } async function deleteDevice(id, name) { if (!confirm(`Poistetaanko laite "${name}"?`)) return; try { await apiCall('device_delete', 'POST', { id }); loadDevices(); } catch (e) { alert(e.message); } } async function loadSitesForDropdown() { try { sitesData = await apiCall('sites'); const sel = document.getElementById('device-form-site'); sel.innerHTML = '' + sitesData.map(s => ``).join(''); } catch (e) { console.error(e); } } document.getElementById('btn-add-device')?.addEventListener('click', async () => { document.getElementById('device-form-id').value = ''; document.getElementById('device-form').reset(); await loadSitesForDropdown(); document.getElementById('device-modal-title').textContent = 'Lisää laite'; document.getElementById('device-modal').style.display = 'flex'; }); document.getElementById('device-modal-close')?.addEventListener('click', () => { document.getElementById('device-modal').style.display = 'none'; }); document.getElementById('device-form-cancel')?.addEventListener('click', () => { document.getElementById('device-modal').style.display = 'none'; }); document.getElementById('device-form')?.addEventListener('submit', async (e) => { e.preventDefault(); const id = document.getElementById('device-form-id').value; const data = { nimi: document.getElementById('device-form-nimi').value.trim(), hallintaosoite: document.getElementById('device-form-hallintaosoite').value.trim(), serial: document.getElementById('device-form-serial').value.trim(), site_id: document.getElementById('device-form-site').value || null, funktio: document.getElementById('device-form-funktio').value.trim(), tyyppi: document.getElementById('device-form-tyyppi').value.trim(), malli: document.getElementById('device-form-malli').value.trim(), ping_check: document.getElementById('device-form-ping-check').checked, lisatiedot: document.getElementById('device-form-lisatiedot').value.trim(), }; if (id) data.id = id; try { await apiCall('device_save', 'POST', data); document.getElementById('device-modal').style.display = 'none'; loadDevices(); } catch (e) { alert(e.message); } }); document.getElementById('device-search-input')?.addEventListener('input', () => renderDevices()); // ==================== SIJAINNIT (SITES) HALLINTA ==================== async function loadSites() { try { sitesData = await apiCall('sites'); renderSites(); } catch (e) { console.error(e); } } function renderSites() { const container = document.getElementById('sites-list'); if (!container) return; if (sitesData.length === 0) { container.innerHTML = '

Ei sijainteja. Lisää ensimmäinen sijainti.

'; return; } container.innerHTML = sitesData.map(s => `
${esc(s.nimi)} ${s.osoite ? `${esc(s.osoite)}` : ''} ${s.kaupunki ? `${esc(s.kaupunki)}` : ''}
`).join(''); } function editSite(id) { const s = sitesData.find(x => x.id === id); if (!s) return; document.getElementById('site-form-id').value = s.id; document.getElementById('site-form-nimi').value = s.nimi || ''; document.getElementById('site-form-osoite').value = s.osoite || ''; document.getElementById('site-form-kaupunki').value = s.kaupunki || ''; document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia'; document.getElementById('site-form-container').style.display = ''; } async function deleteSite(id, name) { if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return; try { await apiCall('site_delete', 'POST', { id }); loadSites(); } catch (e) { alert(e.message); } } document.getElementById('btn-add-site')?.addEventListener('click', () => { document.getElementById('site-form-id').value = ''; document.getElementById('site-form-nimi').value = ''; document.getElementById('site-form-osoite').value = ''; document.getElementById('site-form-kaupunki').value = ''; document.getElementById('site-form-title').textContent = 'Uusi sijainti'; document.getElementById('site-form-container').style.display = ''; }); document.getElementById('btn-save-site')?.addEventListener('click', async () => { const id = document.getElementById('site-form-id').value; const nimi = document.getElementById('site-form-nimi').value.trim(); if (!nimi) return alert('Sijainnin nimi vaaditaan'); const data = { nimi, osoite: document.getElementById('site-form-osoite').value.trim(), kaupunki: document.getElementById('site-form-kaupunki').value.trim(), }; if (id) data.id = id; try { await apiCall('site_save', 'POST', data); document.getElementById('site-form-container').style.display = 'none'; loadSites(); } catch (e) { alert(e.message); } }); document.getElementById('btn-cancel-site')?.addEventListener('click', () => { document.getElementById('site-form-container').style.display = 'none'; }); // ==================== MODUULIT ==================== const ALL_MODULES = ['customers', 'support', 'leads', 'devices', 'archive', 'changelog', 'settings']; const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings']; function applyModules(modules) { // Jos tyhjä array → kaikki moduulit päällä (fallback) const enabled = (modules && modules.length > 0) ? modules : ALL_MODULES; const isAdminUser = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'; ALL_MODULES.forEach(mod => { const tabBtn = document.querySelector(`.tab[data-tab="${mod}"]`); if (tabBtn) { // settings-tabi näkyy vain adminille/superadminille if (mod === 'settings') { tabBtn.style.display = (enabled.includes(mod) && isAdminUser) ? '' : 'none'; } else { tabBtn.style.display = enabled.includes(mod) ? '' : 'none'; } } }); // Jos aktiivinen tabi on piilotettu → vaihda ensimmäiseen näkyvään const activeTab = document.querySelector('.tab.active'); if (activeTab && activeTab.style.display === 'none') { const firstVisible = document.querySelector('.tab[data-tab]:not([style*="display: none"])'); if (firstVisible) switchToTab(firstVisible.dataset.tab); } } // ==================== BRANDING ==================== function applyBranding(branding) { const color = branding.primary_color || '#0f3460'; const nimi = branding.nimi || 'Noxus Intra'; const subtitle = branding.subtitle || ''; const logoUrl = branding.logo_url || ''; // CSS-muuttuja document.documentElement.style.setProperty('--primary-color', color); // Laske tumma variantti document.documentElement.style.setProperty('--primary-dark', color); // Login-sivu const loginLogo = document.getElementById('login-logo'); const loginTitle = document.getElementById('login-title'); const loginSubtitle = document.getElementById('login-subtitle'); if (loginLogo) { if (logoUrl) { loginLogo.src = logoUrl; loginLogo.style.display = ''; } else { loginLogo.style.display = 'none'; } } if (loginTitle) loginTitle.textContent = nimi; if (loginSubtitle) loginSubtitle.textContent = subtitle || 'Kirjaudu sisään'; // Muut login-boxien otsikot document.querySelectorAll('.login-brand-title').forEach(el => el.textContent = nimi); // Header const headerLogo = document.getElementById('header-logo'); const headerIcon = document.getElementById('header-brand-icon'); const headerTitle = document.getElementById('header-title'); const headerSubtitle = document.getElementById('header-subtitle'); if (headerLogo) { if (logoUrl) { headerLogo.src = logoUrl; headerLogo.style.display = ''; if (headerIcon) headerIcon.style.display = 'none'; } else { headerLogo.style.display = 'none'; if (headerIcon) headerIcon.style.display = ''; } } // Kun logo on, piilotetaan tekstit — logo riittää if (headerTitle) { headerTitle.style.display = logoUrl ? 'none' : ''; if (!logoUrl) headerTitle.textContent = nimi; } if (headerSubtitle) { headerSubtitle.style.display = logoUrl ? 'none' : ''; if (!logoUrl) headerSubtitle.textContent = subtitle || ''; } // Sivun title document.title = nimi; } async function loadBranding() { try { const data = await apiCall('branding'); applyBranding(data); } catch (e) { // Oletusbrändäys applyBranding({ nimi: 'Noxus Intra', primary_color: '#0f3460', subtitle: 'Hallintapaneeli', logo_url: '' }); } } // Init loadBranding(); loadCaptcha(); checkAuth();