const API = 'api.php'; let customers = []; let sortField = 'yritys'; let sortAsc = true; let currentDetailId = null; let currentUser = { username: '', nimi: '', role: '' }; // 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')) { 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 }; showDashboard(); } } catch (e) { /* not logged in */ } } 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 }; 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(); }); async function showDashboard() { loginScreen.style.display = 'none'; dashboard.style.display = 'block'; document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username; // Näytä Käyttäjät-tab vain adminille document.getElementById('tab-users').style.display = currentUser.role === 'admin' ? '' : 'none'; await loadCustomers(); } // ==================== TABS ==================== document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); tab.classList.add('active'); const target = tab.dataset.tab; document.getElementById('tab-content-' + target).classList.add('active'); // Lataa sisältö tarvittaessa if (target === 'archive') loadArchive(); if (target === 'changelog') loadChangelog(); if (target === 'users') loadUsers(); }); }); // ==================== 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; } // 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-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, 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(); }); // ==================== 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', }; 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 === 'admin' ? 'Ylläpitäjä' : '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'; 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 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, }; 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(); } catch (e) { alert(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'; }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { customerModal.style.display = 'none'; detailModal.style.display = 'none'; userModal.style.display = 'none'; } }); // Init loadCaptcha(); checkAuth();